add pdlp to the code base; build it with bazel, call it through MPSolver

This commit is contained in:
Laurent Perron
2022-02-09 10:48:30 +01:00
parent 921d1ab863
commit d510b97621
46 changed files with 15299 additions and 1 deletions

View File

@@ -91,3 +91,23 @@ new_git_repository(
tag = "v800",
remote = "https://github.com/scipopt/scip.git",
)
# Eigen has no Bazel build.
http_archive(
name = "eigen",
sha256 = "b4c198460eba6f28d34894e3a5710998818515104d6e74e5cc331ce31e46e626",
strip_prefix = "eigen-3.4.0",
urls = [
"https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.bz2",
],
build_file_content =
"""
cc_library(
name = 'eigen3',
srcs = [],
includes = ['.'],
hdrs = glob(['Eigen/**']),
visibility = ['//visibility:public'],
)
"""
)

View File

@@ -38,8 +38,8 @@ cc_library(
"integral_types.h",
"log_severity.h",
"logging.h",
"logging_flags.h",
"logging_export.h",
"logging_flags.h",
"logging_utilities.h",
"macros.h",
"raw_logging.h",

View File

@@ -60,6 +60,7 @@ cc_library(
"linear_solver.cc",
"lpi_glop.cpp",
"model_validator.cc",
"pdlp_interface.cc",
"sat_interface.cc",
"sat_proto_solver.cc",
"sat_solver_utils.cc",
@@ -102,6 +103,7 @@ cc_library(
":scip_with_glop",
":model_exporter",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/synchronization",
"@com_google_absl//absl/types:optional",
@@ -119,6 +121,9 @@ cc_library(
"//ortools/glop:parameters_cc_proto",
"//ortools/gscip:legacy_scip_params",
"//ortools/gurobi:environment",
"//ortools/pdlp:primal_dual_hybrid_gradient",
"//ortools/pdlp:solve_log_cc_proto",
"//ortools/pdlp:solvers_cc_proto",
"//ortools/port:file",
"//ortools/port:proto_utils",
"//ortools/sat:cp_model_cc_proto",

View File

@@ -1,4 +1,5 @@
file(GLOB _SRCS "*.h" "*.cc")
list(FILTER _SRCS EXCLUDE REGEX ".*/pdlp_interface.cc") # Needs eigen3.
if(USE_SCIP)
list(APPEND _SRCS ${LPI_GLOP_SRC})

View File

@@ -23,6 +23,7 @@
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
#include "absl/status/status.h"
@@ -69,6 +70,7 @@ namespace operations_research {
bool SolverTypeIsMip(MPModelRequest::SolverType solver_type) {
switch (solver_type) {
case MPModelRequest::PDLP_LINEAR_PROGRAMMING:
case MPModelRequest::GLOP_LINEAR_PROGRAMMING:
case MPModelRequest::CLP_LINEAR_PROGRAMMING:
case MPModelRequest::GLPK_LINEAR_PROGRAMMING:
@@ -530,6 +532,7 @@ constexpr
{MPSolver::BOP_INTEGER_PROGRAMMING, "bop"},
{MPSolver::GUROBI_MIXED_INTEGER_PROGRAMMING, "gurobi"},
{MPSolver::GLPK_MIXED_INTEGER_PROGRAMMING, "glpk"},
{MPSolver::PDLP_LINEAR_PROGRAMMING, "pdlp"},
{MPSolver::KNAPSACK_MIXED_INTEGER_PROGRAMMING, "knapsack"},
{MPSolver::CPLEX_MIXED_INTEGER_PROGRAMMING, "cplex"},
{MPSolver::XPRESS_MIXED_INTEGER_PROGRAMMING, "xpress"},

View File

@@ -193,6 +193,10 @@ class MPSolver {
CLP_LINEAR_PROGRAMMING = 0,
GLPK_LINEAR_PROGRAMMING = 1,
GLOP_LINEAR_PROGRAMMING = 2, // Recommended default value. Made in Google.
// In-house linear programming solver based on the primal-dual hybrid
// gradient method. Sometimes faster than Glop for medium-size problems and
// scales to much larger problems than Glop.
PDLP_LINEAR_PROGRAMMING = 8,
// Integer programming problems.
// -----------------------------
@@ -843,6 +847,7 @@ class MPSolver {
friend class GLOPInterface;
friend class BopInterface;
friend class SatInterface;
friend class PdlpInterface;
friend class KnapsackInterface;
// Debugging: verify that the given MPVariable* belongs to this solver.
@@ -1062,6 +1067,7 @@ class MPObjective {
friend class GLOPInterface;
friend class BopInterface;
friend class SatInterface;
friend class PdlpInterface;
friend class KnapsackInterface;
// Constructor. An objective points to a single MPSolverInterface
@@ -1171,6 +1177,7 @@ class MPVariable {
friend class MPVariableSolutionValueTest;
friend class BopInterface;
friend class SatInterface;
friend class PdlpInterface;
friend class KnapsackInterface;
// Constructor. A variable points to a single MPSolverInterface that
@@ -1312,6 +1319,7 @@ class MPConstraint {
friend class GLOPInterface;
friend class BopInterface;
friend class SatInterface;
friend class PdlpInterface;
friend class KnapsackInterface;
// Constructor. A constraint points to a single MPSolverInterface

View File

@@ -293,6 +293,29 @@ message MPModelProto {
// try to return a solution "close" to this assignment in case of multiple
// optimal solutions.
optional PartialVariableAssignment solution_hint = 6;
// Annotations can be freely added by users who want to attach arbitrary
// payload to the model's variables or constraints.
message Annotation {
// The target of an Annotation is a single entity (e.g. a variable).
// Several Annotations may apply to the same entity.
enum TargetType {
VARIABLE_DEFAULT = 0;
CONSTRAINT = 1;
GENERAL_CONSTRAINT = 2;
}
optional TargetType target_type = 1;
// If both `target_index` and `target_name` are set, they must point to the
// same entity.
optional int32 target_index = 2; // Index in the MPModelProto.
optional string target_name = 3; // Alternate to index. Assumes uniqueness.
// The payload is a (key, value) string pair. Depending on the use cases,
// one of the two may be omitted.
optional string payload_key = 4;
optional string payload_value = 5;
}
repeated Annotation annotation = 9;
}
// To support 'unspecified' double value in proto3, the simplest is to wrap
@@ -417,6 +440,11 @@ message MPModelRequest {
// variables).
SAT_INTEGER_PROGRAMMING = 14;
// In-house linear programming solver based on the primal-dual hybrid
// gradient method. Sometimes faster than Glop for medium-size problems and
// scales to much larger problems than Glop.
PDLP_LINEAR_PROGRAMMING = 8;
KNAPSACK_MIXED_INTEGER_PROGRAMMING = 13;
}
optional SolverType solver_type = 2;

View File

@@ -20,12 +20,14 @@
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/types/optional.h"
#include "ortools/base/accurate_sum.h"
#include "ortools/base/commandlineflags.h"
#include "ortools/base/map_util.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/port/file.h"
#include "ortools/port/proto_utils.h"
@@ -418,6 +420,133 @@ std::string FindErrorInSolutionHint(
return std::string();
}
namespace {
// Maps the names of variables (or constraints, or other entities) to their
// index in the MPModelProto. Non-unique names are supported, but are singled
// out as such, by setting their index (the 'value' of the map entry) to -1.
template <class NamedEntity>
absl::flat_hash_map<std::string, int> BuildNameToIndexMap(
const google::protobuf::RepeatedPtrField<NamedEntity>& entities) {
absl::flat_hash_map<std::string, int> out;
for (int i = 0; i < entities.size(); ++i) {
int& index = gtl::LookupOrInsert(&out, entities.Get(i).name(), i);
if (index != i) index = -1;
}
return out;
}
class LazyMPModelNameToIndexMaps {
public:
explicit LazyMPModelNameToIndexMaps(const MPModelProto& model)
: model_(model) {}
absl::StatusOr<int> LookupName(
MPModelProto::Annotation::TargetType target_type,
const std::string& name) {
const absl::flat_hash_map<std::string, int>* map = nullptr;
switch (target_type) {
case MPModelProto::Annotation::VARIABLE_DEFAULT:
if (!variable_name_to_index_) {
variable_name_to_index_ = BuildNameToIndexMap(model_.variable());
}
map = &variable_name_to_index_.value();
break;
case MPModelProto::Annotation::CONSTRAINT:
if (!constraint_name_to_index_) {
constraint_name_to_index_ = BuildNameToIndexMap(model_.constraint());
}
map = &constraint_name_to_index_.value();
break;
case MPModelProto::Annotation::GENERAL_CONSTRAINT:
if (!general_constraint_name_to_index_) {
general_constraint_name_to_index_ =
BuildNameToIndexMap(model_.general_constraint());
}
map = &general_constraint_name_to_index_.value();
break;
}
const int index = gtl::FindWithDefault(*map, name, -2);
if (index == -2) return absl::NotFoundError("name not found");
if (index == -1) return absl::InvalidArgumentError("name is not unique");
return index;
}
private:
const MPModelProto& model_;
std::optional<absl::flat_hash_map<std::string, int>> variable_name_to_index_;
std::optional<absl::flat_hash_map<std::string, int>>
constraint_name_to_index_;
std::optional<absl::flat_hash_map<std::string, int>>
general_constraint_name_to_index_;
};
} // namespace
std::string FindErrorInAnnotation(const MPModelProto::Annotation& annotation,
const MPModelProto& model,
LazyMPModelNameToIndexMaps* name_maps) {
// Checks related to the 'target' fields.
if (!annotation.has_target_index() && !annotation.has_target_name()) {
return "One of target_index or target_name must be set";
}
if (!MPModelProto::Annotation::TargetType_IsValid(annotation.target_type())) {
return "Invalid target_type";
}
int num_entitities = -1;
switch (annotation.target_type()) {
case MPModelProto::Annotation::VARIABLE_DEFAULT:
num_entitities = model.variable_size();
break;
case MPModelProto::Annotation::CONSTRAINT:
num_entitities = model.constraint_size();
break;
case MPModelProto::Annotation::GENERAL_CONSTRAINT:
num_entitities = model.general_constraint_size();
break;
}
int target_index = -1;
if (annotation.has_target_index()) {
target_index = annotation.target_index();
if (target_index < 0 || target_index >= num_entitities) {
return "Invalid target_index";
}
}
if (annotation.has_target_name()) {
if (annotation.has_target_index()) {
// No need to build the name lookup maps to verify consistency: we can
// even accept a name that is not unique, as long as the pointed entity
// (identified by its index) has the right name.
std::string name;
switch (annotation.target_type()) {
case MPModelProto::Annotation::VARIABLE_DEFAULT:
name = model.variable(target_index).name();
break;
case MPModelProto::Annotation::CONSTRAINT:
name = model.constraint(target_index).name();
break;
case MPModelProto::Annotation::GENERAL_CONSTRAINT:
name = model.general_constraint(target_index).name();
break;
}
if (annotation.target_name() != name) {
return absl::StrFormat(
"target_name='%s' doesn't match the name '%s' of target_index=%d",
annotation.target_name(), name, target_index);
}
} else { // !annotation.has_target_index()
const absl::StatusOr<int> index_or = name_maps->LookupName(
annotation.target_type(), annotation.target_name());
if (!index_or.ok()) {
return absl::StrCat("Bad target_name: ", index_or.status().message());
}
target_index = index_or.value();
}
}
// As of 2022-02, there are no checks related to the 'payload' fields. They
// can be set, unset, everything goes.
return "";
}
} // namespace
std::string FindErrorInMPModelProto(
@@ -534,6 +663,17 @@ std::string FindErrorInMPModelProto(
return absl::StrCat("In solution_hint(): ", error);
}
// Validate the annotations.
{
LazyMPModelNameToIndexMaps name_maps(model);
for (int a = 0; a < model.annotation_size(); ++a) {
error = FindErrorInAnnotation(model.annotation(a), model, &name_maps);
if (!error.empty()) {
return absl::StrCat("In annotation #", a, ": ", error);
}
}
}
return std::string();
}

View File

@@ -0,0 +1,328 @@
// Copyright 2010-2021 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 <atomic>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "absl/base/attributes.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/types/optional.h"
#include "ortools/base/logging.h"
#include "ortools/linear_solver/linear_solver.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/linear_solver/model_validator.h"
#include "ortools/pdlp/primal_dual_hybrid_gradient.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/solvers.pb.h"
#include "ortools/port/proto_utils.h"
#include "ortools/util/lazy_mutable_copy.h"
namespace operations_research {
class PdlpInterface : public MPSolverInterface {
public:
explicit PdlpInterface(MPSolver* const solver);
~PdlpInterface() override;
// ----- Solve -----
MPSolver::ResultStatus Solve(const MPSolverParameters& param) override;
absl::optional<MPSolutionResponse> DirectlySolveProto(
const MPModelRequest& request, std::atomic<bool>* interrupt) override;
// ----- Model modifications and extraction -----
void Reset() override;
void SetOptimizationDirection(bool maximize) override;
void SetVariableBounds(int index, double lb, double ub) override;
void SetVariableInteger(int index, bool integer) override;
void SetConstraintBounds(int index, double lb, double ub) override;
void AddRowConstraint(MPConstraint* const ct) override;
void AddVariable(MPVariable* const var) override;
void SetCoefficient(MPConstraint* const constraint,
const MPVariable* const variable, double new_value,
double old_value) override;
void ClearConstraint(MPConstraint* const constraint) override;
void SetObjectiveCoefficient(const MPVariable* const variable,
double coefficient) override;
void SetObjectiveOffset(double value) override;
void ClearObjective() override;
// ------ Query statistics on the solution and the solve ------
int64_t iterations() const override;
int64_t nodes() const override;
MPSolver::BasisStatus row_status(int constraint_index) const override;
MPSolver::BasisStatus column_status(int variable_index) const override;
// ----- Misc -----
bool IsContinuous() const override;
bool IsLP() const override;
bool IsMIP() const override;
std::string SolverVersion() const override;
void* underlying_solver() override;
void ExtractNewVariables() override;
void ExtractNewConstraints() override;
void ExtractObjective() override;
void SetParameters(const MPSolverParameters& param) override;
void SetRelativeMipGap(double value) override;
void SetPrimalTolerance(double value) override;
void SetDualTolerance(double value) override;
void SetPresolveMode(int value) override;
void SetScalingMode(int value) override;
void SetLpAlgorithm(int value) override;
bool SetSolverSpecificParametersAsString(
const std::string& parameters) override;
absl::Status SetNumThreads(int num_threads) override;
private:
void NonIncrementalChange();
pdlp::PrimalDualHybridGradientParams parameters_;
pdlp::SolveLog solve_log_;
};
PdlpInterface::PdlpInterface(MPSolver* const solver)
: MPSolverInterface(solver) {}
PdlpInterface::~PdlpInterface() {}
MPSolver::ResultStatus PdlpInterface::Solve(const MPSolverParameters& param) {
// Reset extraction as this interface is not incremental yet.
Reset();
ExtractModel();
SetParameters(param);
solver_->SetSolverSpecificParametersAsString(
solver_->solver_specific_parameter_string_);
// Time limit.
if (solver_->time_limit()) {
VLOG(1) << "Setting time limit = " << solver_->time_limit() << " ms.";
parameters_.mutable_termination_criteria()->set_time_sec_limit(
static_cast<double>(solver_->time_limit()) / 1000.0);
}
// Mark variables and constraints as extracted.
for (int i = 0; i < solver_->variables_.size(); ++i) {
set_variable_as_extracted(i, true);
}
for (int i = 0; i < solver_->constraints_.size(); ++i) {
set_constraint_as_extracted(i, true);
}
MPModelProto model_proto;
solver_->ExportModelToProto(&model_proto);
// TODO(user): Adjust logging level based on quiet_.
absl::StatusOr<pdlp::SolutionResponseAndLog> solution =
pdlp::SolveMpModelProto(model_proto, parameters_,
/*relax_integer_variables=*/true);
if (!solution.ok()) {
LOG(ERROR) << "Unexpected error solving with PDLP: " << solution.status();
return MPSolver::ABNORMAL;
}
const MPSolutionResponse& response = solution->response;
// The solution must be marked as synchronized even when no solution exists.
sync_status_ = SOLUTION_SYNCHRONIZED;
result_status_ = static_cast<MPSolver::ResultStatus>(response.status());
solve_log_ = solution->solve_log;
if (response.status() == MPSOLVER_FEASIBLE ||
response.status() == MPSOLVER_OPTIMAL) {
const absl::Status result = solver_->LoadSolutionFromProto(response);
if (!result.ok()) {
LOG(ERROR) << "LoadSolutionFromProto failed: " << result;
}
}
return result_status_;
}
absl::optional<MPSolutionResponse> PdlpInterface::DirectlySolveProto(
const MPModelRequest& request, std::atomic<bool>* interrupt) {
if (interrupt != nullptr) {
return absl::nullopt;
}
MPSolutionResponse response;
if (request.has_solver_specific_parameters()) {
if (!solver_->SetSolverSpecificParametersAsString(
request.solver_specific_parameters())) {
response.set_status(
MPSolverResponseStatus::MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS);
return response;
}
}
// TODO(user): Adjust logging level based on enable_internal_solver_output.
if (request.has_solver_time_limit_seconds()) {
VLOG(1) << "Setting time limit = " << request.solver_time_limit_seconds()
<< " s.";
parameters_.mutable_termination_criteria()->set_time_sec_limit(
request.solver_time_limit_seconds());
}
const absl::optional<LazyMutableCopy<MPModelProto>> optional_model =
ExtractValidMPModelOrPopulateResponseStatus(request, &response);
if (!optional_model) {
LOG_IF(WARNING, request.enable_internal_solver_output())
<< "Failed to extract a valid model from protocol buffer. Status: "
<< ProtoEnumToString<MPSolverResponseStatus>(response.status()) << " ("
<< response.status() << "): " << response.status_str();
return absl::nullopt;
}
absl::StatusOr<pdlp::SolutionResponseAndLog> solution =
pdlp::SolveMpModelProto(optional_model->get(), parameters_,
/*relax_integer_variables=*/true);
if (!solution.ok()) {
LOG(ERROR) << "Unexpected error solving with PDLP: " << solution.status();
response.set_status(MPSolverResponseStatus::MPSOLVER_ABNORMAL);
response.set_status_str(solution.status().ToString());
return response;
}
return solution->response;
}
void PdlpInterface::Reset() { ResetExtractionInformation(); }
void PdlpInterface::SetOptimizationDirection(bool maximize) {
NonIncrementalChange();
}
void PdlpInterface::SetVariableBounds(int index, double lb, double ub) {
NonIncrementalChange();
}
void PdlpInterface::SetVariableInteger(int index, bool integer) {
NonIncrementalChange();
}
void PdlpInterface::SetConstraintBounds(int index, double lb, double ub) {
NonIncrementalChange();
}
void PdlpInterface::AddRowConstraint(MPConstraint* const ct) {
NonIncrementalChange();
}
void PdlpInterface::AddVariable(MPVariable* const var) {
NonIncrementalChange();
}
void PdlpInterface::SetCoefficient(MPConstraint* const constraint,
const MPVariable* const variable,
double new_value, double old_value) {
NonIncrementalChange();
}
void PdlpInterface::ClearConstraint(MPConstraint* const constraint) {
NonIncrementalChange();
}
void PdlpInterface::SetObjectiveCoefficient(const MPVariable* const variable,
double coefficient) {
NonIncrementalChange();
}
void PdlpInterface::SetObjectiveOffset(double value) { NonIncrementalChange(); }
void PdlpInterface::ClearObjective() { NonIncrementalChange(); }
int64_t PdlpInterface::iterations() const {
return solve_log_.iteration_count();
}
int64_t PdlpInterface::nodes() const {
LOG(DFATAL) << "Number of nodes only available for discrete problems";
return MPSolverInterface::kUnknownNumberOfNodes;
}
MPSolver::BasisStatus PdlpInterface::row_status(int constraint_index) const {
// TODO(user): While basis status isn't well defined for PDLP, we could
// guess statuses that might be useful.
return MPSolver::BasisStatus::FREE;
}
MPSolver::BasisStatus PdlpInterface::column_status(int variable_index) const {
// TODO(user): While basis status isn't well defined for PDLP, we could
// guess statuses that might be useful.
return MPSolver::BasisStatus::FREE;
}
bool PdlpInterface::IsContinuous() const { return true; }
bool PdlpInterface::IsLP() const { return true; }
bool PdlpInterface::IsMIP() const { return false; }
std::string PdlpInterface::SolverVersion() const { return "PDLP Solver"; }
// TODO(user): Consider returning the SolveLog here, as it could be essential
// for interpreting the PDLP solution.
void* PdlpInterface::underlying_solver() { return nullptr; }
void PdlpInterface::ExtractNewVariables() { NonIncrementalChange(); }
void PdlpInterface::ExtractNewConstraints() { NonIncrementalChange(); }
void PdlpInterface::ExtractObjective() { NonIncrementalChange(); }
void PdlpInterface::SetParameters(const MPSolverParameters& param) {
SetCommonParameters(param);
}
absl::Status PdlpInterface::SetNumThreads(int num_threads) {
if (num_threads < 1) {
return absl::InvalidArgumentError(
absl::StrCat("Invalid number of threads: ", num_threads));
}
parameters_.set_num_threads(num_threads);
return absl::OkStatus();
}
// These have no effect. Use SetSolverSpecificParametersAsString instead.
void PdlpInterface::SetPrimalTolerance(double value) {}
void PdlpInterface::SetDualTolerance(double value) {}
void PdlpInterface::SetScalingMode(int value) {}
void PdlpInterface::SetLpAlgorithm(int value) {}
void PdlpInterface::SetRelativeMipGap(double value) {}
void PdlpInterface::SetPresolveMode(int value) {}
bool PdlpInterface::SetSolverSpecificParametersAsString(
const std::string& parameters) {
return ProtobufTextFormatMergeFromString(parameters, &parameters_);
}
void PdlpInterface::NonIncrementalChange() {
// The current implementation is not incremental.
sync_status_ = MUST_RELOAD;
}
// Register PDLP in the global linear solver factory.
MPSolverInterface* BuildPdlpInterface(MPSolver* const solver) {
return new PdlpInterface(solver);
}
} // namespace operations_research

View File

@@ -265,6 +265,7 @@ void SatInterface::ExtractNewConstraints() { NonIncrementalChange(); }
void SatInterface::ExtractObjective() { NonIncrementalChange(); }
void SatInterface::SetParameters(const MPSolverParameters& param) {
parameters_.Clear();
parameters_.set_num_search_workers(num_threads_);
SetCommonParameters(param);
}

387
ortools/pdlp/BUILD.bazel Normal file
View File

@@ -0,0 +1,387 @@
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
package(default_visibility = ["//visibility:public"])
proto_library(
name = "solve_log_proto",
srcs = ["solve_log.proto"],
deps = [":solvers_proto"],
)
cc_proto_library(
name = "solve_log_cc_proto",
deps = [":solve_log_proto"],
)
proto_library(
name = "solvers_proto",
srcs = ["solvers.proto"],
deps = ["//ortools/glop:parameters_proto"],
)
cc_proto_library(
name = "solvers_cc_proto",
deps = [":solvers_proto"],
)
cc_library(
name = "gtest_main",
srcs = ["gtest_main.cc"],
deps = [
"//ortools/base",
"@com_google_googletest//:gtest",
],
)
cc_library(
name = "sharder",
srcs = ["sharder.cc"],
hdrs = ["sharder.h"],
deps = [
"//ortools/base",
"//ortools/base:mathutil",
"//ortools/base:threadpool",
"//ortools/base:timer",
"@com_google_absl//absl/synchronization",
"@com_google_absl//absl/time",
"@eigen//:eigen3",
],
)
cc_test(
name = "sharder_test",
size = "small",
srcs = ["sharder_test.cc"],
deps = [
":gtest_main",
":sharder",
"//ortools/base",
"//ortools/base:mathutil",
"@com_google_absl//absl/random:distributions",
"@com_google_absl//absl/strings",
"@eigen//:eigen3",
],
)
cc_library(
name = "quadratic_program",
srcs = ["quadratic_program.cc"],
hdrs = ["quadratic_program.h"],
deps = [
"//ortools/base",
"//ortools/base:status_macros",
"//ortools/linear_solver:linear_solver_cc_proto",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/types:optional",
"@eigen//:eigen3",
],
)
cc_test(
name = "quadratic_program_test",
size = "small",
srcs = ["quadratic_program_test.cc"],
deps = [
":gtest_main",
":quadratic_program",
":test_util",
"//ortools/base:protobuf_util",
"//ortools/linear_solver:linear_solver_cc_proto",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/types:optional",
"@eigen//:eigen3",
],
)
cc_library(
name = "test_util",
srcs = ["test_util.cc"],
hdrs = ["test_util.h"],
deps = [
":quadratic_program",
"//ortools/base",
"@com_google_absl//absl/types:span",
"@com_google_googletest//:gtest",
"@eigen//:eigen3",
],
)
cc_test(
name = "test_util_test",
srcs = ["test_util_test.cc"],
deps = [
":gtest_main",
":test_util",
"//ortools/base",
"@com_google_absl//absl/types:span",
"@eigen//:eigen3",
],
)
cc_library(
name = "sharded_quadratic_program",
srcs = ["sharded_quadratic_program.cc"],
hdrs = ["sharded_quadratic_program.h"],
deps = [
":quadratic_program",
":sharder",
"//ortools/base",
"//ortools/base:threadpool",
"@com_google_absl//absl/memory",
"@com_google_absl//absl/strings",
"@eigen//:eigen3",
],
)
cc_test(
name = "sharded_quadratic_program_test",
srcs = ["sharded_quadratic_program_test.cc"],
deps = [
":gtest_main",
":quadratic_program",
":sharded_quadratic_program",
":sharder",
":test_util",
"@eigen//:eigen3",
],
)
cc_library(
name = "sharded_optimization_utils",
srcs = ["sharded_optimization_utils.cc"],
hdrs = ["sharded_optimization_utils.h"],
deps = [
":quadratic_program",
":sharded_quadratic_program",
":sharder",
":solve_log_cc_proto",
"//ortools/base",
"//ortools/base:mathutil",
"@com_google_absl//absl/algorithm:container",
"@com_google_absl//absl/random:distributions",
"@com_google_absl//absl/types:optional",
"@eigen//:eigen3",
],
)
cc_test(
name = "sharded_optimization_utils_test",
size = "small",
srcs = ["sharded_optimization_utils_test.cc"],
deps = [
":gtest_main",
":quadratic_program",
":sharded_optimization_utils",
":sharded_quadratic_program",
":sharder",
":solve_log_cc_proto",
":test_util",
"@com_google_absl//absl/types:optional",
"@eigen//:eigen3",
],
)
cc_library(
name = "termination",
srcs = ["termination.cc"],
hdrs = ["termination.h"],
deps = [
":solve_log_cc_proto",
":solvers_cc_proto",
"//ortools/base",
"@com_google_absl//absl/types:optional",
],
)
cc_test(
name = "termination_test",
size = "small",
srcs = ["termination_test.cc"],
deps = [
":gtest_main",
":solve_log_cc_proto",
":solvers_cc_proto",
":termination",
"//ortools/base:protobuf_util",
"@com_google_absl//absl/types:optional",
],
)
cc_library(
name = "trust_region",
srcs = ["trust_region.cc"],
hdrs = ["trust_region.h"],
deps = [
":quadratic_program",
":sharded_optimization_utils",
":sharded_quadratic_program",
":sharder",
"//ortools/base",
"//ortools/base:mathutil",
"@com_google_absl//absl/algorithm:container",
"@com_google_absl//absl/types:optional",
"@eigen//:eigen3",
],
)
cc_test(
name = "trust_region_test",
srcs = ["trust_region_test.cc"],
deps = [
":gtest_main",
":quadratic_program",
":sharded_optimization_utils",
":sharded_quadratic_program",
":sharder",
":test_util",
":trust_region",
"@eigen//:eigen3",
],
)
cc_library(
name = "solvers_proto_validation",
srcs = ["solvers_proto_validation.cc"],
hdrs = ["solvers_proto_validation.h"],
deps = [
":solvers_cc_proto",
"//ortools/base:status_macros",
"@com_google_absl//absl/status",
],
)
cc_test(
name = "solvers_proto_validation_test",
srcs = ["solvers_proto_validation_test.cc"],
deps = [
":gtest_main",
":solvers_cc_proto",
":solvers_proto_validation",
"@com_google_absl//absl/status",
],
)
cc_library(
name = "quadratic_program_io",
srcs = ["quadratic_program_io.cc"],
hdrs = ["quadratic_program_io.h"],
deps = [
":quadratic_program",
"//ortools/base",
"//ortools/base:mathutil",
"//ortools/base:status_macros",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/linear_solver:model_exporter",
"//ortools/lp_data:mps_reader",
"//ortools/util:file_util",
"@com_google_absl//absl/base",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/types:optional",
"@eigen//:eigen3",
],
)
cc_library(
name = "iteration_stats",
srcs = ["iteration_stats.cc"],
hdrs = ["iteration_stats.h"],
deps = [
":quadratic_program",
":sharded_quadratic_program",
":sharder",
":solve_log_cc_proto",
":solvers_cc_proto",
"//ortools/base",
"//ortools/base:mathutil",
"@com_google_absl//absl/random:distributions",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/types:optional",
"@eigen//:eigen3",
],
)
cc_test(
name = "iteration_stats_test",
srcs = ["iteration_stats_test.cc"],
deps = [
":gtest_main",
":iteration_stats",
":quadratic_program",
":sharded_quadratic_program",
":solve_log_cc_proto",
":test_util",
"//ortools/base:protobuf_util",
"@com_google_absl//absl/types:optional",
"@eigen//:eigen3",
],
)
cc_library(
name = "primal_dual_hybrid_gradient",
srcs = ["primal_dual_hybrid_gradient.cc"],
hdrs = ["primal_dual_hybrid_gradient.h"],
deps = [
":iteration_stats",
":quadratic_program",
":sharded_optimization_utils",
":sharded_quadratic_program",
":sharder",
":solve_log_cc_proto",
":solvers_cc_proto",
":solvers_proto_validation",
":termination",
":trust_region",
"//ortools/base",
"//ortools/base:mathutil",
"//ortools/base:status_macros",
"//ortools/base:timer",
"//ortools/glop:parameters_cc_proto",
"//ortools/glop:preprocessor",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/lp_data",
"//ortools/lp_data:base",
"//ortools/lp_data:proto_utils",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/time",
"@com_google_absl//absl/types:optional",
"@eigen//:eigen3",
],
)
cc_test(
name = "primal_dual_hybrid_gradient_test",
size = "medium",
srcs = ["primal_dual_hybrid_gradient_test.cc"],
shard_count = 3,
deps = [
":gtest_main",
":iteration_stats",
":primal_dual_hybrid_gradient",
":quadratic_program",
":quadratic_program_io",
":sharded_quadratic_program",
":solve_log_cc_proto",
":solvers_cc_proto",
":test_util",
"//ortools/base:protobuf_util",
"//ortools/linear_solver",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/lp_data",
"//ortools/lp_data:base",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/types:optional",
"@eigen//:eigen3",
],
)

View File

@@ -0,0 +1,23 @@
// Copyright 2010-2021 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 "gtest/gtest.h"
#include "ortools/base/commandlineflags.h"
#include "ortools/base/logging.h"
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
google::InitGoogleLogging(argv[0]);
absl::ParseCommandLine(argc, argv);
return RUN_ALL_TESTS();
}

View File

@@ -0,0 +1,552 @@
// Copyright 2010-2021 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/iteration_stats.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <random>
#include <string>
#include <utility>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/random/distributions.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "absl/types/optional.h"
#include "ortools/base/logging.h"
#include "ortools/base/mathutil.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/pdlp/sharded_quadratic_program.h"
#include "ortools/pdlp/sharder.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/solvers.pb.h"
namespace operations_research::pdlp {
namespace {
using ::Eigen::VectorXd;
// ResidualNorms contains four measures of the infeasibility of a primal or
// dual solution. "objective_correction" is the (additive) adjustment to the
// objective function from the reduced costs. "objective_full_correction" is the
// (additive) adjustment to the objective function if all dual residuals were
// set to zero, while l_inf_residual and l_2_residual are the L_infinity and L_2
// norms of the residuals (portions of the primal gradient not included in the
// reduced costs).
struct ResidualNorms {
double objective_correction;
double objective_full_correction;
double l_inf_residual;
double l_2_residual;
};
// Computes norms of the primal residual infeasibilities (b - A x) of the
// unscaled problem. Note the primal residuals of the unscaled problem are equal
// to those of the scaled problem divided by row_scaling_vec. Does not perform
// any corrections (so the returned .correction == 0.0). sharded_qp is assumed
// to be the scaled problem. If use_homogeneous_primal_bounds is set to true
// the residuals are computed with upper and lower bounds zeroed out (note that
// we only zero out the bounds that are finite in the original problem).
ResidualNorms PrimalResidualNorms(
const ShardedQuadraticProgram& sharded_qp, const VectorXd& row_scaling_vec,
const VectorXd& scaled_primal_solution,
bool use_homogeneous_constraint_bounds = false) {
const QuadraticProgram& qp = sharded_qp.Qp();
CHECK_EQ(row_scaling_vec.size(), sharded_qp.DualSize());
CHECK_EQ(scaled_primal_solution.size(), sharded_qp.PrimalSize());
VectorXd primal_product = TransposedMatrixVectorProduct(
sharded_qp.TransposedConstraintMatrix(), scaled_primal_solution,
sharded_qp.TransposedConstraintMatrixSharder());
VectorXd local_l_inf_residual(sharded_qp.DualSharder().NumShards());
VectorXd local_sumsq_residual(sharded_qp.DualSharder().NumShards());
sharded_qp.DualSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
const auto lower_bound_shard = shard(qp.constraint_lower_bounds);
const auto upper_bound_shard = shard(qp.constraint_upper_bounds);
const auto row_scaling_shard = shard(row_scaling_vec);
const auto primal_product_shard = shard(primal_product);
double l_inf_residual = 0.0;
double sumsq_residual = 0.0;
for (int64_t i = 0; i < primal_product_shard.size(); ++i) {
const double upper_bound = (use_homogeneous_constraint_bounds &&
std::isfinite(upper_bound_shard[i]))
? 0.0
: upper_bound_shard[i];
const double lower_bound = (use_homogeneous_constraint_bounds &&
std::isfinite(lower_bound_shard[i]))
? 0.0
: lower_bound_shard[i];
double scaled_residual = 0.0;
if (primal_product_shard[i] > upper_bound) {
scaled_residual = primal_product_shard[i] - upper_bound;
} else if (primal_product_shard[i] < lower_bound) {
scaled_residual = lower_bound - primal_product_shard[i];
}
const double residual = scaled_residual / row_scaling_shard[i];
l_inf_residual = std::max(l_inf_residual, residual);
sumsq_residual += residual * residual;
}
local_l_inf_residual[shard.Index()] = l_inf_residual;
local_sumsq_residual[shard.Index()] = sumsq_residual;
});
return ResidualNorms{
.objective_correction = 0.0,
.objective_full_correction = 0.0,
.l_inf_residual = local_l_inf_residual.lpNorm<Eigen::Infinity>(),
.l_2_residual = std::sqrt(local_sumsq_residual.sum()),
};
}
// Decides whether a primal gradient term should be handled as a reduced cost or
// as a dual residual.
bool HandlePrimalGradientTermAsReducedCost(double primal_gradient,
double primal_value,
double lower_bound,
double upper_bound) {
if (primal_gradient == 0.0) return true;
return std::abs(primal_value -
(primal_gradient > 0.0 ? lower_bound : upper_bound)) <=
std::abs(primal_value);
}
// Computes norms of the dual residuals and reduced costs of the unscaled
// problem. Note the primal gradient of the unscaled problem is equal to the
// scaled primal gradient divided by col_scaling_vec. sharded_qp is assumed to
// be the scaled problem. See
// https://developers.google.com/optimization/lp/pdlp_math for details and
// notation. Primal gradients that have corresponding (finite) bounds (the
// finite terms from (l^v)^T[r]_+ (u^v)^T[r]_ in the dual objective), and
// have |x - b| <= |x| (where x is the variable's value and b is the
// corresponding bound) are treated as reduced costs and accumulated in
// objective_correction, while the other primal gradient terms are handled as
// residual infeasibilities in l_inf_residual and l_2_residual.
ResidualNorms DualResidualNorms(const ShardedQuadraticProgram& sharded_qp,
const VectorXd& col_scaling_vec,
const VectorXd& scaled_primal_solution,
const VectorXd& scaled_primal_gradient) {
const QuadraticProgram& qp = sharded_qp.Qp();
CHECK_EQ(col_scaling_vec.size(), sharded_qp.PrimalSize());
CHECK_EQ(scaled_primal_gradient.size(), sharded_qp.PrimalSize());
VectorXd local_dual_correction(sharded_qp.PrimalSharder().NumShards());
VectorXd local_dual_full_correction(sharded_qp.PrimalSharder().NumShards());
VectorXd local_l_inf_residual(sharded_qp.PrimalSharder().NumShards());
VectorXd local_sumsq_residual(sharded_qp.PrimalSharder().NumShards());
sharded_qp.PrimalSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
const auto lower_bound_shard = shard(qp.variable_lower_bounds);
const auto upper_bound_shard = shard(qp.variable_upper_bounds);
const auto primal_gradient_shard = shard(scaled_primal_gradient);
const auto col_scaling_shard = shard(col_scaling_vec);
const auto primal_solution_shard = shard(scaled_primal_solution);
double dual_correction = 0.0;
double dual_full_correction = 0.0;
double l_inf_residual = 0.0;
double sumsq_residual = 0.0;
for (int64_t i = 0; i < primal_gradient_shard.size(); ++i) {
// The corrections use the scaled values because
// unscaled_lower_bound = lower_bound * scale and
// unscaled_primal_gradient = primal_gradient / scale, so the scales
// cancel out.
if (primal_gradient_shard[i] == 0.0) continue;
const double bound_for_rc = primal_gradient_shard[i] > 0.0
? lower_bound_shard[i]
: upper_bound_shard[i];
dual_full_correction += bound_for_rc * primal_gradient_shard[i];
if (HandlePrimalGradientTermAsReducedCost(
primal_gradient_shard[i], primal_solution_shard[i],
lower_bound_shard[i], upper_bound_shard[i])) {
dual_correction += bound_for_rc * primal_gradient_shard[i];
} else {
const double scaled_residual = std::abs(primal_gradient_shard[i]);
const double residual = scaled_residual / col_scaling_shard[i];
l_inf_residual = std::max(l_inf_residual, residual);
sumsq_residual += residual * residual;
}
}
local_dual_correction[shard.Index()] = dual_correction;
local_dual_full_correction[shard.Index()] = dual_full_correction;
local_l_inf_residual[shard.Index()] = l_inf_residual;
local_sumsq_residual[shard.Index()] = sumsq_residual;
});
return ResidualNorms{
.objective_correction = local_dual_correction.sum(),
.objective_full_correction = local_dual_full_correction.sum(),
.l_inf_residual = local_l_inf_residual.lpNorm<Eigen::Infinity>(),
.l_2_residual = std::sqrt(local_sumsq_residual.sum()),
};
}
// Returns Qx.
VectorXd ObjectiveProduct(const ShardedQuadraticProgram& sharded_qp,
const VectorXd& primal_solution) {
CHECK_EQ(primal_solution.size(), sharded_qp.PrimalSize());
VectorXd result(primal_solution.size());
if (IsLinearProgram(sharded_qp.Qp())) {
result.setZero();
} else {
sharded_qp.PrimalSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
shard(result) =
shard(*sharded_qp.Qp().objective_matrix) * shard(primal_solution);
});
}
return result;
}
// Returns 1/2 x^T Q x (the quadratic term in the objective).
double QuadraticObjective(const ShardedQuadraticProgram& sharded_qp,
const VectorXd& primal_solution,
const VectorXd& objective_product) {
CHECK_EQ(primal_solution.size(), sharded_qp.PrimalSize());
CHECK_EQ(objective_product.size(), sharded_qp.PrimalSize());
return 0.5 *
Dot(objective_product, primal_solution, sharded_qp.PrimalSharder());
}
// Returns objective_product + c A^T y when use_zero_primal_objective =
// false, and returns A^T y when use_zero_primal_objective = true.
// objective_product is passed by copy, and modified in place.
VectorXd PrimalGradientFromObjectiveProduct(
const ShardedQuadraticProgram& sharded_qp, const VectorXd& dual_solution,
VectorXd objective_product, bool use_zero_primal_objective = false) {
const QuadraticProgram& qp = sharded_qp.Qp();
CHECK_EQ(dual_solution.size(), sharded_qp.DualSize());
CHECK_EQ(objective_product.size(), sharded_qp.PrimalSize());
// Note that this modifies objective_product, replacing its entries with
// the primal gradient.
sharded_qp.ConstraintMatrixSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
if (use_zero_primal_objective) {
shard(objective_product) =
-shard(qp.constraint_matrix).transpose() * dual_solution;
} else {
shard(objective_product) +=
shard(qp.objective_vector) -
shard(qp.constraint_matrix).transpose() * dual_solution;
}
});
return objective_product;
}
// Returns the value of y term in the objective of the dual problem, see
// (l^c)^T[y]_+ (u^c)^T[y]_ in the dual objective from
// https://developers.google.com/optimization/lp/pdlp_math.
double DualObjectiveBoundsTerm(const ShardedQuadraticProgram& sharded_qp,
const VectorXd& dual_solution) {
const QuadraticProgram& qp = sharded_qp.Qp();
return sharded_qp.DualSharder().ParallelSumOverShards(
[&](const Sharder::Shard& shard) {
// This assumes that the dual variables are feasible, that is, that
// the term corresponding to the "y" variables in the dual objective
// in https://developers.google.com/optimization/lp/pdlp_math is finite.
const auto lower_bound_shard = shard(qp.constraint_lower_bounds);
const auto upper_bound_shard = shard(qp.constraint_upper_bounds);
const auto dual_shard = shard(dual_solution);
// Can't use .dot(.cwiseMin()) because that gives 0 * inf = NaN.
double sum = 0.0;
for (int64_t i = 0; i < dual_shard.size(); ++i) {
if (dual_shard[i] > 0.0) {
sum += lower_bound_shard[i] * dual_shard[i];
} else if (dual_shard[i] < 0.0) {
sum += upper_bound_shard[i] * dual_shard[i];
}
}
return sum;
});
}
// Computes the projection of the vector onto a pseudo-random vector determined
// by seed_generator. seed_generator is used as the source of a random seed for
// each shard's portion of the vector.
double RandomProjection(const VectorXd& vector, const Sharder& sharder,
std::mt19937& seed_generator) {
std::vector<std::mt19937> shard_seeds;
shard_seeds.reserve(sharder.NumShards());
for (int shard = 0; shard < sharder.NumShards(); ++shard) {
shard_seeds.emplace_back((seed_generator)());
}
// Computes vector * gaussian_random_vector and
// ||gaussian_random_vector||^2 to normalize by afterwards.
VectorXd dot_product(sharder.NumShards());
VectorXd gaussian_norm_squared(sharder.NumShards());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
const auto vector_shard = shard(vector);
double shard_dot_product = 0.0;
double shard_norm_squared = 0.0;
std::mt19937 random{shard_seeds[shard.Index()]};
for (int64_t i = 0; i < vector_shard.size(); ++i) {
const double projection_element = absl::Gaussian(random, 0.0, 1.0);
shard_dot_product += projection_element * vector_shard[i];
shard_norm_squared += MathUtil::Square(projection_element);
}
dot_product[shard.Index()] = shard_dot_product;
gaussian_norm_squared[shard.Index()] = shard_norm_squared;
});
return dot_product.sum() / std::sqrt(gaussian_norm_squared.sum());
}
} // namespace
ConvergenceInformation ComputeConvergenceInformation(
const ShardedQuadraticProgram& scaled_sharded_qp,
const Eigen::VectorXd& col_scaling_vec,
const Eigen::VectorXd& row_scaling_vec,
const Eigen::VectorXd& scaled_primal_solution,
const Eigen::VectorXd& scaled_dual_solution, PointType candidate_type) {
const QuadraticProgram& qp = scaled_sharded_qp.Qp();
CHECK_EQ(col_scaling_vec.size(), scaled_sharded_qp.PrimalSize());
CHECK_EQ(row_scaling_vec.size(), scaled_sharded_qp.DualSize());
CHECK_EQ(scaled_primal_solution.size(), scaled_sharded_qp.PrimalSize());
CHECK_EQ(scaled_dual_solution.size(), scaled_sharded_qp.DualSize());
// See https://developers.google.com/optimization/lp/pdlp_math#rescaling for
// notes describing the connection between the scaled and unscaled problem.
ConvergenceInformation result;
ResidualNorms primal_residuals = PrimalResidualNorms(
scaled_sharded_qp, row_scaling_vec, scaled_primal_solution);
result.set_l_inf_primal_residual(primal_residuals.l_inf_residual);
result.set_l2_primal_residual(primal_residuals.l_2_residual);
result.set_l_inf_primal_variable(
ScaledLInfNorm(scaled_primal_solution, col_scaling_vec,
scaled_sharded_qp.PrimalSharder()));
result.set_l2_primal_variable(ScaledNorm(scaled_primal_solution,
col_scaling_vec,
scaled_sharded_qp.PrimalSharder()));
result.set_l_inf_dual_variable(ScaledLInfNorm(
scaled_dual_solution, row_scaling_vec, scaled_sharded_qp.DualSharder()));
result.set_l2_dual_variable(ScaledNorm(scaled_dual_solution, row_scaling_vec,
scaled_sharded_qp.DualSharder()));
VectorXd scaled_objective_product =
ObjectiveProduct(scaled_sharded_qp, scaled_primal_solution);
const double quadratic_objective = QuadraticObjective(
scaled_sharded_qp, scaled_primal_solution, scaled_objective_product);
VectorXd scaled_primal_gradient = PrimalGradientFromObjectiveProduct(
scaled_sharded_qp, scaled_dual_solution,
std::move(scaled_objective_product));
result.set_primal_objective(qp.ApplyObjectiveScalingAndOffset(
quadratic_objective + Dot(qp.objective_vector, scaled_primal_solution,
scaled_sharded_qp.PrimalSharder())));
// This is the dual objective from
// https://developers.google.com/optimization/lp/pdlp_math minus the last term
// (involving r). All scaling terms cancel out.
const double dual_objective_piece =
-quadratic_objective +
DualObjectiveBoundsTerm(scaled_sharded_qp, scaled_dual_solution);
ResidualNorms dual_residuals =
DualResidualNorms(scaled_sharded_qp, col_scaling_vec,
scaled_primal_solution, scaled_primal_gradient);
result.set_dual_objective(qp.ApplyObjectiveScalingAndOffset(
dual_objective_piece + dual_residuals.objective_correction));
result.set_corrected_dual_objective(qp.ApplyObjectiveScalingAndOffset(
dual_objective_piece + dual_residuals.objective_full_correction));
result.set_l_inf_dual_residual(dual_residuals.l_inf_residual);
result.set_l2_dual_residual(dual_residuals.l_2_residual);
result.set_candidate_type(candidate_type);
return result;
}
InfeasibilityInformation ComputeInfeasibilityInformation(
const ShardedQuadraticProgram& scaled_sharded_qp,
const Eigen::VectorXd& col_scaling_vec,
const Eigen::VectorXd& row_scaling_vec,
const Eigen::VectorXd& scaled_primal_ray,
const Eigen::VectorXd& scaled_dual_ray, PointType candidate_type) {
const QuadraticProgram& qp = scaled_sharded_qp.Qp();
CHECK_EQ(col_scaling_vec.size(), scaled_sharded_qp.PrimalSize());
CHECK_EQ(row_scaling_vec.size(), scaled_sharded_qp.DualSize());
CHECK_EQ(scaled_primal_ray.size(), scaled_sharded_qp.PrimalSize());
CHECK_EQ(scaled_dual_ray.size(), scaled_sharded_qp.DualSize());
double l_inf_primal = ScaledLInfNorm(scaled_primal_ray, col_scaling_vec,
scaled_sharded_qp.PrimalSharder());
double l_inf_dual = ScaledLInfNorm(scaled_dual_ray, row_scaling_vec,
scaled_sharded_qp.DualSharder());
InfeasibilityInformation result;
// Compute primal infeasibility information.
VectorXd scaled_primal_gradient = PrimalGradientFromObjectiveProduct(
scaled_sharded_qp, scaled_dual_ray,
VectorXd::Zero(scaled_sharded_qp.PrimalSize()),
/*use_zero_primal_objective=*/true);
ResidualNorms dual_residuals =
DualResidualNorms(scaled_sharded_qp, col_scaling_vec, scaled_primal_ray,
scaled_primal_gradient);
double dual_ray_objective =
DualObjectiveBoundsTerm(scaled_sharded_qp, scaled_dual_ray) +
dual_residuals.objective_correction;
if (l_inf_dual > 0) {
result.set_dual_ray_objective(dual_ray_objective / l_inf_dual);
result.set_max_dual_ray_infeasibility(dual_residuals.l_inf_residual /
l_inf_dual);
} else {
result.set_dual_ray_objective(0.0);
result.set_max_dual_ray_infeasibility(0.0);
}
// Compute dual infeasibility information.
ResidualNorms primal_residuals =
PrimalResidualNorms(scaled_sharded_qp, row_scaling_vec, scaled_primal_ray,
/*use_homogeneous_constraint_bounds=*/true);
// primal_residuals contains the violations of the linear constraints. The
// signs of the components are also constrained by the presence or absence
// of variable bounds.
VectorXd primal_ray_local_sign_max_violation(
scaled_sharded_qp.PrimalSharder().NumShards());
scaled_sharded_qp.PrimalSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
const auto lower_bound_shard =
shard(scaled_sharded_qp.Qp().variable_lower_bounds);
const auto upper_bound_shard =
shard(scaled_sharded_qp.Qp().variable_upper_bounds);
const auto ray_shard = shard(scaled_primal_ray);
const auto scale_shard = shard(col_scaling_vec);
double local_max = 0.0;
for (int64_t i = 0; i < ray_shard.size(); ++i) {
if (std::isfinite(lower_bound_shard[i])) {
local_max = std::max(local_max, -ray_shard[i] * scale_shard[i]);
}
if (std::isfinite(upper_bound_shard[i])) {
local_max = std::max(local_max, ray_shard[i] * scale_shard[i]);
}
}
primal_ray_local_sign_max_violation[shard.Index()] = local_max;
});
const double primal_ray_sign_max_violation =
primal_ray_local_sign_max_violation.lpNorm<Eigen::Infinity>();
if (l_inf_primal > 0.0) {
VectorXd scaled_objective_product =
ObjectiveProduct(scaled_sharded_qp, scaled_primal_ray);
result.set_primal_ray_quadratic_norm(
LInfNorm(scaled_objective_product, scaled_sharded_qp.PrimalSharder()) /
l_inf_primal);
result.set_max_primal_ray_infeasibility(
std::max(primal_residuals.l_inf_residual,
primal_ray_sign_max_violation) /
l_inf_primal);
result.set_primal_ray_linear_objective(
Dot(scaled_primal_ray, qp.objective_vector,
scaled_sharded_qp.PrimalSharder()) /
l_inf_primal);
} else {
result.set_primal_ray_quadratic_norm(0.0);
result.set_max_primal_ray_infeasibility(0.0);
result.set_primal_ray_linear_objective(0.0);
}
result.set_candidate_type(candidate_type);
return result;
}
ConvergenceInformation ComputeScaledConvergenceInformation(
const ShardedQuadraticProgram& sharded_qp, const VectorXd& primal_solution,
const VectorXd& dual_solution, PointType candidate_type) {
return ComputeConvergenceInformation(
sharded_qp, VectorXd::Ones(sharded_qp.PrimalSize()),
VectorXd::Ones(sharded_qp.DualSize()), primal_solution, dual_solution,
candidate_type);
}
VectorXd ReducedCosts(const ShardedQuadraticProgram& sharded_qp,
const VectorXd& primal_solution,
const VectorXd& dual_solution,
bool use_zero_primal_objective) {
VectorXd objective_product;
if (use_zero_primal_objective) {
objective_product = VectorXd::Zero(sharded_qp.PrimalSize());
} else {
objective_product = ObjectiveProduct(sharded_qp, primal_solution);
}
VectorXd reduced_costs = PrimalGradientFromObjectiveProduct(
sharded_qp, dual_solution, std::move(objective_product),
use_zero_primal_objective);
sharded_qp.PrimalSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
auto rc_shard = shard(reduced_costs);
const auto lower_bound_shard =
shard(sharded_qp.Qp().variable_lower_bounds);
const auto upper_bound_shard =
shard(sharded_qp.Qp().variable_upper_bounds);
const auto primal_solution_shard = shard(primal_solution);
for (int64_t i = 0; i < rc_shard.size(); ++i) {
if (rc_shard[i] != 0.0 &&
!HandlePrimalGradientTermAsReducedCost(
rc_shard[i], primal_solution_shard[i], lower_bound_shard[i],
upper_bound_shard[i])) {
rc_shard[i] = 0.0;
}
}
});
return reduced_costs;
}
absl::optional<ConvergenceInformation> GetConvergenceInformation(
const IterationStats& stats, PointType candidate_type) {
for (const auto& convergence_information : stats.convergence_information()) {
if (convergence_information.candidate_type() == candidate_type) {
return convergence_information;
}
}
return absl::nullopt;
}
absl::optional<InfeasibilityInformation> GetInfeasibilityInformation(
const IterationStats& stats, PointType candidate_type) {
for (const auto& infeasibility_information :
stats.infeasibility_information()) {
if (infeasibility_information.candidate_type() == candidate_type) {
return infeasibility_information;
}
}
return absl::nullopt;
}
absl::optional<PointMetadata> GetPointMetadata(const IterationStats& stats,
const PointType point_type) {
for (const auto& metadata : stats.point_metadata()) {
if (metadata.point_type() == point_type) {
return metadata;
}
}
return absl::nullopt;
}
void SetRandomProjections(const ShardedQuadraticProgram& sharded_qp,
const Eigen::VectorXd& primal_solution,
const Eigen::VectorXd& dual_solution,
const std::vector<int>& random_projection_seeds,
PointMetadata& metadata) {
for (const int random_projection_seed : random_projection_seeds) {
std::mt19937 seed_generator(random_projection_seed);
metadata.mutable_random_primal_projections()->Add(RandomProjection(
primal_solution, sharded_qp.PrimalSharder(), seed_generator));
metadata.mutable_random_dual_projections()->Add(RandomProjection(
dual_solution, sharded_qp.DualSharder(), seed_generator));
}
}
} // namespace operations_research::pdlp

View File

@@ -0,0 +1,108 @@
// Copyright 2010-2021 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.
#ifndef PDLP_ITERATION_STATS_H_
#define PDLP_ITERATION_STATS_H_
#include <limits>
#include <string>
#include <vector>
#include "Eigen/Core"
#include "absl/types/optional.h"
#include "ortools/pdlp/sharded_quadratic_program.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/solvers.pb.h"
namespace operations_research::pdlp {
// Returns convergence statistics about a primal/dual solution pair. The stats
// are with respect to sharded_qp (which is typically scaled).
// This function is equivalent to ComputeConvergenceInformation given scaling
// vectors uniformly equal to one.
ConvergenceInformation ComputeScaledConvergenceInformation(
const ShardedQuadraticProgram& sharded_qp,
const Eigen::VectorXd& primal_solution,
const Eigen::VectorXd& dual_solution, PointType candidate_type);
// Returns convergence statistics about a primal/dual solution pair. It is
// assumed that scaled_sharded_qp has been transformed from the original qp by
// ShardedQuadraticProgram::RescaleQuadraticProgram(col_scaling_vec,
// row_scaling_vec). scaled_primal_solution and scaled_dual_solution are
// solutions for the scaled problem. The stats are computed with respect to the
// implicit original problem.
// NOTE: This function assumes that scaled_primal_solution satisfies the
// variable bounds and scaled_dual_solution satisfies the dual variable bounds;
// see
// https://developers.google.com/optimization/lp/pdlp_math#dual_variable_bounds.
ConvergenceInformation ComputeConvergenceInformation(
const ShardedQuadraticProgram& sharded_qp,
const Eigen::VectorXd& col_scaling_vec,
const Eigen::VectorXd& row_scaling_vec,
const Eigen::VectorXd& scaled_primal_solution,
const Eigen::VectorXd& scaled_dual_solution, PointType candidate_type);
// Returns infeasibility statistics about a primal/dual infeasibility
// certificate estimate. It is assumed that scaled_sharded_qp has been
// transformed from the original qp by
// ShardedQuadraticProgram::RescaleQuadraticProgram(col_scaling_vec,
// row_scaling_vec). primal_ray and dual_ray are certificates for the scaled
// problem. The stats are computed with respect to the implicit original
// problem.
InfeasibilityInformation ComputeInfeasibilityInformation(
const ShardedQuadraticProgram& scaled_sharded_qp,
const Eigen::VectorXd& col_scaling_vec,
const Eigen::VectorXd& row_scaling_vec,
const Eigen::VectorXd& scaled_primal_ray,
const Eigen::VectorXd& scaled_dual_ray, PointType candidate_type);
// Computes the reduced costs vector, objective_matrix * primal_solution +
// objective_vector - constraint_matrix * dual_solution - dual_residuals, when
// use_zero_primal_objective is false, and -constraint_matrix * dual_solution -
// dual_residuals when use_zero_primal_objective is true. The elements of the
// vector are corrected component-wise to zero to ensure that the dual objective
// takes a finite value. See
// https://developers.google.com/optimization/lp/pdlp_math#reduced_costs_dual_residuals_and_the_corrected_dual_objective.
Eigen::VectorXd ReducedCosts(const ShardedQuadraticProgram& scaled_sharded_qp,
const Eigen::VectorXd& primal_solution,
const Eigen::VectorXd& dual_solution,
bool use_zero_primal_objective = false);
// Finds and returns the ConvergenceInformation with the specified
// candidate_type, or absl::nullopt if no such candidate exists.
absl::optional<ConvergenceInformation> GetConvergenceInformation(
const IterationStats& stats, PointType candidate_type);
// Finds and returns the InfeasibilityInformation with the specified
// candidate_type, or absl::nullopt if no such candidate exists.
absl::optional<InfeasibilityInformation> GetInfeasibilityInformation(
const IterationStats& stats, PointType candidate_type);
// Finds and returns the PointMetadata with the specified
// point_type, or absl::nullopt if no such point exists.
absl::optional<PointMetadata> GetPointMetadata(const IterationStats& stats,
PointType point_type);
// For each entry in random_projection_seeds, computes a random projection of
// the primal/dual solution pair onto pseudo-random vectors generated from that
// seed and adds the results to
// random_primal_projections/random_dual_projections in metadata.
void SetRandomProjections(const ShardedQuadraticProgram& sharded_qp,
const Eigen::VectorXd& primal_solution,
const Eigen::VectorXd& dual_solution,
const std::vector<int>& random_projection_seeds,
PointMetadata& metadata);
} // namespace operations_research::pdlp
#endif // PDLP_ITERATION_STATS_H_

View File

@@ -0,0 +1,281 @@
// Copyright 2010-2021 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/iteration_stats.h"
#include <cmath>
#include <limits>
#include <utility>
#include "Eigen/Core"
#include "absl/types/optional.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/base/protobuf_util.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/pdlp/sharded_quadratic_program.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/test_util.h"
namespace operations_research::pdlp {
namespace {
using ::google::protobuf::util::ParseTextOrDie;
using ::testing::AllOf;
using ::testing::Each;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Ge;
using ::testing::Le;
using ::testing::Ne;
using ::testing::SizeIs;
TEST(CorrectedDualTest, SimpleLpWithSuboptimalDual) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
Eigen::VectorXd primal_solution(4), dual_solution(4);
// Set the primal variables that have primal gradients at their bounds, so
// that the primal gradients are reduced costs.
primal_solution << 0, 0, 6, 2.5;
dual_solution << -2, 0, 2.375, 1;
const ConvergenceInformation stats = ComputeScaledConvergenceInformation(
sharded_qp, primal_solution, dual_solution, POINT_TYPE_CURRENT_ITERATE);
// -36.5 = -14 - 24 - 9.5 - 1 - 3 + 15
EXPECT_DOUBLE_EQ(stats.dual_objective(), -36.5);
EXPECT_DOUBLE_EQ(stats.corrected_dual_objective(), -36.5);
}
// This is similar to SimpleLpWithSuboptimalDual, except with
// x_2 = 2. In the dual correction calculation, the corresponding bound is 6, so
// the primal gradient will be treated as a residual of 0.5 instead of a dual
// correction of -3, but in the corrected dual objective it is still treated as
// a dual correction.
TEST(CorrectedDualTest, SimpleLpWithVariableFarFromBound) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
Eigen::VectorXd primal_solution(4), dual_solution(4);
primal_solution << 0, 0, 2, 2.5;
dual_solution << -2, 0, 2.375, 1;
const ConvergenceInformation stats = ComputeScaledConvergenceInformation(
sharded_qp, primal_solution, dual_solution, POINT_TYPE_CURRENT_ITERATE);
// -33.5 = -14 - 24 - 9.5 - 1 + 15
EXPECT_DOUBLE_EQ(stats.dual_objective(), -33.5);
EXPECT_DOUBLE_EQ(stats.corrected_dual_objective(), -36.5);
EXPECT_DOUBLE_EQ(stats.l_inf_dual_residual(), 0.5);
EXPECT_DOUBLE_EQ(stats.l2_dual_residual(), 0.5);
}
TEST(CorrectedDualObjective, QpSuboptimal) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestDiagonalQp1(), num_threads,
num_shards);
Eigen::VectorXd primal_solution(2), dual_solution(1);
dual_solution << -3;
primal_solution << -2.0, 2.0;
const ConvergenceInformation stats = ComputeScaledConvergenceInformation(
sharded_qp, primal_solution, dual_solution, POINT_TYPE_CURRENT_ITERATE);
// primal gradient vector: [-6, 4]
// Constant term: 5
// Quadratic term: -(16+4)/2 = -10
// Dual objective term: -3 * 1
// Primal variables at bounds term: 2*-6 + -2*4 = -20
// -28.0 = 5 - 10 - 3 - 20
EXPECT_DOUBLE_EQ(stats.corrected_dual_objective(), -28.0);
}
TEST(RandomProjectionsTest, OneRandomProjectionsOfZeroVector) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
PointMetadata metadata;
SetRandomProjections(sharded_qp, /*primal_solution=*/Eigen::VectorXd::Zero(4),
/*dual_solution=*/Eigen::VectorXd::Zero(4),
/*random_projection_seeds=*/{1}, metadata);
EXPECT_THAT(metadata.random_primal_projections(), ElementsAre(0.0));
EXPECT_THAT(metadata.random_dual_projections(), ElementsAre(0.0));
}
TEST(RandomProjectionsTest, TwoRandomProjectionsOfVector) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
PointMetadata metadata;
SetRandomProjections(sharded_qp, /*primal_solution=*/Eigen::VectorXd::Ones(4),
/*dual_solution=*/Eigen::VectorXd::Zero(4),
/*random_projection_seeds=*/{1, 2}, metadata);
EXPECT_THAT(metadata.random_primal_projections(), SizeIs(2));
EXPECT_THAT(metadata.random_dual_projections(), SizeIs(2));
// The primal solution has norm 2; the random projection should only reduce
// the norm. Obtaining 0.0 is a probability-zero event.
EXPECT_THAT(metadata.random_primal_projections(),
Each(AllOf(Ge(-2.0), Le(2.0), Ne(0.0))));
EXPECT_THAT(metadata.random_dual_projections(), Each(Eq(0.0)));
}
TEST(ReducedCostsTest, SimpleLp) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
Eigen::VectorXd primal_solution(4), dual_solution(4);
// Use a primal solution at the relevant bounds, to ensure handling as
// reduced costs.
primal_solution << 0.0, -2.0, 6.0, 3.5;
dual_solution << 1.0, 0.0, 0.0, -2.0;
// c is: [5.5, -2, -1, 1]
// -A'y is: [-2, -1, 2, -4]
// c - A'y is: [3.5, -3.0, 1.0, -3.0].
EXPECT_THAT(ReducedCosts(sharded_qp, primal_solution, dual_solution),
ElementsAre(0.0, 0.0, 0.0, -3.0));
EXPECT_THAT(ReducedCosts(sharded_qp, primal_solution, dual_solution,
/*use_zero_primal_objective=*/true),
ElementsAre(0.0, 0.0, 0.0, -4.0));
}
TEST(ReducedCostsTest, SimpleLpWithGapResiduals) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestLp(), num_threads, num_shards);
Eigen::VectorXd primal_solution(4), dual_solution(4);
primal_solution = Eigen::VectorXd::Zero(4);
dual_solution << 1.0, 0.0, 0.0, -1.0;
// c is: [5.5, -2, -1, 1]
// -A'y is: [-2, -1, 0.5, -3]
// c - A'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 c - A'y is
// always handled as a residual.
EXPECT_THAT(ReducedCosts(sharded_qp, primal_solution, dual_solution),
ElementsAre(0.0, 0.0, 0.0, 0.0));
// If the primal variables are closer to the bound, c - A'y is handled as a
// reduced cost.
primal_solution << 0.0, 0.0, 4.0, 3.0;
EXPECT_THAT(ReducedCosts(sharded_qp, primal_solution, dual_solution),
ElementsAre(0.0, 0.0, -0.5, -2.0));
}
TEST(ReducedCostsTest, SimpleQp) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestDiagonalQp1(), num_threads,
num_shards);
Eigen::VectorXd primal_solution(2), dual_solution(1);
primal_solution << 1.0, 2.0;
dual_solution << 0.0;
// Q*x is: [4.0, 2.0]
// c is: [-1, -1]
// A'y is zero.
// The second primal gradient term is handled as a residual, not a reduced
// cost.
EXPECT_THAT(ReducedCosts(sharded_qp, primal_solution, dual_solution),
ElementsAre(3.0, 0.0));
EXPECT_THAT(ReducedCosts(sharded_qp, primal_solution, dual_solution,
/*use_zero_primal_objective=*/true),
ElementsAre(0.0, 0.0));
}
TEST(GetConvergenceInformation, GetsCorrectEntry) {
const auto test_stats = ParseTextOrDie<IterationStats>(R"pb(
convergence_information {
candidate_type: POINT_TYPE_CURRENT_ITERATE
primal_objective: 1.0
}
convergence_information {
candidate_type: POINT_TYPE_AVERAGE_ITERATE
primal_objective: 2.0
}
)pb");
const auto average_info =
GetConvergenceInformation(test_stats, POINT_TYPE_AVERAGE_ITERATE);
ASSERT_TRUE(average_info.has_value());
EXPECT_EQ(average_info->candidate_type(), POINT_TYPE_AVERAGE_ITERATE);
EXPECT_EQ(average_info->primal_objective(), 2.0);
const auto current_info =
GetConvergenceInformation(test_stats, POINT_TYPE_CURRENT_ITERATE);
ASSERT_TRUE(current_info.has_value());
EXPECT_EQ(current_info->candidate_type(), POINT_TYPE_CURRENT_ITERATE);
EXPECT_EQ(current_info->primal_objective(), 1.0);
EXPECT_THAT(
GetConvergenceInformation(test_stats, POINT_TYPE_ITERATE_DIFFERENCE),
Eq(absl::nullopt));
}
TEST(GetInfeasibilityInformation, GetsCorrectEntry) {
const auto test_stats = ParseTextOrDie<IterationStats>(R"pb(
infeasibility_information {
candidate_type: POINT_TYPE_CURRENT_ITERATE
primal_ray_linear_objective: 1.0
}
infeasibility_information {
candidate_type: POINT_TYPE_AVERAGE_ITERATE
primal_ray_linear_objective: 2.0
}
)pb");
const auto average_info =
GetInfeasibilityInformation(test_stats, POINT_TYPE_AVERAGE_ITERATE);
ASSERT_TRUE(average_info.has_value());
EXPECT_EQ(average_info->candidate_type(), POINT_TYPE_AVERAGE_ITERATE);
EXPECT_EQ(average_info->primal_ray_linear_objective(), 2.0);
const auto current_info =
GetInfeasibilityInformation(test_stats, POINT_TYPE_CURRENT_ITERATE);
ASSERT_TRUE(current_info.has_value());
EXPECT_EQ(current_info->candidate_type(), POINT_TYPE_CURRENT_ITERATE);
EXPECT_EQ(current_info->primal_ray_linear_objective(), 1.0);
EXPECT_THAT(
GetInfeasibilityInformation(test_stats, POINT_TYPE_ITERATE_DIFFERENCE),
Eq(absl::nullopt));
}
TEST(GetPointMetadata, GetsCorrectEntry) {
const auto test_stats = ParseTextOrDie<IterationStats>(R"pb(
point_metadata {
point_type: POINT_TYPE_CURRENT_ITERATE
active_primal_variable_count: 1
}
point_metadata {
point_type: POINT_TYPE_AVERAGE_ITERATE
active_primal_variable_count: 2
}
)pb");
const auto average_info =
GetPointMetadata(test_stats, POINT_TYPE_AVERAGE_ITERATE);
ASSERT_TRUE(average_info.has_value());
EXPECT_EQ(average_info->point_type(), POINT_TYPE_AVERAGE_ITERATE);
EXPECT_EQ(average_info->active_primal_variable_count(), 2);
const auto current_info =
GetPointMetadata(test_stats, POINT_TYPE_CURRENT_ITERATE);
ASSERT_TRUE(current_info.has_value());
EXPECT_EQ(current_info->point_type(), POINT_TYPE_CURRENT_ITERATE);
EXPECT_EQ(current_info->active_primal_variable_count(), 1);
EXPECT_THAT(GetPointMetadata(test_stats, POINT_TYPE_ITERATE_DIFFERENCE),
Eq(absl::nullopt));
}
} // namespace
} // namespace operations_research::pdlp

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
// Copyright 2010-2021 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.
#ifndef PDLP_PRIMAL_DUAL_HYBRID_GRADIENT_H_
#define PDLP_PRIMAL_DUAL_HYBRID_GRADIENT_H_
#include <functional>
#include "Eigen/Core"
#include "absl/status/statusor.h"
#include "absl/types/optional.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/lp_data/lp_data.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/solvers.pb.h"
#include "ortools/pdlp/termination.h"
namespace operations_research::pdlp {
struct PrimalAndDualSolution {
Eigen::VectorXd primal_solution;
Eigen::VectorXd dual_solution;
};
// The following table defines the interpretation of the result vectors
// depending on the value of solve_log.termination_reason: (the
// TERMINATION_REASON_ prefix is omitted for brevity):
//
// * OPTIMAL: the vectors satisfy the termination criteria for optimality.
// * PRIMAL_INFEASIBLE: dual_solution and reduced_costs provide an approximate
// certificate of primal infeasibility; see
// https://developers.google.com/optimization/lp/pdlp_math#infeasibility_identification
// for more information.
// * DUAL_INFEASIBLE: primal_solution provides an approximate certificate of
// dual infeasibility; see
// https://developers.google.com/optimization/lp/pdlp_math#infeasibility_identification
// for more information.
// * PRIMAL_OR_DUAL_INFEASIBLE: the problem was shown to be primal and/or dual
// infeasible but no certificate of infeasibility is available. The
// primal_solution and dual_solution have no meaning. This status is only used
// when presolve is enabled.
// * TIME_LIMIT, ITERATION_LIMIT, KKT_MATRIX_PASS_LIMIT, NUMERICAL_ERROR:
// the vectors contain an iterate at the time that the respective event
// occurred. Their values may or may not be meaningful. In some cases
// solution quality information is available; see documentation for
// solve_log.solution_type.
// * INVALID_PROBLEM, INVALID_PARAMETER, OTHER: the solution vectors are
// meaningless and may not have lengths consistent with the input problem.
struct SolverResult {
Eigen::VectorXd primal_solution;
// See https://developers.google.com/optimization/lp/pdlp_math for the
// interpretation of the dual and reduced costs.
Eigen::VectorXd dual_solution;
Eigen::VectorXd reduced_costs;
SolveLog solve_log;
};
struct IterationCallbackInfo {
TerminationCriteria termination_criteria;
IterationStats iteration_stats;
QuadraticProgramBoundNorms bound_norms;
};
// Solves the given QP using PDLP (Primal-Dual hybrid gradient enhanced for LP).
//
// All operations that are repeated each iteration are executed in parallel
// using params.num_threads() threads.
//
// The algorithm generally follows the description in
// https://arxiv.org/pdf/2106.04756.pdf, with further enhancements for QP.
// Notation here and in the implementation follows Chambolle and Pock, "On the
// ergodic convergence rates of a first-order primal-dual algorithm"
// (http://www.optimization-online.org/DB_FILE/2014/09/4532.pdf).
// That paper doesn't explicitly use the terminology "primal-dual hybrid
// gradient" but their Theorem 1 is analyzing PDHG. See
// https://developers.google.com/optimization/lp/pdlp_math#saddle-point_formulation
// for the saddle-point formulation of the QP we use that is compatible with
// Chambolle and Pock.
//
// We use 0.5 ||.||^2 for both the primal and dual distance functions.
//
// We parameterize the primal and dual step sizes (tau and sigma in Chambolle
// and Pock) as:
// primal_stepsize = step_size / primal_weight
// dual_stepsize = step_size * primal_weight
// where step_size and primal_weight are parameters.
// params.linesearch_rule determines the update rule for step_size.
// params.initial_primal_weight specifies how primal_weight is initialized
// and params.primal_weight_update_smoothing controls how primal_weight is
// updated.
//
// If iteration_stats_callback is not nullptr, then at each termination step
// (when iteration stats are VLOG(2)ed), iteration_stats_callback will also
// be called with those iteration stats.
//
// Callers MUST check solve_log.termination_reason before using the vectors in
// the SolverResult. See the comment on SolverResult for interpreting the
// termination reason.
//
// All objective values reported by the algorithm are transformed by using
// QuadraticProgram::ApplyObjectiveScalingAndOffset.
//
// NOTE: qp is intentionally passed by value, because PrimalDualHybridGradient
// modifies its copy.
SolverResult PrimalDualHybridGradient(
QuadraticProgram qp, const PrimalDualHybridGradientParams& params,
std::function<void(const IterationCallbackInfo&)> iteration_stats_callback =
nullptr);
// Like above but optionally starts with the given initial solution. If no
// initial solution is given the zero vector is used. In either case the initial
// solution is projected onto the primal and dual variable bounds before use.
// Convergence should be faster if the initial solution is close to an optimal
// solution. NOTE: initial_solution is intentionally passed by value.
SolverResult PrimalDualHybridGradient(
QuadraticProgram qp, const PrimalDualHybridGradientParams& params,
absl::optional<PrimalAndDualSolution> initial_solution,
std::function<void(const IterationCallbackInfo&)> iteration_stats_callback =
nullptr);
struct SolutionResponseAndLog {
MPSolutionResponse response;
SolveLog solve_log;
};
// Uses PrimalDualHybridGradient to solve the instance specified by the
// MPModelProto. If relax_integer_variables is true, integrality constraints are
// relaxed before solving. If false, integrality constraints result in an error.
// The MPSolutionResponse in the result enables partial compatibility with the
// MPSolver ecosystem, e.g., with MPSolver::LoadSolutionFromProto. The SolveLog
// has more detailed solve information. Users of this interface should be aware
// of the size limitations of MPModelProto (see, e.g.,
// large_linear_program.proto). Returns an error if the conversion from
// MPModelProto to QuadraticProgram fails. The lack of an error does not imply
// success. Check solve_log.termination_reason to see if the solve succeeded.
absl::StatusOr<SolutionResponseAndLog> SolveMpModelProto(
const MPModelProto& model, const PrimalDualHybridGradientParams& params,
bool relax_integer_variables = false,
std::function<void(const IterationCallbackInfo&)> iteration_stats_callback =
nullptr);
namespace internal {
// Computes variable and constraint statuses. This determines if primal
// variables are at their bounds based on exact comparisons and therefore may
// not work with unscaled solutions. The primal and dual solution in the
// returned ProblemSolution are NOT set.
glop::ProblemSolution ComputeStatuses(const QuadraticProgram& qp,
const PrimalAndDualSolution& solution);
} // namespace internal
} // namespace operations_research::pdlp
#endif // PDLP_PRIMAL_DUAL_HYBRID_GRADIENT_H_

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,366 @@
// Copyright 2010-2021 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/quadratic_program.h"
#include <algorithm>
#include <cstdint>
#include <iterator>
#include <limits>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/types/optional.h"
#include "ortools/base/logging.h"
#include "ortools/base/status_macros.h"
#include "ortools/linear_solver/linear_solver.pb.h"
namespace operations_research::pdlp {
using ::Eigen::VectorXd;
absl::Status ValidateQuadraticProgramDimensions(const QuadraticProgram& qp) {
const int64_t var_lb_size = qp.variable_lower_bounds.size();
const int64_t con_lb_size = qp.constraint_lower_bounds.size();
if (var_lb_size != qp.variable_upper_bounds.size()) {
return absl::InvalidArgumentError(absl::StrCat(
"Inconsistent dimensions: variable lower bound vector has size ",
var_lb_size, " while variable upper bound vector has size ",
qp.variable_upper_bounds.size()));
}
if (var_lb_size != qp.objective_vector.size()) {
return absl::InvalidArgumentError(absl::StrCat(
"Inconsistent dimensions: variable lower bound vector has size ",
var_lb_size, " while objective vector has size ",
qp.objective_vector.size()));
}
if (var_lb_size != qp.constraint_matrix.cols()) {
return absl::InvalidArgumentError(absl::StrCat(
"Inconsistent dimensions: variable lower bound vector has size ",
var_lb_size, " while constraint matrix has ",
qp.constraint_matrix.cols(), " columns"));
}
if (qp.objective_matrix.has_value() &&
var_lb_size != qp.objective_matrix->rows()) {
return absl::InvalidArgumentError(absl::StrCat(
"Inconsistent dimensions: variable lower bound vector has size ",
var_lb_size, " while objective matrix has ",
qp.objective_matrix->rows(), " rows"));
}
if (con_lb_size != qp.constraint_upper_bounds.size()) {
return absl::InvalidArgumentError(absl::StrCat(
"Inconsistent dimensions: constraint lower bound vector has size ",
con_lb_size, " while constraint upper bound vector has size ",
qp.constraint_upper_bounds.size()));
}
if (con_lb_size != qp.constraint_matrix.rows()) {
return absl::InvalidArgumentError(absl::StrCat(
"Inconsistent dimensions: constraint lower bound vector has size ",
con_lb_size, " while constraint matrix has ",
qp.constraint_matrix.rows(), " rows "));
}
return absl::OkStatus();
}
bool HasValidBounds(const QuadraticProgram& qp) {
const bool constraint_bounds_valid =
(qp.constraint_lower_bounds.array() <= qp.constraint_upper_bounds.array())
.all();
const bool variable_bounds_valid =
(qp.variable_lower_bounds.array() <= qp.variable_upper_bounds.array())
.all();
return constraint_bounds_valid && variable_bounds_valid;
}
absl::StatusOr<QuadraticProgram> QpFromMpModelProto(
const MPModelProto& proto, bool relax_integer_variables,
bool include_names) {
if (!proto.general_constraint().empty()) {
return absl::InvalidArgumentError("General constraints are not supported.");
}
const int primal_size = proto.variable_size();
const int dual_size = proto.constraint_size();
QuadraticProgram qp(primal_size, dual_size);
if (include_names) {
qp.problem_name = proto.name();
qp.variable_names = std::vector<std::string>(primal_size);
qp.constraint_names = std::vector<std::string>(dual_size);
}
for (int i = 0; i < primal_size; ++i) {
const auto& var = proto.variable(i);
qp.variable_lower_bounds[i] = var.lower_bound();
qp.variable_upper_bounds[i] = var.upper_bound();
qp.objective_vector[i] = var.objective_coefficient();
if (var.is_integer() && !relax_integer_variables) {
return absl::InvalidArgumentError(
"Integer variable encountered with relax_integer_variables == false");
}
if (include_names) {
(*qp.variable_names)[i] = var.name();
}
}
std::vector<int> nonzeros_by_column(primal_size);
for (int i = 0; i < dual_size; ++i) {
const auto& con = proto.constraint(i);
for (int j = 0; j < con.var_index_size(); ++j) {
if (con.var_index(j) < 0 || con.var_index(j) >= primal_size) {
return absl::InvalidArgumentError(absl::StrCat(
"Variable index of ", i, "th constraint's ", j, "th nonzero is ",
con.var_index(j), " which is not in the allowed range [0, ",
primal_size, ")"));
}
nonzeros_by_column[con.var_index(j)]++;
}
qp.constraint_lower_bounds[i] = con.lower_bound();
qp.constraint_upper_bounds[i] = con.upper_bound();
if (include_names) {
(*qp.constraint_names)[i] = con.name();
}
}
// To reduce peak RAM usage we construct the constraint matrix in-place.
// According to the documentation of SparseMatrix::insert() it's effecient to
// construct a matrix with insert()s as long as reserve() is called first and
// the non-zeros are inserted in increasing order of inner index.
// The non-zeros in each input constraint may not be sorted so this is only
// efficient with Column major format.
static_assert(qp.constraint_matrix.IsRowMajor == 0, "See comment.");
qp.constraint_matrix.reserve(nonzeros_by_column);
for (int i = 0; i < dual_size; ++i) {
const auto& con = proto.constraint(i);
CHECK_EQ(con.var_index_size(), con.coefficient_size())
<< " in " << i << "th constraint";
if (con.var_index_size() != con.coefficient_size()) {
return absl::InvalidArgumentError(
absl::StrCat(i, "th constraint has ", con.coefficient_size(),
" coefficients, expected ", con.var_index_size()));
}
for (int j = 0; j < con.var_index_size(); ++j) {
qp.constraint_matrix.insert(i, con.var_index(j)) = con.coefficient(j);
}
}
if (qp.constraint_matrix.outerSize() > 0) {
qp.constraint_matrix.makeCompressed();
}
// We use triplets-based initialization for the objective matrix because the
// objective non-zeros may be in arbitrary order in the input.
std::vector<Eigen::Triplet<double, int64_t>> triplets;
const auto& quadratic = proto.quadratic_objective();
if (quadratic.qvar1_index_size() != quadratic.qvar2_index_size() ||
quadratic.qvar1_index_size() != quadratic.coefficient_size()) {
return absl::InvalidArgumentError(absl::StrCat(
"The quadratic objective has ", quadratic.qvar1_index_size(),
" qvar1_indices, ", quadratic.qvar2_index_size(),
" qvar2_indices, and ", quadratic.coefficient_size(),
" coefficients, expected equal numbers."));
}
if (quadratic.qvar1_index_size() > 0) {
qp.objective_matrix.emplace();
qp.objective_matrix->setZero(primal_size);
}
for (int i = 0; i < quadratic.qvar1_index_size(); ++i) {
const int index1 = quadratic.qvar1_index(i);
const int index2 = quadratic.qvar2_index(i);
if (index1 < 0 || index2 < 0 || index1 >= primal_size ||
index2 >= primal_size) {
return absl::InvalidArgumentError(absl::StrCat(
"The quadratic objective's ", i, "th nonzero has indices ", index1,
" and ", index2, ", which are not both in the expected range [0, ",
primal_size, ")"));
}
if (index1 != index2) {
return absl::InvalidArgumentError(absl::StrCat(
"The quadratic objective's ", i,
"th nonzero has off-diagonal element at (", index1, ", ", index2,
"). Only diagonal objective matrices are supported."));
}
// Note: QuadraticProgram has an implicit "1/2" in front of the quadratic
// term.
qp.objective_matrix->diagonal()[index1] = 2 * quadratic.coefficient(i);
}
qp.objective_offset = proto.objective_offset();
if (proto.maximize()) {
qp.objective_offset *= -1;
qp.objective_vector *= -1;
if (qp.objective_matrix.has_value()) {
qp.objective_matrix->diagonal() *= -1;
}
qp.objective_scaling_factor = -1;
}
return std::move(qp);
}
absl::Status CanFitInMpModelProto(const QuadraticProgram& qp) {
return internal::TestableCanFitInMpModelProto(
qp, std::numeric_limits<int32_t>::max());
}
namespace internal {
absl::Status TestableCanFitInMpModelProto(const QuadraticProgram& qp,
const int64_t largest_ok_size) {
const int64_t primal_size = qp.variable_lower_bounds.size();
const int64_t dual_size = qp.constraint_lower_bounds.size();
bool primal_too_big = primal_size > largest_ok_size;
if (primal_too_big) {
return absl::InvalidArgumentError(absl::StrCat(
"Too many variables (", primal_size, ") to index with an int32_t."));
}
bool dual_too_big = dual_size > largest_ok_size;
if (dual_too_big) {
return absl::InvalidArgumentError(absl::StrCat(
"Too many constraints (", dual_size, ") to index with an int32_t."));
}
return absl::OkStatus();
}
} // namespace internal
absl::StatusOr<MPModelProto> QpToMpModelProto(const QuadraticProgram& qp) {
RETURN_IF_ERROR(CanFitInMpModelProto(qp));
if (qp.objective_scaling_factor == 0) {
return absl::InvalidArgumentError(
"objective_scaling_factor cannot be zero.");
}
const int64_t primal_size = qp.variable_lower_bounds.size();
const int64_t dual_size = qp.constraint_lower_bounds.size();
MPModelProto proto;
if (qp.problem_name.has_value() && !qp.problem_name->empty()) {
proto.set_name(*qp.problem_name);
}
proto.set_objective_offset(qp.objective_scaling_factor * qp.objective_offset);
if (qp.objective_scaling_factor < 0) {
proto.set_maximize(true);
} else {
proto.set_maximize(false);
}
proto.mutable_variable()->Reserve(primal_size);
for (int64_t i = 0; i < primal_size; ++i) {
auto* var = proto.add_variable();
var->set_lower_bound(qp.variable_lower_bounds[i]);
var->set_upper_bound(qp.variable_upper_bounds[i]);
var->set_objective_coefficient(qp.objective_scaling_factor *
qp.objective_vector[i]);
if (qp.variable_names.has_value() && i < qp.variable_names->size()) {
const std::string& name = (*qp.variable_names)[i];
if (!name.empty()) {
var->set_name(name);
}
}
}
proto.mutable_constraint()->Reserve(dual_size);
for (int64_t i = 0; i < dual_size; ++i) {
auto* con = proto.add_constraint();
con->set_lower_bound(qp.constraint_lower_bounds[i]);
con->set_upper_bound(qp.constraint_upper_bounds[i]);
if (qp.constraint_names.has_value() && i < qp.constraint_names->size()) {
const std::string& name = (*qp.constraint_names)[i];
if (!name.empty()) {
con->set_name(name);
}
}
}
using InnerIterator =
::Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>::InnerIterator;
for (int64_t col = 0; col < qp.constraint_matrix.cols(); ++col) {
for (InnerIterator iter(qp.constraint_matrix, col); iter; ++iter) {
auto* con = proto.mutable_constraint(iter.row());
// To avoid reallocs during the inserts, we could count the nonzeros
// and reserve() before filling.
con->add_var_index(iter.col());
con->add_coefficient(iter.value());
}
}
// Some OR tools decide the objective is quadratic based on
// has_quadratic_objective() rather than on quadratic_objective_size() == 0,
// so don't create the quadratic objective for linear programs.
if (!IsLinearProgram(qp)) {
auto* quadratic_objective = proto.mutable_quadratic_objective();
const auto& diagonal = qp.objective_matrix->diagonal();
for (int64_t i = 0; i < diagonal.size(); ++i) {
if (diagonal[i] != 0.0) {
quadratic_objective->add_qvar1_index(i);
quadratic_objective->add_qvar2_index(i);
// Undo the implicit (1/2) term in QuadraticProgram's objective.
quadratic_objective->add_coefficient(qp.objective_scaling_factor *
diagonal[i] / 2.0);
}
}
}
return proto;
}
void SetEigenMatrixFromTriplets(
std::vector<Eigen::Triplet<double, int64_t>> triplets,
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix) {
using Triplet = Eigen::Triplet<double, int64_t>;
std::sort(triplets.begin(), triplets.end(),
[](const Triplet& lhs, const Triplet& rhs) {
return std::tie(lhs.col(), lhs.row()) <
std::tie(rhs.col(), rhs.row());
});
// The triplets are allowed to contain duplicate entries (and intentionally
// do for the diagonals of the objective matrix). For efficiency of insert
// and reserve, merge the duplicates first.
internal::CombineRepeatedTripletsInPlace(triplets);
std::vector<int64_t> num_column_entries(matrix.cols());
for (const Triplet& triplet : triplets) {
++num_column_entries[triplet.col()];
}
// NOTE: reserve() takes column counts because matrix is in column major
// order.
matrix.reserve(num_column_entries);
for (const Triplet& triplet : triplets) {
matrix.insert(triplet.row(), triplet.col()) = triplet.value();
}
if (matrix.outerSize() > 0) {
matrix.makeCompressed();
}
}
namespace internal {
void CombineRepeatedTripletsInPlace(
std::vector<Eigen::Triplet<double, int64_t>>& triplets) {
if (triplets.empty()) return;
auto output_iter = triplets.begin();
for (auto p = output_iter + 1; p != triplets.end(); ++p) {
if (output_iter->row() == p->row() && output_iter->col() == p->col()) {
*output_iter = {output_iter->row(), output_iter->col(),
output_iter->value() + p->value()};
} else {
++output_iter;
if (output_iter != p) { // Small optimization - skip no-op copies.
*output_iter = *p;
}
}
}
// *output_iter is the last output value, so erase everything after that.
triplets.erase(output_iter + 1, triplets.end());
}
} // namespace internal
} // namespace operations_research::pdlp

View File

@@ -0,0 +1,216 @@
// Copyright 2010-2021 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.
#ifndef PDLP_QUADRATIC_PROGRAM_H_
#define PDLP_QUADRATIC_PROGRAM_H_
#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/types/optional.h"
#include "ortools/base/logging.h"
#include "ortools/linear_solver/linear_solver.pb.h"
namespace operations_research::pdlp {
// Represents the quadratic program (QP):
// min_x (objective_vector^T x + (1/2) x^T objective_matrix x) s.t.
// constraint_lower_bounds <= constraint_matrix x <= constraint_upper_bounds
// variable_lower_bounds <= x <= variable_upper_bounds
//
// constraint_lower_bounds and variable_lower_bounds may include negative
// infinities. constraint_upper_bounds and variable_upper_bounds may
// contain positive infinities. Other than that all entries of all fields must
// be finite. The objective_matrix must be symmetric and positive semi-definite.
//
// For convenience, the struct also stores a scaling factor and objective
// offset. These factors can be used to transform objective values based on the
// problem definition above into objective values that are meaningful for the
// user. See ApplyObjectiveScalingAndOffset.
//
// This struct is also intended for use with linear programs (LPs), which are
// QPs with a zero objective_matrix.
//
// The dual is documented at
// https://developers.google.com/optimization/lp/pdlp_math.
struct QuadraticProgram {
QuadraticProgram(int64_t num_variables, int64_t num_constraints) {
ResizeAndInitialize(num_variables, num_constraints);
}
QuadraticProgram() : QuadraticProgram(0, 0) {}
// QuadraticPrograms may be copied or moved. Eigen::SparseMatrix doesn't
// have move operations so we use custom implementations based on swap.
QuadraticProgram(const QuadraticProgram& other) = default;
QuadraticProgram(QuadraticProgram&& other) noexcept
: objective_vector(std::move(other.objective_vector)),
objective_matrix(std::move(other.objective_matrix)),
constraint_lower_bounds(std::move(other.constraint_lower_bounds)),
constraint_upper_bounds(std::move(other.constraint_upper_bounds)),
variable_lower_bounds(std::move(other.variable_lower_bounds)),
variable_upper_bounds(std::move(other.variable_upper_bounds)),
problem_name(std::move(other.problem_name)),
variable_names(std::move(other.variable_names)),
constraint_names(std::move(other.constraint_names)),
objective_offset(other.objective_offset),
objective_scaling_factor(other.objective_scaling_factor) {
constraint_matrix.swap(other.constraint_matrix);
}
QuadraticProgram& operator=(const QuadraticProgram& other) = default;
QuadraticProgram& operator=(QuadraticProgram&& other) {
objective_vector = std::move(other.objective_vector);
objective_matrix = std::move(other.objective_matrix);
constraint_matrix.swap(other.constraint_matrix);
constraint_lower_bounds = std::move(other.constraint_lower_bounds);
constraint_upper_bounds = std::move(other.constraint_upper_bounds);
variable_lower_bounds = std::move(other.variable_lower_bounds);
variable_upper_bounds = std::move(other.variable_upper_bounds);
problem_name = std::move(other.problem_name);
variable_names = std::move(other.variable_names);
constraint_names = std::move(other.constraint_names);
objective_offset = other.objective_offset;
objective_scaling_factor = other.objective_scaling_factor;
return *this;
}
// Initializes the quadratic program with num_variables variables and
// num_constraints constraints. Lower and upper bounds are set to to negative
// and positive infinity, repectively. The objective matrix is cleared. All
// other matrices and vectors are set to zero. Resets the optional names
// (program_name, variable_names, and constraint_names). objective_offset is
// set to 0 and objective_scaling_factor is set to 1.
void ResizeAndInitialize(int64_t num_variables, int64_t num_constraints) {
constexpr double kInfinity = std::numeric_limits<double>::infinity();
objective_vector = Eigen::VectorXd::Zero(num_variables);
objective_matrix.reset();
constraint_matrix.resize(num_constraints, num_variables);
constraint_lower_bounds =
Eigen::VectorXd::Constant(num_constraints, -kInfinity);
constraint_upper_bounds =
Eigen::VectorXd::Constant(num_constraints, kInfinity);
variable_lower_bounds =
Eigen::VectorXd::Constant(num_variables, -kInfinity);
variable_upper_bounds = Eigen::VectorXd::Constant(num_variables, kInfinity);
problem_name.reset();
variable_names.reset();
constraint_names.reset();
objective_offset = 0.0;
objective_scaling_factor = 1.0;
}
// Returns objective_scaling_factor * (objective + objective_offset).
// objective_scaling_factor is useful for modeling maximization problems.
// For example, max c'x = -1 * min (-c)'x. objective_offset can be a
// by-product of presolve transformations that eliminate variables.
double ApplyObjectiveScalingAndOffset(double objective) const {
return objective_scaling_factor * (objective + objective_offset);
}
Eigen::VectorXd objective_vector;
// If this field isn't set, the objective matrix is interpreted to be zero,
// i.e., this is a linear programming problem.
std::optional<Eigen::DiagonalMatrix<double, Eigen::Dynamic>> objective_matrix;
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> constraint_matrix;
Eigen::VectorXd constraint_lower_bounds, constraint_upper_bounds;
Eigen::VectorXd variable_lower_bounds, variable_upper_bounds;
// The problem, constraint, and variable names are optional.
absl::optional<std::string> problem_name;
absl::optional<std::vector<std::string>> variable_names;
absl::optional<std::vector<std::string>> constraint_names;
// These fields are provided for convenience; they don't change the
// mathematical definition of the problem, but they could change the objective
// values reported to the user.
double objective_offset;
double objective_scaling_factor;
};
// Returns InvalidArgument if vector or matrix dimensions are inconsistent.
// Returns OkStatus otherwise.
absl::Status ValidateQuadraticProgramDimensions(const QuadraticProgram& qp);
inline bool IsLinearProgram(const QuadraticProgram& qp) {
return !qp.objective_matrix.has_value();
}
// Checks if the lower and upper bounds of the problem are consistent, i.e. for
// each variable and constraint bound we have lower_bound <= upper_bound. If
// the input is consistent the method returns true, otherwise it returns false.
// See also HasValidBounds(const ShardedQuadraticProgram&).
bool HasValidBounds(const QuadraticProgram& qp);
// Converts an MPModelProto into a QuadraticProgram.
// Returns an error if general constraints are present.
// If relax_integer_variables is true integer variables are relaxed to
// continuous; otherwise integer variables are an error.
// If include_names is true (the default is false), the problem, constraint,
// and variable names are included in the QuadraticProgram; otherwise they are
// left empty.
// Maximization problems are converted to minimization by negating the
// objective and setting objective_scaling_factor to -1, which preserves the
// reported objective values.
absl::StatusOr<QuadraticProgram> QpFromMpModelProto(
const MPModelProto& proto, bool relax_integer_variables,
bool include_names = false);
// Returns InvalidArgument if the given quadratic program is too large to
// convert to MPModelProto and OkStatus otherwise.
absl::Status CanFitInMpModelProto(const QuadraticProgram& qp);
// Converts a QuadraticProgram into an MPModelProto. To preserve objective
// values in the conversion, the objective vector, objective matrix, and
// objective offset are scaled by objective_scaling_factor, and if
// objective_scaling_factor is negative, then the proto is a maximization
// problem (otherwise it's a minimization problem). Returns InvalidArgumentError
// if objective_scaling_factor is zero or if CanFitInMpModelProto() fails.
absl::StatusOr<MPModelProto> QpToMpModelProto(const QuadraticProgram& qp);
// Like matrix.setFromTriplets(triplets), except that setFromTriplets results
// in having three copies of the nonzeros in memory at the same time, because it
// first fills one matrix from triplets, and then transposes it into another.
// This avoids having the third copy in memory by sorting the triplets,
// reserving space in the matrix, and then inserting in sorted order.
// Compresses the matrix (SparseMatrix.makeCompressed()) after loading it.
// NOTE: This intentionally passes triplets by copy, because it modifies them.
// To avoid the copy, pass a move reference.
void SetEigenMatrixFromTriplets(
std::vector<Eigen::Triplet<double, int64_t>> triplets,
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix);
// Utility functions for internal use only.
namespace internal {
// Like CanFitInMpModelProto() but has an extra argument for the largest number
// of variables, constraints, or objective non-zeros that should be counted as
// convertible. CanFitInMpModelProto() passes 2^31 - 1 for this argument and
// unit tests pass small values.
absl::Status TestableCanFitInMpModelProto(const QuadraticProgram& qp,
int64_t largest_ok_size);
// Modifies a vector of Eigen::Triplets in place, combining consecutive entries
// with the same row and column, summing their values. This is most effective
// if the triplets are sorted by row and column, so that multiple entries for
// the same entry will be consecutive.
void CombineRepeatedTripletsInPlace(
std::vector<Eigen::Triplet<double, int64_t>>& triplets);
} // namespace internal
} // namespace operations_research::pdlp
#endif // PDLP_QUADRATIC_PROGRAM_H_

View File

@@ -0,0 +1,114 @@
// Copyright 2010-2021 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/quadratic_program_io.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/base/casts.h"
#include "absl/container/flat_hash_map.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/match.h"
#include "absl/types/optional.h"
#include "ortools/base/basictypes.h"
#include "ortools/base/file.h"
#include "ortools/base/logging.h"
#include "ortools/base/mathutil.h"
#include "ortools/base/status_macros.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/linear_solver/model_exporter.h"
#include "ortools/lp_data/mps_reader.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/util/file_util.h"
namespace operations_research::pdlp {
// TODO(user): Update internal helper functions to use references instead of
// pointers.
using ::operations_research::glop::MPSReader;
QuadraticProgram ReadQuadraticProgramOrDie(const std::string& filename,
bool include_names) {
if (absl::EndsWith(filename, ".mps") || absl::EndsWith(filename, ".mps.gz") ||
absl::EndsWith(filename, ".mps.bz2")) {
return ReadMpsLinearProgramOrDie(filename, include_names);
}
if (absl::EndsWith(filename, ".pb") ||
absl::EndsWith(filename, ".textproto") ||
absl::EndsWith(filename, ".json") ||
absl::EndsWith(filename, ".json.gz")) {
return ReadMPModelProtoFileOrDie(filename, include_names);
}
LOG(QFATAL) << "Invalid filename suffix in " << filename
<< ". Valid suffixes are .mps, .mps.gz, .pb, .textproto,"
<< ".json, and .json.gz";
}
QuadraticProgram ReadMpsLinearProgramOrDie(const std::string& lp_file,
bool include_names) {
MPModelProto lp_proto;
LOG(INFO) << "Reading " << lp_file;
QCHECK_OK(MPSReader().ParseFile(lp_file, &lp_proto));
// MPSReader sometimes fails silently if the file isn't read properly.
QCHECK_GT(lp_proto.variable_size(), 0)
<< "No variables in LP. Error reading file? " << lp_file;
auto result = QpFromMpModelProto(lp_proto, /*relax_integer_variables=*/true,
include_names);
QCHECK_OK(result);
return *std::move(result);
}
QuadraticProgram ReadMPModelProtoFileOrDie(
const std::string& mpmodel_proto_file, bool include_names) {
MPModelProto lp_proto;
QCHECK(ReadFileToProto(mpmodel_proto_file, &lp_proto)) << mpmodel_proto_file;
auto result = QpFromMpModelProto(lp_proto, /*relax_integer_variables=*/true,
include_names);
QCHECK_OK(result);
return *std::move(result);
}
absl::Status WriteLinearProgramToMps(const QuadraticProgram& linear_program,
const std::string& mps_file) {
if (!IsLinearProgram(linear_program)) {
return absl::InvalidArgumentError(
"'linear_program' has a quadratic objective");
}
ASSIGN_OR_RETURN(MPModelProto proto, QpToMpModelProto(linear_program));
ASSIGN_OR_RETURN(std::string mps_export, ExportModelAsMpsFormat(proto));
File* file;
RETURN_IF_ERROR(file::Open(mps_file, "w", &file, file::Defaults()));
auto status = file::WriteString(file, mps_export, file::Defaults());
status.Update(file->Close(file::Defaults()));
return status;
}
absl::Status WriteQuadraticProgramToMPModelProto(
const QuadraticProgram& quadratic_program,
const std::string& mpmodel_proto_file) {
ASSIGN_OR_RETURN(MPModelProto proto, QpToMpModelProto(quadratic_program));
return file::SetBinaryProto(mpmodel_proto_file, proto, file::Defaults());
}
} // namespace operations_research::pdlp

View File

@@ -0,0 +1,51 @@
// Copyright 2010-2021 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.
#ifndef PDLP_QUADRATIC_PROGRAM_IO_H_
#define PDLP_QUADRATIC_PROGRAM_IO_H_
#include <cstdint>
#include <string>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/status/status.h"
#include "ortools/pdlp/quadratic_program.h"
namespace operations_research::pdlp {
// Reads a quadratic program, determining the type based on the filename's
// suffix:
// *.mps, *.mps.gz, *.mps.bz2 -> ReadMpsLinearProgramOrDie
// *.pb, *.textproto, *.json, *.json.gz -> ReadMPModelProtoFileOrDie
// otherwise CHECK-fails.
QuadraticProgram ReadQuadraticProgramOrDie(const std::string& filename,
bool include_names = false);
QuadraticProgram ReadMpsLinearProgramOrDie(const std::string& lp_file,
bool include_names = false);
// The input may be MPModelProto in text format, binary format, or JSON,
// possibly gzipped.
QuadraticProgram ReadMPModelProtoFileOrDie(
const std::string& mpmodel_proto_file, bool include_names = false);
// NOTE: This will fail if 'linear_program' is actually a quadratic program
// (that is, has a non-empty quadratic objective term).
absl::Status WriteLinearProgramToMps(const QuadraticProgram& linear_program,
const std::string& mps_file);
absl::Status WriteQuadraticProgramToMPModelProto(
const QuadraticProgram& quadratic_program,
const std::string& mpmodel_proto_file);
} // namespace operations_research::pdlp
#endif // PDLP_QUADRATIC_PROGRAM_IO_H_

View File

@@ -0,0 +1,488 @@
// Copyright 2010-2021 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/quadratic_program.h"
#include <cstdint>
#include <limits>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/types/optional.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/base/protobuf_util.h"
#include "ortools/base/status_macros.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 ::operations_research::pdlp::internal::CombineRepeatedTripletsInPlace;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::Optional;
using ::testing::PrintToString;
const double kInfinity = std::numeric_limits<double>::infinity();
TEST(QuadraticProgram, DefaultConstructorWorks) { QuadraticProgram qp; }
TEST(QuadraticProgram, MoveConstructor) {
QuadraticProgram qp1 = TestDiagonalQp1();
QuadraticProgram qp2(std::move(qp1));
VerifyTestQp(qp2);
}
TEST(QuadraticProgram, MoveAssignment) {
QuadraticProgram qp1 = TestDiagonalQp1();
QuadraticProgram qp2;
qp2 = std::move(qp1);
VerifyTestQp(qp2);
}
TEST(ValidateQuadraticProgramDimensions, ValidProblem) {
const absl::Status status =
ValidateQuadraticProgramDimensions(TestDiagonalQp1());
EXPECT_TRUE(status.ok()) << status;
}
TEST(ValidateQuadraticProgramDimensions, ConstraintLowerBoundsInconsistent) {
QuadraticProgram qp;
qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
qp.constraint_lower_bounds.resize(10);
EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
absl::StatusCode::kInvalidArgument);
}
TEST(ValidateQuadraticProgramDimensions, ConstraintUpperBoundsInconsistent) {
QuadraticProgram qp;
qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
qp.constraint_upper_bounds.resize(10);
EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
absl::StatusCode::kInvalidArgument);
}
TEST(ValidateQuadraticProgramDimensions, ObjectiveVectorInconsistent) {
QuadraticProgram qp;
qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
qp.objective_vector.resize(10);
EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
absl::StatusCode::kInvalidArgument);
}
TEST(ValidateQuadraticProgramDimensions, VariableLowerBoundsInconsistent) {
QuadraticProgram qp;
qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
qp.variable_lower_bounds.resize(10);
EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
absl::StatusCode::kInvalidArgument);
}
TEST(ValidateQuadraticProgramDimensions, VariableUpperBoundsInconsistent) {
QuadraticProgram qp;
qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
qp.variable_upper_bounds.resize(10);
EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
absl::StatusCode::kInvalidArgument);
}
TEST(ValidateQuadraticProgramDimensions, ConstraintMatrixRowsInconsistent) {
QuadraticProgram qp;
qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
qp.constraint_matrix.resize(10, 2);
EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
absl::StatusCode::kInvalidArgument);
}
TEST(ValidateQuadraticProgramDimensions, ConstraintMatrixColsInconsistent) {
QuadraticProgram qp;
qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
qp.constraint_matrix.resize(2, 10);
EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
absl::StatusCode::kInvalidArgument);
}
TEST(ValidateQuadraticProgramDimensions, ObjectiveMatrixRowsInconsistent) {
QuadraticProgram qp;
qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
qp.objective_matrix.emplace();
qp.objective_matrix->resize(10);
EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
absl::StatusCode::kInvalidArgument);
}
TEST(HasValidBoundsTest, InconsistentConstraintBounds) {
QuadraticProgram invalid_lp = SmallInvalidProblemLp();
EXPECT_FALSE(HasValidBounds(invalid_lp));
}
TEST(HasValidBoundsTest, InconsistentVariableBounds) {
QuadraticProgram invalid_lp = SmallInconsistentVariableBoundsLp();
EXPECT_FALSE(HasValidBounds(invalid_lp));
}
TEST(HasValidBoundsTest, SmallValidLp) {
QuadraticProgram valid_lp = SmallPrimalInfeasibleLp();
EXPECT_TRUE(HasValidBounds(valid_lp));
}
class ConvertQpMpModelProtoTest : public testing::TestWithParam<bool> {};
// The LP:
// optimize 5.5 x_0 + 2 x_1 - x_2 + x_3 - 14 s.t.
// 2 x_0 + x_1 + x_2 + 2 x_3 = 12
// x_0 + x_2 >= 7
// 3.5 x_0 <= -4
// -1 <= 1.5 x_2 - x_3 <= 1
// -infinity <= x_0 <= infinity
// -2 <= x_1 <= infinity
// -infinity <= x_2 <= 6
// 2.5 <= x_3 <= 3.5
MPModelProto TestLpProto(bool maximize) {
auto proto = ParseTextOrDie<MPModelProto>(R"pb(variable {
lower_bound: -inf
upper_bound: inf
objective_coefficient: 5.5
}
variable {
lower_bound: -2
upper_bound: inf
objective_coefficient: -2
}
variable {
lower_bound: -inf
upper_bound: 6
objective_coefficient: -1
}
variable {
lower_bound: 2.5
upper_bound: 3.5
objective_coefficient: 1
}
constraint {
lower_bound: 12
upper_bound: 12
var_index: [ 0, 1, 2, 3 ]
coefficient: [ 2, 1, 1, 2 ]
}
constraint {
lower_bound: -inf
upper_bound: 7
var_index: [ 0, 2 ]
coefficient: [ 1, 1 ]
}
constraint {
lower_bound: -4
upper_bound: inf
var_index: [ 0 ]
coefficient: [ 4 ]
}
constraint {
lower_bound: -1
upper_bound: 1
var_index: [ 2, 3 ]
coefficient: [ 1.5, -1 ]
}
objective_offset: -14)pb");
proto.set_maximize(maximize);
return proto;
}
// This is tested for both minimization and maximization.
TEST_P(ConvertQpMpModelProtoTest, LpFromMpModelProto) {
const bool maximize = GetParam();
MPModelProto proto = TestLpProto(maximize);
const auto lp = QpFromMpModelProto(proto, /*relax_integer_variables=*/false);
ASSERT_TRUE(lp.ok()) << lp.status();
VerifyTestLp(*lp, maximize);
}
// The QP:
// optimize x_0^2 + x_1^2 + 3 x_0 - 4 s.t.
// x_0 + x_1 <= 42
// -1 <= x_0 <= 2
// -2 <= x_1 <= 3
MPModelProto TestQpProto(bool maximize) {
auto proto = ParseTextOrDie<MPModelProto>(
R"pb(variable { lower_bound: -1 upper_bound: 2 objective_coefficient: 3 }
variable { lower_bound: -2 upper_bound: 3 objective_coefficient: 0 }
constraint {
lower_bound: -inf
upper_bound: 42
var_index: [ 0, 1 ]
coefficient: [ 1, 1 ]
}
objective_offset: -4
quadratic_objective {
qvar1_index: [ 0, 1 ]
qvar2_index: [ 0, 1 ]
coefficient: [ 1, 1 ]
}
)pb");
proto.set_maximize(maximize);
return proto;
}
// This is tested for both minimization and maximization.
TEST_P(ConvertQpMpModelProtoTest, QpFromMpModelProto) {
const bool maximize = GetParam();
MPModelProto proto = TestQpProto(maximize);
const auto qp = QpFromMpModelProto(proto, /*relax_integer_variables=*/false);
ASSERT_TRUE(qp.ok()) << qp.status();
EXPECT_THAT(qp->constraint_lower_bounds, ElementsAre(-kInfinity));
EXPECT_THAT(qp->constraint_upper_bounds, ElementsAre(42));
EXPECT_THAT(qp->variable_lower_bounds, ElementsAre(-1, -2));
EXPECT_THAT(qp->variable_upper_bounds, ElementsAre(2, 3));
EXPECT_THAT(ToDense(qp->constraint_matrix), EigenArrayEq<double>({{1, 1}}));
EXPECT_TRUE(qp->constraint_matrix.isCompressed());
double sign = maximize ? -1 : 1;
EXPECT_EQ(sign * qp->objective_offset, -4);
EXPECT_EQ(qp->objective_scaling_factor, sign);
EXPECT_THAT(sign * qp->objective_vector, ElementsAre(3, 0));
EXPECT_THAT(sign * (qp->objective_matrix->diagonal()),
EigenArrayEq<double>({2, 2}));
}
TEST(QpFromMpModelProto, ErrorsOnOffDiagonalTerms) {
auto proto = ParseTextOrDie<MPModelProto>(
R"pb(variable { lower_bound: -1 upper_bound: 2 objective_coefficient: 3 }
variable { lower_bound: -2 upper_bound: 3 objective_coefficient: 0 }
constraint {
lower_bound: -inf
upper_bound: 42
var_index: [ 0, 1 ]
coefficient: [ 1, 1 ]
}
objective_offset: -4
quadratic_objective {
qvar1_index: [ 0 ]
qvar2_index: [ 1 ]
coefficient: [ 1 ]
}
)pb");
EXPECT_EQ(QpFromMpModelProto(proto, /*relax_integer_variables=*/false)
.status()
.code(),
absl::StatusCode::kInvalidArgument);
}
TEST(CanFitInMpModelProto, SmallQpOk) {
// QpFromMpModelProtoTest verifies that qp is as expected.
const auto qp = QpFromMpModelProto(TestQpProto(/*maximize=*/false),
/*relax_integer_variables=*/false);
ASSERT_TRUE(qp.ok()) << qp.status();
EXPECT_TRUE(CanFitInMpModelProto(*qp).ok());
}
// The ILP:
// optimize x_0 + 2 * x_1 s.t.
// x_0 + x_1 <= 1
// -1 <= x_0 <= 2
// -2 <= x_1 <= 3
// x_1 integer
// This is tested for both minimization and maximization.
TEST_P(ConvertQpMpModelProtoTest, IntegerVariablesFromMpModelProto) {
const bool maximize = GetParam();
auto proto = ParseTextOrDie<MPModelProto>(
R"pb(variable { lower_bound: -1 upper_bound: 2 objective_coefficient: 1 }
variable {
lower_bound: -2
upper_bound: 3
objective_coefficient: 2
is_integer: true
}
constraint {
lower_bound: -inf
upper_bound: 1
var_index: [ 0, 1 ]
coefficient: [ 1, 1 ]
}
)pb");
proto.set_maximize(maximize);
EXPECT_EQ(QpFromMpModelProto(proto, /*relax_integer_variables=*/false)
.status()
.code(),
absl::StatusCode::kInvalidArgument);
const auto lp = QpFromMpModelProto(proto, /*relax_integer_variables=*/true);
ASSERT_TRUE(lp.ok()) << lp.status();
EXPECT_THAT(lp->constraint_lower_bounds, ElementsAre(-kInfinity));
EXPECT_THAT(lp->constraint_upper_bounds, ElementsAre(1));
EXPECT_THAT(lp->variable_lower_bounds, ElementsAre(-1, -2));
EXPECT_THAT(lp->variable_upper_bounds, ElementsAre(2, 3));
EXPECT_THAT(ToDense(lp->constraint_matrix), EigenArrayEq<double>({{1, 1}}));
EXPECT_TRUE(lp->constraint_matrix.isCompressed());
double sign = maximize ? -1 : 1;
EXPECT_EQ(lp->objective_offset, 0);
EXPECT_THAT(sign * lp->objective_vector, ElementsAre(1, 2));
EXPECT_FALSE(lp->objective_matrix.has_value());
}
MPModelProto TinyModelWithNames() {
return ParseTextOrDie<MPModelProto>(
R"pb(name: "problem"
variable {
name: "x_0"
lower_bound: -1
upper_bound: 2
objective_coefficient: 1
}
variable {
name: "x_1"
lower_bound: -2
upper_bound: 3
objective_coefficient: 2
}
constraint {
name: "c_0"
lower_bound: -inf
upper_bound: 1
var_index: [ 0, 1 ]
coefficient: [ 1, 1 ]
}
)pb");
}
TEST(QpFromMpModelProtoTest, EmptyQp) {
MPModelProto proto;
const auto qp = QpFromMpModelProto(proto, /*relax_integer_variables=*/false);
ASSERT_TRUE(qp.ok()) << qp.status();
EXPECT_THAT(qp->constraint_lower_bounds, ElementsAre());
EXPECT_THAT(qp->constraint_upper_bounds, ElementsAre());
EXPECT_THAT(qp->variable_lower_bounds, ElementsAre());
EXPECT_THAT(qp->variable_upper_bounds, ElementsAre());
EXPECT_EQ(qp->constraint_matrix.cols(), 0);
EXPECT_EQ(qp->constraint_matrix.rows(), 0);
EXPECT_EQ(qp->objective_offset, 0);
EXPECT_EQ(qp->objective_scaling_factor, 1);
EXPECT_FALSE(qp->objective_matrix.has_value());
EXPECT_THAT(qp->objective_vector, ElementsAre());
}
TEST(QpFromMpModelProtoTest, DoesNotIncludeNames) {
const auto lp =
QpFromMpModelProto(TinyModelWithNames(), /*relax_integer_variables=*/true,
/*include_names=*/false);
ASSERT_TRUE(lp.ok()) << lp.status();
EXPECT_EQ(lp->problem_name, absl::nullopt);
EXPECT_EQ(lp->variable_names, absl::nullopt);
EXPECT_EQ(lp->constraint_names, absl::nullopt);
}
TEST(QpFromMpModelProtoTest, IncludesNames) {
const auto lp =
QpFromMpModelProto(TinyModelWithNames(), /*relax_integer_variables=*/true,
/*include_names=*/true);
ASSERT_TRUE(lp.ok()) << lp.status();
EXPECT_THAT(lp->problem_name, Optional(Eq("problem")));
EXPECT_THAT(lp->variable_names, Optional(ElementsAre("x_0", "x_1")));
EXPECT_THAT(lp->constraint_names, Optional(ElementsAre("c_0")));
}
INSTANTIATE_TEST_SUITE_P(
ConvertQpMpModelProtoTests, ConvertQpMpModelProtoTest, testing::Bool(),
[](const testing::TestParamInfo<ConvertQpMpModelProtoTest::ParamType>&
info) {
if (info.param) {
return "maximize";
} else {
return "minimize";
}
});
// A matcher for Eigen Triplets.
MATCHER_P3(IsEigenTriplet, row, col, value,
std::string(negation ? "isn't" : "is") + " the triplet " +
PrintToString(row) + "," + PrintToString(col) + "=" +
PrintToString(value)) {
return arg.row() == row && arg.col() == col && arg.value() == value;
}
TEST(CombineRepeatedTripletsInPlace, HandlesEmptyTriplets) {
std::vector<Eigen::Triplet<double, int64_t>> triplets;
CombineRepeatedTripletsInPlace(triplets);
EXPECT_THAT(triplets, IsEmpty());
}
TEST(CombineRepeatedTripletsInPlace, CorrectForSingleTriplet) {
std::vector<Eigen::Triplet<double, int64_t>> triplets = {{1, 2, 3.0}};
CombineRepeatedTripletsInPlace(triplets);
EXPECT_THAT(triplets, ElementsAre(IsEigenTriplet(1, 2, 3.0)));
}
TEST(CombineRepeatedTripletsInPlace, CorrectForDistinctTriplets) {
std::vector<Eigen::Triplet<double, int64_t>> triplets = {
{1, 2, 3.0}, {2, 1, 1.0}, {1, 1, 0.0}};
CombineRepeatedTripletsInPlace(triplets);
EXPECT_THAT(triplets,
ElementsAre(IsEigenTriplet(1, 2, 3.0), IsEigenTriplet(2, 1, 1.0),
IsEigenTriplet(1, 1, 0.0)));
}
TEST(CombineRepeatedTripletsInPlace, CombinesDuplicatesAtStart) {
std::vector<Eigen::Triplet<double, int64_t>> triplets = {
{1, 2, 3.0}, {1, 2, -1.0}, {1, 1, 0.0}};
CombineRepeatedTripletsInPlace(triplets);
EXPECT_THAT(triplets, ElementsAre(IsEigenTriplet(1, 2, 2.0),
IsEigenTriplet(1, 1, 0.0)));
}
TEST(CombineRepeatedTripletsInPlace, CombinesDuplicatesAtEnd) {
std::vector<Eigen::Triplet<double, int64_t>> triplets = {
{1, 2, 3.0}, {2, 1, 1.0}, {2, 1, 1.0}};
CombineRepeatedTripletsInPlace(triplets);
EXPECT_THAT(triplets, ElementsAre(IsEigenTriplet(1, 2, 3.0),
IsEigenTriplet(2, 1, 2.0)));
}
TEST(CombineRepeatedTripletsInPlace, CombinesToSingleton) {
std::vector<Eigen::Triplet<double, int64_t>> triplets = {
{1, 2, 3.0}, {1, 2, 1.0}, {1, 2, 2.0}};
CombineRepeatedTripletsInPlace(triplets);
EXPECT_THAT(triplets, ElementsAre(IsEigenTriplet(1, 2, 6.0)));
}
TEST(SetEigenMatrixFromTriplets, HandlesEmptyMatrix) {
std::vector<Eigen::Triplet<double, int64_t>> triplets;
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> matrix(2, 2);
SetEigenMatrixFromTriplets(std::move(triplets), matrix);
EXPECT_THAT(ToDense(matrix), EigenArrayEq<double>({{0, 0}, //
{0, 0}}));
}
TEST(SetEigenMatrixFromTriplets, CorrectForTinyMatrix) {
std::vector<Eigen::Triplet<double, int64_t>> triplets = {
{0, 0, 1.0}, {1, 0, -1.0}, {0, 0, 0.0}, {1, 1, 1.0}, {0, 0, 1.0}};
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> matrix(2, 2);
SetEigenMatrixFromTriplets(std::move(triplets), matrix);
EXPECT_THAT(ToDense(matrix), EigenArrayEq<double>({{2, 0}, //
{-1, 1}}));
}
} // namespace
} // namespace operations_research::pdlp

View File

@@ -0,0 +1,797 @@
// Copyright 2010-2021 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/sharded_optimization_utils.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <numeric>
#include <random>
#include <utility>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/algorithm/container.h"
#include "absl/random/distributions.h"
#include "absl/types/optional.h"
#include "ortools/base/logging.h"
#include "ortools/base/mathutil.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/pdlp/sharded_quadratic_program.h"
#include "ortools/pdlp/sharder.h"
namespace operations_research::pdlp {
constexpr double kInfinity = std::numeric_limits<double>::infinity();
using ::Eigen::ColMajor;
using ::Eigen::SparseMatrix;
using ::Eigen::VectorXd;
using ::Eigen::VectorXi;
ShardedWeightedAverage::ShardedWeightedAverage(const Sharder* sharder)
: sharder_(sharder) {
average_ = VectorXd::Zero(sharder->NumElements());
}
// We considered the five averaging algorithms M_* listed on the first page of
// https://www.jstor.org/stable/2286154 and the Kahan summation algorithm
// (https://en.wikipedia.org/wiki/Kahan_summation_algorithm). Of these only M_14
// satisfies our desired property that a constant sequence is averaged without
// roundoff while requiring only a single vector be stored. We therefore use
// M_14 (actually a natural weighted generalization, see below).
void ShardedWeightedAverage::Add(const VectorXd& datapoint, double weight) {
CHECK_GE(weight, 0.0);
CHECK_EQ(datapoint.size(), average_.size());
const double weight_ratio = weight / (sum_weights_ + weight);
sharder_->ParallelForEachShard([&](const Sharder::Shard& shard) {
shard(average_) += weight_ratio * (shard(datapoint) - shard(average_));
});
sum_weights_ += weight;
++num_terms_;
}
void ShardedWeightedAverage::Clear() {
// TODO(user): There may be a performance gain from using the sharder to
// zero-out the vectors.
average_.setZero();
sum_weights_ = 0.0;
num_terms_ = 0;
}
VectorXd ShardedWeightedAverage::ComputeAverage() const {
VectorXd result;
// TODO(user): consider returning a reference to avoid this copy.
AssignVector(average_, *sharder_, result);
return result;
}
namespace {
double CombineBounds(const double v1, const double v2,
const double infinite_bound_threshold) {
double max = 0.0;
if (std::abs(v1) < infinite_bound_threshold) {
max = std::abs(v1);
}
if (std::abs(v2) < infinite_bound_threshold) {
max = std::max(max, std::abs(v2));
}
return max;
}
struct VectorInfo {
int64_t num_finite = 0;
int64_t num_nonzero = 0;
double largest = 0.0;
double smallest = 0.0;
double average = 0.0;
double l2_norm = 0.0;
};
struct InfNormInfo {
double max_col_norm;
double min_col_norm;
double max_row_norm;
double min_row_norm;
};
// The functions below are used to generate default values for
// QuadraticProgramStats when the underlying program is empty or has no
// constraints.
double MaxOrZero(const VectorXd& vec) {
if (vec.size() == 0) {
return 0.0;
} else if (std::isinf(vec.maxCoeff())) {
return 0.0;
} else {
return vec.maxCoeff();
}
}
double MinOrZero(const VectorXd& vec) {
if (vec.size() == 0) {
return 0.0;
} else if (std::isinf(vec.minCoeff())) {
return 0.0;
} else {
return vec.minCoeff();
}
}
VectorInfo ComputeVectorInfo(const Eigen::Ref<const VectorXd>& vec,
const Sharder& sharder) {
VectorXd local_max(sharder.NumShards());
VectorXd local_min(sharder.NumShards());
VectorXd local_sum(sharder.NumShards());
VectorXd local_sum_squared(sharder.NumShards());
std::vector<int64_t> local_num_nonzero(sharder.NumShards());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
const VectorXd shard_abs = shard(vec).cwiseAbs();
local_max[shard.Index()] = shard_abs.maxCoeff();
local_min[shard.Index()] = shard_abs.minCoeff();
local_sum[shard.Index()] = shard_abs.sum();
local_sum_squared[shard.Index()] = shard_abs.squaredNorm();
for (double element : shard_abs) {
if (element != 0) {
local_num_nonzero[shard.Index()] += 1;
}
}
});
const int64_t num_elements = vec.size();
return VectorInfo{
.num_finite = num_elements,
.num_nonzero = std::accumulate(local_num_nonzero.begin(),
local_num_nonzero.end(), int64_t{0}),
.largest = MaxOrZero(local_max),
.smallest = MinOrZero(local_min),
.average = (num_elements > 0) ? local_sum.sum() / num_elements : NAN,
.l2_norm = (num_elements > 0) ? std::sqrt(local_sum_squared.sum()) : 0.0};
}
VectorInfo VariableBoundGapInfo(const VectorXd& lower_bounds,
const VectorXd& upper_bounds,
const Sharder& sharder) {
VectorXd local_max(sharder.NumShards());
VectorXd local_min(sharder.NumShards());
VectorXd local_sum(sharder.NumShards());
std::vector<int64_t> local_num_finite(sharder.NumShards());
std::vector<int64_t> local_num_nonzero(sharder.NumShards());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
const auto gap_shard = shard(upper_bounds) - shard(lower_bounds);
double max = -kInfinity;
double min = kInfinity;
double sum = 0.0;
int64_t num_finite = 0;
int64_t num_nonzero = 0;
for (int64_t i = 0; i < gap_shard.size(); ++i) {
if (std::isfinite(gap_shard[i])) {
max = std::max(max, gap_shard[i]);
min = std::min(min, gap_shard[i]);
sum += gap_shard[i];
num_finite += 1;
if (gap_shard[i] != 0) {
num_nonzero += 1;
}
}
}
local_max[shard.Index()] = max;
local_min[shard.Index()] = min;
local_sum[shard.Index()] = sum;
local_num_finite[shard.Index()] = num_finite;
local_num_nonzero[shard.Index()] = num_nonzero;
});
// If an empty model was given, local_sum could be an empty vector,
// in which case calling .sum() directly would crash.
const int64_t num_finite = std::accumulate(
local_num_finite.begin(), local_num_finite.end(), int64_t{0});
return VectorInfo{
.num_finite = num_finite,
.num_nonzero = std::accumulate(local_num_nonzero.begin(),
local_num_nonzero.end(), int64_t{0}),
.largest = MaxOrZero(local_max),
.smallest = MinOrZero(local_min),
.average = (num_finite > 0) ? local_sum.sum() / num_finite : NAN};
}
VectorInfo MatrixAbsElementInfo(
const SparseMatrix<double, ColMajor, int64_t>& matrix,
const Sharder& sharder) {
VectorXd local_max(sharder.NumShards());
VectorXd local_min(sharder.NumShards());
VectorXd local_sum(sharder.NumShards());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
const auto matrix_shard = shard(matrix);
double max = -kInfinity;
double min = kInfinity;
double sum = 0.0;
for (int64_t col_idx = 0; col_idx < matrix_shard.outerSize(); ++col_idx) {
for (decltype(matrix_shard)::InnerIterator it(matrix_shard, col_idx); it;
++it) {
max = std::max(max, std::abs(it.value()));
min = std::min(min, std::abs(it.value()));
sum += std::abs(it.value());
}
}
local_max[shard.Index()] = max;
local_min[shard.Index()] = min;
local_sum[shard.Index()] = sum;
});
const int64_t num_nonzeros = matrix.nonZeros();
return VectorInfo{
.num_finite = num_nonzeros,
.largest = MaxOrZero(local_max),
.smallest = MinOrZero(local_min),
.average = (num_nonzeros > 0) ? local_sum.sum() / num_nonzeros : NAN};
}
VectorInfo CombinedBoundsInfo(const VectorXd& rhs_upper_bounds,
const VectorXd& rhs_lower_bounds,
const Sharder& sharder,
const double infinite_bound_threshold =
std::numeric_limits<double>::infinity()) {
VectorXd local_max(sharder.NumShards());
VectorXd local_min(sharder.NumShards());
VectorXd local_sum(sharder.NumShards());
VectorXd local_sum_squared(sharder.NumShards());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
const auto lb_shard = shard(rhs_lower_bounds);
const auto ub_shard = shard(rhs_upper_bounds);
double max = -kInfinity;
double min = kInfinity;
double sum = 0.0;
double sum_squared = 0.0;
for (int64_t i = 0; i < lb_shard.size(); ++i) {
const double combined =
CombineBounds(ub_shard[i], lb_shard[i], infinite_bound_threshold);
max = std::max(max, combined);
min = std::min(min, combined);
sum += combined;
sum_squared += combined * combined;
}
local_max[shard.Index()] = max;
local_min[shard.Index()] = min;
local_sum[shard.Index()] = sum;
local_sum_squared[shard.Index()] = sum_squared;
});
const int num_constraints = rhs_lower_bounds.size();
return VectorInfo{
.num_finite = num_constraints,
.largest = MaxOrZero(local_max),
.smallest = MinOrZero(local_min),
.average =
(num_constraints > 0) ? local_sum.sum() / num_constraints : NAN,
.l2_norm =
(num_constraints > 0) ? std::sqrt(local_sum_squared.sum()) : 0.0};
}
InfNormInfo ConstraintMatrixRowColInfo(
const SparseMatrix<double, ColMajor, int64_t>& constraint_matrix,
const SparseMatrix<double, ColMajor, int64_t>& constraint_matrix_transpose,
const Sharder& matrix_sharder, const Sharder& matrix_transpose_sharder) {
VectorXd col_norms = ScaledColLInfNorm(
constraint_matrix,
/*row_scaling_vec=*/VectorXd::Ones(constraint_matrix.rows()),
/*col_scaling_vec=*/VectorXd::Ones(constraint_matrix.cols()),
matrix_sharder);
VectorXd row_norms =
ScaledColLInfNorm(constraint_matrix_transpose,
VectorXd::Ones(constraint_matrix_transpose.rows()),
VectorXd::Ones(constraint_matrix_transpose.cols()),
matrix_transpose_sharder);
return InfNormInfo{.max_col_norm = MaxOrZero(col_norms),
.min_col_norm = MinOrZero(col_norms),
.max_row_norm = MaxOrZero(row_norms),
.min_row_norm = MinOrZero(row_norms)};
}
} // namespace
QuadraticProgramStats ComputeStats(
const ShardedQuadraticProgram& qp,
const double infinite_constraint_bound_threshold) {
// Caution: if the constraint matrix is empty, elementwise operations
// (like .coeffs().maxCoeff() or .minCoeff()) will fail.
InfNormInfo cons_matrix_norm_info = ConstraintMatrixRowColInfo(
qp.Qp().constraint_matrix, qp.TransposedConstraintMatrix(),
qp.ConstraintMatrixSharder(), qp.TransposedConstraintMatrixSharder());
VectorInfo cons_matrix_info = MatrixAbsElementInfo(
qp.Qp().constraint_matrix, qp.ConstraintMatrixSharder());
VectorInfo combined_bounds_info = CombinedBoundsInfo(
qp.Qp().constraint_lower_bounds, qp.Qp().constraint_upper_bounds,
qp.DualSharder(), infinite_constraint_bound_threshold);
VectorInfo obj_vec_info =
ComputeVectorInfo(qp.Qp().objective_vector, qp.PrimalSharder());
VectorInfo gaps_info =
VariableBoundGapInfo(qp.Qp().variable_lower_bounds,
qp.Qp().variable_upper_bounds, qp.PrimalSharder());
QuadraticProgramStats program_stats;
program_stats.set_num_variables(qp.PrimalSize());
program_stats.set_num_constraints(qp.DualSize());
program_stats.set_constraint_matrix_col_min_l_inf_norm(
cons_matrix_norm_info.min_col_norm);
program_stats.set_constraint_matrix_row_min_l_inf_norm(
cons_matrix_norm_info.min_row_norm);
program_stats.set_constraint_matrix_num_nonzeros(cons_matrix_info.num_finite);
program_stats.set_constraint_matrix_abs_max(cons_matrix_info.largest);
program_stats.set_constraint_matrix_abs_min(cons_matrix_info.smallest);
program_stats.set_constraint_matrix_abs_avg(cons_matrix_info.average);
program_stats.set_combined_bounds_max(combined_bounds_info.largest);
program_stats.set_combined_bounds_min(combined_bounds_info.smallest);
program_stats.set_combined_bounds_avg(combined_bounds_info.average);
program_stats.set_combined_bounds_l2_norm(combined_bounds_info.l2_norm);
program_stats.set_variable_bound_gaps_num_finite(gaps_info.num_finite);
program_stats.set_variable_bound_gaps_max(gaps_info.largest);
program_stats.set_variable_bound_gaps_min(gaps_info.smallest);
program_stats.set_variable_bound_gaps_avg(gaps_info.average);
program_stats.set_objective_vector_abs_max(obj_vec_info.largest);
program_stats.set_objective_vector_abs_min(obj_vec_info.smallest);
program_stats.set_objective_vector_abs_avg(obj_vec_info.average);
program_stats.set_objective_vector_l2_norm(obj_vec_info.l2_norm);
if (IsLinearProgram(qp.Qp())) {
program_stats.set_objective_matrix_num_nonzeros(0);
program_stats.set_objective_matrix_abs_max(0);
program_stats.set_objective_matrix_abs_min(0);
program_stats.set_objective_matrix_abs_avg(NAN);
} else {
VectorInfo obj_matrix_info = ComputeVectorInfo(
qp.Qp().objective_matrix->diagonal(), qp.PrimalSharder());
program_stats.set_objective_matrix_num_nonzeros(
obj_matrix_info.num_nonzero);
program_stats.set_objective_matrix_abs_max(obj_matrix_info.largest);
program_stats.set_objective_matrix_abs_min(obj_matrix_info.smallest);
program_stats.set_objective_matrix_abs_avg(obj_matrix_info.average);
}
return program_stats;
}
namespace {
enum class ScalingNorm { kL2, kLInf };
// Divides the vector (componentwise) by the square root of the divisor,
// updating the vector in-place. If a component of the divisor is equal to zero,
// leaves the component of the vector unchanged. The Sharder should have the
// same size as the vector. For best performance the Sharder should have been
// created with the Sharder(int64_t, int, ThreadPool*) constructor.
void DivideBySquareRootOfDivisor(const VectorXd& divisor,
const Sharder& sharder, VectorXd& vector) {
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
auto vec_shard = shard(vector);
auto divisor_shard = shard(divisor);
for (int64_t index = 0; index < vec_shard.size(); ++index) {
if (divisor_shard[index] != 0) {
vec_shard[index] /= std::sqrt(divisor_shard[index]);
}
}
});
}
void ApplyScalingIterationsForNorm(const ShardedQuadraticProgram& sharded_qp,
const int num_iterations,
const ScalingNorm norm,
VectorXd& row_scaling_vec,
VectorXd& col_scaling_vec) {
const QuadraticProgram& qp = sharded_qp.Qp();
const int64_t num_col = qp.constraint_matrix.cols();
const int64_t num_row = qp.constraint_matrix.rows();
CHECK_EQ(num_col, col_scaling_vec.size());
CHECK_EQ(num_row, row_scaling_vec.size());
for (int i = 0; i < num_iterations; ++i) {
VectorXd col_norm;
VectorXd row_norm;
switch (norm) {
case ScalingNorm::kL2: {
col_norm = ScaledColL2Norm(qp.constraint_matrix, row_scaling_vec,
col_scaling_vec,
sharded_qp.ConstraintMatrixSharder());
row_norm = ScaledColL2Norm(
sharded_qp.TransposedConstraintMatrix(), col_scaling_vec,
row_scaling_vec, sharded_qp.TransposedConstraintMatrixSharder());
break;
}
case ScalingNorm::kLInf: {
col_norm = ScaledColLInfNorm(qp.constraint_matrix, row_scaling_vec,
col_scaling_vec,
sharded_qp.ConstraintMatrixSharder());
row_norm = ScaledColLInfNorm(
sharded_qp.TransposedConstraintMatrix(), col_scaling_vec,
row_scaling_vec, sharded_qp.TransposedConstraintMatrixSharder());
break;
}
}
DivideBySquareRootOfDivisor(col_norm, sharded_qp.PrimalSharder(),
col_scaling_vec);
DivideBySquareRootOfDivisor(row_norm, sharded_qp.DualSharder(),
row_scaling_vec);
}
}
} // namespace
void LInfRuizRescaling(const ShardedQuadraticProgram& sharded_qp,
const int num_iterations, VectorXd& row_scaling_vec,
VectorXd& col_scaling_vec) {
ApplyScalingIterationsForNorm(sharded_qp, num_iterations, ScalingNorm::kLInf,
row_scaling_vec, col_scaling_vec);
}
void L2NormRescaling(const ShardedQuadraticProgram& sharded_qp,
VectorXd& row_scaling_vec, VectorXd& col_scaling_vec) {
ApplyScalingIterationsForNorm(sharded_qp, /*num_iterations=*/1,
ScalingNorm::kL2, row_scaling_vec,
col_scaling_vec);
}
ScalingVectors ApplyRescaling(const RescalingOptions& rescaling_options,
ShardedQuadraticProgram& sharded_qp) {
ScalingVectors scaling{
.row_scaling_vec = VectorXd::Ones(sharded_qp.DualSize()),
.col_scaling_vec = VectorXd::Ones(sharded_qp.PrimalSize())};
bool do_rescale = false;
if (rescaling_options.l_inf_ruiz_iterations > 0) {
do_rescale = true;
LInfRuizRescaling(sharded_qp, rescaling_options.l_inf_ruiz_iterations,
scaling.row_scaling_vec, scaling.col_scaling_vec);
}
if (rescaling_options.l2_norm_rescaling) {
do_rescale = true;
L2NormRescaling(sharded_qp, scaling.row_scaling_vec,
scaling.col_scaling_vec);
}
if (do_rescale) {
sharded_qp.RescaleQuadraticProgram(scaling.col_scaling_vec,
scaling.row_scaling_vec);
}
return scaling;
}
LagrangianPart ComputePrimalGradient(const ShardedQuadraticProgram& sharded_qp,
const VectorXd& primal_solution,
const VectorXd& dual_product) {
LagrangianPart result{.gradient = VectorXd(sharded_qp.PrimalSize())};
const QuadraticProgram& qp = sharded_qp.Qp();
VectorXd value_parts(sharded_qp.PrimalSharder().NumShards());
sharded_qp.PrimalSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
if (IsLinearProgram(qp)) {
shard(result.gradient) =
shard(qp.objective_vector) - shard(dual_product);
value_parts[shard.Index()] =
shard(primal_solution).dot(shard(result.gradient));
} else {
// Note: using auto instead of VectorXd for the type of
// objective_product causes eigen to defer the matrix product until it
// is used (twice).
const VectorXd objective_product =
shard(*qp.objective_matrix) * shard(primal_solution);
shard(result.gradient) = shard(qp.objective_vector) +
objective_product - shard(dual_product);
value_parts[shard.Index()] =
shard(primal_solution)
.dot(shard(result.gradient) - 0.5 * objective_product);
}
});
result.value = value_parts.sum();
return result;
}
namespace {
// Returns a subderivative of the concave dual penalty function that appears in
// the Lagrangian: -p(dual; -constraint_upper_bound, -constraint_lower_bound) =
// { constraint_upper_bound * dual when dual < 0, 0 when dual == 0, and
// constraint_lower_bound * dual when dual > 0}
// (as defined at https://developers.google.com/optimization/lp/pdlp_math).
// The subderivative is not necessarily unique when dual == 0. In this case, if
// only one of the bounds is finite, we return that one. If both are finite, we
// return the primal product projected onto the bounds, which causes the dual
// Lagrangian gradient to be zero when the constraint is not violated. If both
// are infinite, we return zero. The value returned is valid only when the
// function is finite-valued.
double DualSubgradientCoefficient(const double constraint_lower_bound,
const double constraint_upper_bound,
const double dual,
const double primal_product) {
if (dual < 0.0) {
return constraint_upper_bound;
} else if (dual > 0.0) {
return constraint_lower_bound;
} else if (std::isfinite(constraint_lower_bound) &&
std::isfinite(constraint_upper_bound)) {
if (primal_product < constraint_lower_bound) {
return constraint_lower_bound;
} else if (primal_product > constraint_upper_bound) {
return constraint_upper_bound;
} else {
return primal_product;
}
} else if (std::isfinite(constraint_lower_bound)) {
return constraint_lower_bound;
} else if (std::isfinite(constraint_upper_bound)) {
return constraint_upper_bound;
} else {
return 0.0;
}
}
} // namespace
LagrangianPart ComputeDualGradient(const ShardedQuadraticProgram& sharded_qp,
const Eigen::VectorXd& dual_solution,
const Eigen::VectorXd& primal_product) {
LagrangianPart result{.gradient = VectorXd(sharded_qp.DualSize())};
const QuadraticProgram& qp = sharded_qp.Qp();
VectorXd value_parts(sharded_qp.DualSharder().NumShards());
sharded_qp.DualSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
auto constraint_lower_bounds = shard(qp.constraint_lower_bounds);
auto constraint_upper_bounds = shard(qp.constraint_upper_bounds);
auto dual_solution_shard = shard(dual_solution);
auto dual_gradient_shard = shard(result.gradient);
auto primal_product_shard = shard(primal_product);
double value_sum = 0.0;
for (int64_t i = 0; i < dual_gradient_shard.size(); ++i) {
dual_gradient_shard[i] = DualSubgradientCoefficient(
constraint_lower_bounds[i], constraint_upper_bounds[i],
dual_solution_shard[i], primal_product_shard[i]);
value_sum += dual_gradient_shard[i] * dual_solution_shard[i];
}
value_parts[shard.Index()] = value_sum;
dual_gradient_shard -= primal_product_shard;
});
result.value = value_parts.sum();
return result;
}
namespace {
using ::Eigen::ColMajor;
using ::Eigen::SparseMatrix;
// Scales a vector (in-place) to have norm 1, unless it has norm 0 (in which
// case it is left unscaled). Returns the norm of the input vector.
double NormalizeVector(const Sharder& sharder, VectorXd& vector) {
const double norm = Norm(vector, sharder);
if (norm != 0.0) {
sharder.ParallelForEachShard(
[&](const Sharder::Shard& shard) { shard(vector) /= norm; });
}
return norm;
}
// Estimates the probability that the power method, after k iterations, has
// relative error > epsilon. This is based on Theorem 4.1(a) (on page 13) from
// "Estimating the Largest Eigenvalue by the Power and Lanczos Algorithms with a
// Random Start"
// https://pdfs.semanticscholar.org/2b2e/a941e55e5fa2ee9d8f4ff393c14482051143.pdf
double PowerMethodFailureProbability(int64_t dimension, double epsilon, int k) {
if (k < 2 || epsilon <= 0.0) {
// The theorem requires epsilon > 0 and k >= 2.
return 1.0;
}
return std::min(0.824, 0.354 / (epsilon * (k - 1))) * std::sqrt(dimension) *
std::pow(1.0 - epsilon, k - 0.5);
}
SingularValueAndIterations EstimateMaximumSingularValue(
const SparseMatrix<double, ColMajor, int64_t>& matrix,
const SparseMatrix<double, ColMajor, int64_t>& matrix_transpose,
const absl::optional<VectorXd>& active_set_indicator,
const absl::optional<VectorXd>& transpose_active_set_indicator,
const Sharder& matrix_sharder, const Sharder& matrix_transpose_sharder,
const Sharder& primal_vector_sharder, const Sharder& dual_vector_sharder,
const double desired_relative_error, const double failure_probability,
std::mt19937& mt_generator) {
// Easy case: matrix is diagonal.
if (IsDiagonal(matrix, matrix_sharder)) {
VectorXd local_max(matrix_sharder.NumShards());
matrix_sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
const auto matrix_shard = shard(matrix);
local_max[shard.Index()] =
(matrix_shard *
VectorXd::Ones(matrix_sharder.ShardSize(shard.Index())))
.lpNorm<Eigen::Infinity>();
});
return {.singular_value = local_max.lpNorm<Eigen::Infinity>(),
.num_iterations = 0,
.estimated_relative_error = 0.0};
}
const int64_t dimension = matrix.cols();
VectorXd eigenvector(dimension);
// Even though it will be slower, we initialize eigenvector sequentially so
// that the result doesn't depend on the number of threads.
for (double& entry : eigenvector) {
entry = absl::Gaussian<double>(mt_generator);
}
if (active_set_indicator.has_value()) {
CoefficientWiseProductInPlace(*active_set_indicator, primal_vector_sharder,
eigenvector);
}
NormalizeVector(primal_vector_sharder, eigenvector);
double eigenvalue_estimate = 0.0;
int num_iterations = 0;
// The maximum singular value of A is the square root of the maximum
// eigenvalue of A^T A. epsilon is the relative error needed for the maximum
// eigenvalue of A^T A that gives desired_relative_error for the maximum
// singular value of A.
const double epsilon = 1.0 - MathUtil::Square(1.0 - desired_relative_error);
while (PowerMethodFailureProbability(dimension, epsilon, num_iterations) >
failure_probability) {
VectorXd dual_eigenvector = TransposedMatrixVectorProduct(
matrix_transpose, eigenvector, matrix_transpose_sharder);
if (transpose_active_set_indicator.has_value()) {
CoefficientWiseProductInPlace(*transpose_active_set_indicator,
dual_vector_sharder, dual_eigenvector);
}
VectorXd next_eigenvector =
TransposedMatrixVectorProduct(matrix, dual_eigenvector, matrix_sharder);
if (active_set_indicator.has_value()) {
CoefficientWiseProductInPlace(*active_set_indicator,
primal_vector_sharder, next_eigenvector);
}
eigenvalue_estimate =
Dot(eigenvector, next_eigenvector, primal_vector_sharder);
eigenvector = std::move(next_eigenvector);
++num_iterations;
const double primal_norm =
NormalizeVector(primal_vector_sharder, eigenvector);
VLOG(1) << "Iteration " << num_iterations << " singular value estimate "
<< std::sqrt(eigenvalue_estimate) << " primal norm " << primal_norm;
}
return SingularValueAndIterations{
.singular_value = std::sqrt(eigenvalue_estimate),
.num_iterations = num_iterations,
.estimated_relative_error = desired_relative_error};
}
// Given a primal solution, compute a {0, 1}-valued vector that is nonzero in
// all the coordinates that are not saturating the primal variable bounds.
VectorXd ComputePrimalActiveSetIndicator(
const ShardedQuadraticProgram& sharded_qp,
const VectorXd& primal_solution) {
VectorXd indicator(sharded_qp.PrimalSize());
sharded_qp.PrimalSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
const auto lower_bound_shard =
shard(sharded_qp.Qp().variable_lower_bounds);
const auto upper_bound_shard =
shard(sharded_qp.Qp().variable_upper_bounds);
const auto primal_solution_shard = shard(primal_solution);
auto indicator_shard = shard(indicator);
const int64_t shard_size =
sharded_qp.PrimalSharder().ShardSize(shard.Index());
for (int64_t i = 0; i < shard_size; ++i) {
if ((primal_solution_shard[i] == lower_bound_shard[i]) ||
(primal_solution_shard[i] == upper_bound_shard[i])) {
indicator_shard[i] = 0.0;
} else {
indicator_shard[i] = 1.0;
}
}
});
return indicator;
}
// Like ComputePrimalActiveSetIndicator(sharded_qp, primal_solution), but this
// time using the implicit bounds on the dual variable.
VectorXd ComputeDualActiveSetIndicator(
const ShardedQuadraticProgram& sharded_qp, const VectorXd& dual_solution) {
VectorXd indicator(sharded_qp.DualSize());
sharded_qp.DualSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
const auto lower_bound_shard =
shard(sharded_qp.Qp().constraint_lower_bounds);
const auto upper_bound_shard =
shard(sharded_qp.Qp().constraint_upper_bounds);
const auto dual_solution_shard = shard(dual_solution);
auto indicator_shard = shard(indicator);
const int64_t shard_size =
sharded_qp.DualSharder().ShardSize(shard.Index());
for (int64_t i = 0; i < shard_size; ++i) {
if (dual_solution_shard[i] == 0.0 &&
(std::isinf(lower_bound_shard[i]) ||
std::isinf(upper_bound_shard[i]))) {
indicator_shard[i] = 0.0;
} else {
indicator_shard[i] = 1.0;
}
}
});
return indicator;
}
} // namespace
SingularValueAndIterations EstimateMaximumSingularValueOfConstraintMatrix(
const ShardedQuadraticProgram& sharded_qp,
const absl::optional<VectorXd>& primal_solution,
const absl::optional<VectorXd>& dual_solution,
const double desired_relative_error, const double failure_probability,
std::mt19937& mt_generator) {
absl::optional<VectorXd> primal_active_set_indicator;
absl::optional<VectorXd> dual_active_set_indicator;
if (primal_solution.has_value()) {
primal_active_set_indicator =
ComputePrimalActiveSetIndicator(sharded_qp, *primal_solution);
}
if (dual_solution.has_value()) {
dual_active_set_indicator =
ComputeDualActiveSetIndicator(sharded_qp, *dual_solution);
}
return EstimateMaximumSingularValue(
sharded_qp.Qp().constraint_matrix,
sharded_qp.TransposedConstraintMatrix(), primal_active_set_indicator,
dual_active_set_indicator, sharded_qp.ConstraintMatrixSharder(),
sharded_qp.TransposedConstraintMatrixSharder(),
sharded_qp.PrimalSharder(), sharded_qp.DualSharder(),
desired_relative_error, failure_probability, mt_generator);
}
bool HasValidBounds(const ShardedQuadraticProgram& sharded_qp) {
const QuadraticProgram& qp = sharded_qp.Qp();
const bool constraint_bounds_valid =
sharded_qp.DualSharder().ParallelTrueForAllShards(
[&](const Sharder::Shard& shard) {
return (shard(qp.constraint_lower_bounds).array() <=
shard(qp.constraint_upper_bounds).array())
.all();
});
const bool variable_bounds_valid =
sharded_qp.PrimalSharder().ParallelTrueForAllShards(
[&](const Sharder::Shard& shard) {
return (shard(qp.variable_lower_bounds).array() <=
shard(qp.variable_upper_bounds).array())
.all();
});
return constraint_bounds_valid && variable_bounds_valid;
}
void ProjectToPrimalVariableBounds(const ShardedQuadraticProgram& sharded_qp,
VectorXd& primal) {
sharded_qp.PrimalSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
const QuadraticProgram& qp = sharded_qp.Qp();
shard(primal) = shard(primal)
.cwiseMin(shard(qp.variable_upper_bounds))
.cwiseMax(shard(qp.variable_lower_bounds));
});
}
void ProjectToDualVariableBounds(const ShardedQuadraticProgram& sharded_qp,
VectorXd& dual) {
const QuadraticProgram& qp = sharded_qp.Qp();
sharded_qp.DualSharder().ParallelForEachShard(
[&](const Sharder::Shard& shard) {
const auto lower_bound_shard = shard(qp.constraint_lower_bounds);
const auto upper_bound_shard = shard(qp.constraint_upper_bounds);
auto dual_shard = shard(dual);
for (int64_t i = 0; i < dual_shard.size(); ++i) {
if (!std::isfinite(upper_bound_shard[i])) {
dual_shard[i] = std::max(dual_shard[i], 0.0);
}
if (!std::isfinite(lower_bound_shard[i])) {
dual_shard[i] = std::min(dual_shard[i], 0.0);
}
}
});
}
} // namespace operations_research::pdlp

View File

@@ -0,0 +1,191 @@
// Copyright 2010-2021 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.
// These are internal helper functions and classes that implicitly or explicitly
// operate on a ShardedQuadraticProgram. Utilities that are purely linear
// algebra operations (e.g., norms) should be defined in sharder.h instead.
#ifndef PDLP_SHARDED_OPTIMIZATION_UTILS_H_
#define PDLP_SHARDED_OPTIMIZATION_UTILS_H_
#include <limits>
#include <random>
#include "Eigen/Core"
#include "absl/types/optional.h"
#include "ortools/pdlp/sharded_quadratic_program.h"
#include "ortools/pdlp/sharder.h"
#include "ortools/pdlp/solve_log.pb.h"
namespace operations_research::pdlp {
// This computes weighted averages of vectors.
// It satisfies the following: if all the averaged vectors have component i
// equal to x then the average has component i exactly equal to x, without any
// floating-point roundoff. In practice the above is probably still true with
// "equal to x" replaced with "at least x" or "at most x". However unrealistic
// counter examples probably exist involving a new item with weight 10^15 times
// greater than the total weight so far.
class ShardedWeightedAverage {
public:
// Initializes the weighted average by creating a vector sized according to
// the number of elements in the sharder. Retains the pointer to sharder, so
// the sharder must outlive this object.
explicit ShardedWeightedAverage(const Sharder* sharder);
ShardedWeightedAverage(ShardedWeightedAverage&&) = default;
ShardedWeightedAverage& operator=(ShardedWeightedAverage&&) = default;
// Adds the datapoint to the average with the given weight. CHECK-fails if
// the weight is negative.
void Add(const Eigen::VectorXd& datapoint, double weight);
// Clears the sum to zero, i.e., just constructed.
void Clear();
// Returns true if there is at least one term in the average with a positive
// weight.
bool HasNonzeroWeight() const { return sum_weights_ > 0.0; }
// Returns the sum of the weights of the datapoints added so far.
double Weight() const { return sum_weights_; }
// Computes the weighted average of the datapoints added so far, i.e.,
// sum_i weight[i] * datapoint[i] / sum_i weight[i]. The results are set to
// zero if HasNonzeroWeight() is false.
Eigen::VectorXd ComputeAverage() const;
int NumTerms() const { return num_terms_; }
private:
Eigen::VectorXd average_;
double sum_weights_ = 0.0;
int num_terms_ = 0;
const Sharder* sharder_;
};
// Returns a QuadraticProgramStats object for a ShardedQuadraticProgram.
QuadraticProgramStats ComputeStats(const ShardedQuadraticProgram& qp,
double infinite_constraint_bound_threshold =
std::numeric_limits<double>::infinity());
// LInfRuizRescaling and L2NormRescaling rescale the (scaled) constraint matrix
// of the LP by updating the scaling vectors in-place. More specifically, the
// (scaled) constraint matrix always has the format: diag(row_scaling_vec) *
// sharded_qp.Qp().constraint_matrix * diag(col_scaling_vec), and
// row_scaling_vec and col_scaling_vec are updated in-place. On input,
// row_scaling_vec and col_scaling_vec provide the initial scaling.
// With each iteration of LInfRuizRescaling scaling, row_scaling_vec
// (col_scaling_vec) is divided by the sqrt of the row (col) LInf norm of the
// current (scaled) constraint matrix. The (scaled) constraint matrix approaches
// having all row and column LInf norms equal to 1 as the number of iterations
// goes to infinity. This convergence is fast (linear). More details of Ruiz
// rescaling algorithm can be found at:
// http://www.numerical.rl.ac.uk/reports/drRAL2001034.pdf.
void LInfRuizRescaling(const ShardedQuadraticProgram& sharded_qp,
const int num_iterations,
Eigen::VectorXd& row_scaling_vec,
Eigen::VectorXd& col_scaling_vec);
// L2NormRescaling divides row_scaling_vec (col_scaling_vec) by the sqrt of the
// row (col) L2 norm of the current (scaled) constraint matrix. Unlike
// LInfRescaling, this function does only one iteration because the scaling
// procedure does not converge in general. This is not Ruiz rescaling for the L2
// norm.
void L2NormRescaling(const ShardedQuadraticProgram& sharded_qp,
Eigen::VectorXd& row_scaling_vec,
Eigen::VectorXd& col_scaling_vec);
struct RescalingOptions {
int l_inf_ruiz_iterations;
bool l2_norm_rescaling;
};
struct ScalingVectors {
Eigen::VectorXd row_scaling_vec;
Eigen::VectorXd col_scaling_vec;
};
// Applies the rescaling specified by rescaling_options to sharded_qp (in
// place). Returns the scaling vectors that were applied.
ScalingVectors ApplyRescaling(const RescalingOptions& rescaling_options,
ShardedQuadraticProgram& sharded_qp);
struct LagrangianPart {
double value = 0.0;
Eigen::VectorXd gradient;
};
// Computes the value of the primal part of the Lagrangian function defined at
// https://developers.google.com/optimization/lp/pdlp_math, i.e.,
// c'x + (1/2) x'Qx - y'Ax and its gradient with respect to the primal variables
// x, i.e., c + Qx - A'y. The dual_product argument is A'y. Note: The objective
// constant is omitted. The result is undefined and invalid if any primal bounds
// are violated.
LagrangianPart ComputePrimalGradient(const ShardedQuadraticProgram& sharded_qp,
const Eigen::VectorXd& primal_solution,
const Eigen::VectorXd& dual_product);
// Computes the value of the dual part of the Lagrangian function defined at
// https://developers.google.com/optimization/lp/pdlp_math, i.e., -h^*(y) and
// the gradient of the Lagrangian with respect to the dual variables y, i.e.,
// -Ax - \grad_y h^*(y). Note the asymmetry with ComputePrimalGradient: the term
// -y'Ax is not part of the value. Because h^*(y) is piece-wise linear, a
// subgradient is returned at a point of non- smoothness. The primal_product
// argument is Ax. The result is undefined and invalid if any duals violate
// their bounds.
LagrangianPart ComputeDualGradient(const ShardedQuadraticProgram& sharded_qp,
const Eigen::VectorXd& dual_solution,
const Eigen::VectorXd& primal_product);
struct SingularValueAndIterations {
double singular_value;
int num_iterations;
double estimated_relative_error;
};
// Estimates the maximum singular value of A by applying the method of power
// iteration to A^T A. If a primal and/or dual solution is provided, restricts
// to the "active" part of A, that is, the columns (rows) for variables that are
// not at their bounds in the solution. The estimate will have
// desired_relative_error with probability at least 1 - failure_probability.
// The number of iterations will be approximately
// log(primal_size / failure_probability^2)/(2 * desired_relative_error).
// Uses a mersenne-twister portable random number generator to generate the
// starting point for the power method, in order to have deterministic results.
SingularValueAndIterations EstimateMaximumSingularValueOfConstraintMatrix(
const ShardedQuadraticProgram& sharded_qp,
const absl::optional<Eigen::VectorXd>& primal_solution,
const absl::optional<Eigen::VectorXd>& dual_solution,
const double desired_relative_error, const double failure_probability,
std::mt19937& mt_generator);
// Checks if the lower and upper bounds of the problem are consistent, i.e. for
// each variable and constraint bound we have lower_bound <= upper_bound. If
// the input is consistent the method returns true, otherwise it returns false.
// See also HasValidBounds(const QuadraticProgram&).
bool HasValidBounds(const ShardedQuadraticProgram& sharded_qp);
// Projects a primal vector onto the variable bounds constraints.
void ProjectToPrimalVariableBounds(const ShardedQuadraticProgram& sharded_qp,
Eigen::VectorXd& primal);
// Projects the dual vector to the dual variable bounds; see
// https://developers.google.com/optimization/lp/pdlp_math#dual_variable_bounds.
void ProjectToDualVariableBounds(const ShardedQuadraticProgram& sharded_qp,
Eigen::VectorXd& dual);
} // namespace operations_research::pdlp
#endif // PDLP_SHARDED_OPTIMIZATION_UTILS_H_

View File

@@ -0,0 +1,616 @@
// Copyright 2010-2021 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/sharded_optimization_utils.h"
#include <cmath>
#include <cstdint>
#include <random>
#include <utility>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/types/optional.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/pdlp/sharded_quadratic_program.h"
#include "ortools/pdlp/sharder.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/test_util.h"
namespace operations_research::pdlp {
namespace {
using ::Eigen::VectorXd;
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
using ::testing::IsNan;
TEST(ShardedWeightedAverageTest, SimpleAverage) {
Sharder sharder(/*num_elements=*/2, /*num_shards=*/2,
/*thread_pool=*/nullptr);
Eigen::VectorXd vec1(2), vec2(2);
vec1 << 4, 1;
vec2 << 1, 7;
ShardedWeightedAverage average(&sharder);
average.Add(vec1, 1.0);
average.Add(vec2, 2.0);
ASSERT_TRUE(average.HasNonzeroWeight());
EXPECT_EQ(average.NumTerms(), 2);
EXPECT_THAT(average.ComputeAverage(), ElementsAre(2.0, 5.0));
average.Clear();
EXPECT_FALSE(average.HasNonzeroWeight());
EXPECT_EQ(average.NumTerms(), 0);
}
TEST(ShardedWeightedAverageTest, MoveConstruction) {
Sharder sharder(/*num_elements=*/2, /*num_shards=*/2,
/*thread_pool=*/nullptr);
Eigen::VectorXd vec(2);
vec << 4, 1;
ShardedWeightedAverage average(&sharder);
average.Add(vec, 2.0);
ShardedWeightedAverage average2(std::move(average));
EXPECT_THAT(average2.ComputeAverage(), ElementsAre(4.0, 1.0));
}
TEST(ShardedWeightedAverageTest, MoveAssignment) {
Sharder sharder1(/*num_elements=*/2, /*num_shards=*/2,
/*thread_pool=*/nullptr);
Sharder sharder2(/*num_elements=*/3, /*num_shards=*/2,
/*thread_pool=*/nullptr);
Eigen::VectorXd vec1(2), vec2(2);
vec1 << 4, 1;
vec2 << 0, 3;
ShardedWeightedAverage average1(&sharder1);
average1.Add(vec1, 2.0);
ShardedWeightedAverage average2(&sharder2);
average2 = std::move(average1);
average2.Add(vec2, 2.0);
EXPECT_THAT(average2.ComputeAverage(), ElementsAre(2.0, 2.0));
}
TEST(ShardedWeightedAverageTest, ZeroAverage) {
Sharder sharder(/*num_elements=*/1, /*num_shards=*/1,
/*thread_pool=*/nullptr);
ShardedWeightedAverage average(&sharder);
ASSERT_FALSE(average.HasNonzeroWeight());
EXPECT_THAT(average.ComputeAverage(), ElementsAre(0.0));
}
// This test verifies that if we average an identical vector repeatedly the
// average is exactly that vector, with no roundoff.
TEST(ShardedWeightedAverageTest, AveragesEqualWithoutRoundoff) {
Sharder sharder(/*num_elements=*/4, /*num_shards=*/1,
/*thread_pool=*/nullptr);
ShardedWeightedAverage average(&sharder);
EXPECT_THAT(average.ComputeAverage(), ElementsAre(0, 0, 0, 0));
VectorXd data(4);
data << 1.0, 1.0 / 3, 3.0 / 7, 3.14159;
average.Add(data, 341.45);
EXPECT_THAT(average.ComputeAverage(), ElementsAreArray(data));
average.Add(data, 1.4134);
EXPECT_THAT(average.ComputeAverage(), ElementsAreArray(data));
average.Add(data, 7.23);
EXPECT_THAT(average.ComputeAverage(), ElementsAreArray(data));
}
// The combined bounds vector for TestLp() is [12, 7, 4, 1].
// L_inf norm: 12.0
// L_2 norm: sqrt(210.0) ≈ 14.49
TEST(ProblemStatsTest, TestLp) {
ShardedQuadraticProgram lp(TestLp(), 2, 2);
const QuadraticProgramStats stats = ComputeStats(lp);
EXPECT_EQ(stats.num_variables(), 4);
EXPECT_EQ(stats.num_constraints(), 4);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_col_min_l_inf_norm(), 1.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_row_min_l_inf_norm(), 1.0);
EXPECT_EQ(stats.constraint_matrix_num_nonzeros(), 9);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_max(), 4.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_avg(), 14.5 / 9.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_max(), 5.5);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_avg(), 2.375);
EXPECT_DOUBLE_EQ(stats.objective_vector_l2_norm(), std::sqrt(36.25));
EXPECT_EQ(stats.objective_matrix_num_nonzeros(), 0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_min(), 0.0);
EXPECT_THAT(stats.objective_matrix_abs_avg(), IsNan());
EXPECT_EQ(stats.variable_bound_gaps_num_finite(), 1);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_max(), 1.0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_avg(), 1.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_max(), 12.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_avg(), 6.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_l2_norm(), std::sqrt(210.0));
}
TEST(ProblemStatsTest, TinyLp) {
ShardedQuadraticProgram lp(TinyLp(), 2, 2);
const QuadraticProgramStats stats = ComputeStats(lp);
EXPECT_EQ(stats.num_variables(), 4);
EXPECT_EQ(stats.num_constraints(), 3);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_col_min_l_inf_norm(), 1.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_row_min_l_inf_norm(), 1.0);
EXPECT_EQ(stats.constraint_matrix_num_nonzeros(), 8);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_max(), 2.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_avg(), 1.25);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_max(), 5.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_avg(), 2.25);
EXPECT_DOUBLE_EQ(stats.objective_vector_l2_norm(), std::sqrt(31.0));
EXPECT_EQ(stats.objective_matrix_num_nonzeros(), 0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_min(), 0.0);
EXPECT_THAT(stats.objective_matrix_abs_avg(), IsNan());
EXPECT_EQ(stats.variable_bound_gaps_num_finite(), 4);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_max(), 6.0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_min(), 2.0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_avg(), 3.75);
EXPECT_DOUBLE_EQ(stats.combined_bounds_max(), 12.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_avg(), 20.0 / 3.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_l2_norm(), std::sqrt(194.0));
}
TEST(ProblemStatsTest, TestDiagonalQp1) {
ShardedQuadraticProgram qp(TestDiagonalQp1(), 2, 2);
const QuadraticProgramStats stats = ComputeStats(qp);
EXPECT_EQ(stats.num_variables(), 2);
EXPECT_EQ(stats.num_constraints(), 1);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_col_min_l_inf_norm(), 1.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_row_min_l_inf_norm(), 1.0);
EXPECT_EQ(stats.constraint_matrix_num_nonzeros(), 2);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_max(), 1.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_avg(), 1.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_max(), 1.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_avg(), 1.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_l2_norm(), std::sqrt(2.0));
EXPECT_EQ(stats.objective_matrix_num_nonzeros(), 2);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_max(), 4.0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_avg(), 2.5);
EXPECT_EQ(stats.variable_bound_gaps_num_finite(), 2);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_max(), 6.0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_avg(), 3.5);
EXPECT_DOUBLE_EQ(stats.combined_bounds_max(), 1.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_avg(), 1.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_l2_norm(), 1.0);
}
TEST(ProblemStatsTest, ModifiedTestDiagonalQp1) {
QuadraticProgram orig_qp = TestDiagonalQp1();
// A case where objective_matrix_num_nonzeros doesn't match the dimension.
orig_qp.objective_matrix->diagonal() << 2.0, 0.0;
ShardedQuadraticProgram qp(orig_qp, 2, 2);
const QuadraticProgramStats stats = ComputeStats(qp);
EXPECT_EQ(stats.objective_matrix_num_nonzeros(), 1);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_max(), 2.0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_min(), 0.0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_avg(), 1.0);
}
// This is like SmallLp, except that an infinite_bound_threshold of 10 treats
// the first bound as infinite, leaving [7, 4, 1] as the combined bounds vector.
TEST(ProblemStatsTest, TestLpWithInfiniteConstraintBoundThreshold) {
ShardedQuadraticProgram lp(TestLp(), 2, 2);
const QuadraticProgramStats stats =
ComputeStats(lp, /*infinite_constraint_bound_threshold=*/10);
EXPECT_EQ(stats.num_variables(), 4);
EXPECT_EQ(stats.num_constraints(), 4);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_col_min_l_inf_norm(), 1.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_row_min_l_inf_norm(), 1.0);
EXPECT_EQ(stats.constraint_matrix_num_nonzeros(), 9);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_max(), 4.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_avg(), 14.5 / 9.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_max(), 5.5);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_avg(), 2.375);
EXPECT_DOUBLE_EQ(stats.objective_vector_l2_norm(), std::sqrt(36.25));
EXPECT_EQ(stats.objective_matrix_num_nonzeros(), 0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_min(), 0.0);
EXPECT_THAT(stats.objective_matrix_abs_avg(), IsNan());
EXPECT_EQ(stats.variable_bound_gaps_num_finite(), 1);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_max(), 1.0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_min(), 1.0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_avg(), 1.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_max(), 7.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_min(), 0.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_avg(), 3.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_l2_norm(), std::sqrt(66.0));
}
TEST(ProblemStatsTest, NoFiniteGaps) {
ShardedQuadraticProgram lp(SmallInvalidProblemLp(), 2, 2);
const QuadraticProgramStats stats = ComputeStats(lp);
// Ensure max/min/avg take their default values when no finite gaps exist.
EXPECT_EQ(stats.variable_bound_gaps_num_finite(), 0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_min(), 0.0);
EXPECT_THAT(stats.variable_bound_gaps_avg(), IsNan());
}
TEST(ProblemStatsTest, LpWithoutConstraints) {
ShardedQuadraticProgram lp(LpWithoutConstraints(), 2, 2);
const QuadraticProgramStats stats = ComputeStats(lp);
// When there are no constraints, max/min absolute values and infinity norms
// are assigned 0 by convention. The same is true for the combined bounds.
EXPECT_EQ(stats.constraint_matrix_num_nonzeros(), 0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_min(), 0.0);
EXPECT_THAT(stats.constraint_matrix_abs_avg(), IsNan());
EXPECT_DOUBLE_EQ(stats.constraint_matrix_col_min_l_inf_norm(), 0.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_row_min_l_inf_norm(), 0.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_min(), 0.0);
EXPECT_THAT(stats.combined_bounds_avg(), IsNan());
}
TEST(ProblemStatsTest, EmptyLp) {
ShardedQuadraticProgram lp(QuadraticProgram(0, 0), 2, 2);
const QuadraticProgramStats stats = ComputeStats(lp);
// When LP is empty, everything except averages should be 0 and averages
// should be NaN
EXPECT_EQ(stats.num_variables(), 0);
EXPECT_EQ(stats.num_constraints(), 0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_col_min_l_inf_norm(), 0.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_row_min_l_inf_norm(), 0.0);
EXPECT_EQ(stats.constraint_matrix_num_nonzeros(), 0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.constraint_matrix_abs_min(), 0.0);
EXPECT_THAT(stats.constraint_matrix_abs_avg(), IsNan());
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.objective_vector_abs_min(), 0.0);
EXPECT_THAT(stats.objective_vector_abs_avg(), IsNan());
EXPECT_DOUBLE_EQ(stats.objective_vector_l2_norm(), 0.0);
EXPECT_EQ(stats.objective_matrix_num_nonzeros(), 0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.objective_matrix_abs_min(), 0.0);
EXPECT_THAT(stats.objective_matrix_abs_avg(), IsNan());
EXPECT_EQ(stats.variable_bound_gaps_num_finite(), 0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.variable_bound_gaps_min(), 0.0);
EXPECT_THAT(stats.variable_bound_gaps_avg(), IsNan());
EXPECT_DOUBLE_EQ(stats.combined_bounds_max(), 0.0);
EXPECT_DOUBLE_EQ(stats.combined_bounds_min(), 0.0);
EXPECT_THAT(stats.combined_bounds_avg(), IsNan());
EXPECT_DOUBLE_EQ(stats.combined_bounds_l2_norm(), 0.0);
}
// The test_lp matrix is [ 2 1 1 2; 1 0 1 0; 4 0 0 0; 0 0 1.5 -1],
// the scaled matrix is [ 0 1 2 -2; 0 0 4 0; 0 0 0 0; 0 0 9 3],
// so the row LInf norms are [2 4 0 9] and the column LInf norms are [0 1 9 3].
// Rescaling divides the scaling vectors by sqrt(norms).
TEST(LInfRuizRescaling, OneIteration) {
ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
VectorXd row_scaling_vec(4), col_scaling_vec(4);
row_scaling_vec << 1, 2, 1, 3;
col_scaling_vec << 0, 1, 2, -1;
LInfRuizRescaling(lp, /*num_iterations=*/1, row_scaling_vec, col_scaling_vec);
EXPECT_THAT(row_scaling_vec, ElementsAre(1 / std::sqrt(2), 1.0, 1.0, 1.0));
EXPECT_THAT(col_scaling_vec,
ElementsAre(0.0, 1.0, 2.0 / 3.0, -1.0 / std::sqrt(3.0)));
}
// The test_lp matrix is [ 2 1 1 2; 1 0 1 0; 4 0 0 0; 0 0 1.5 -1],
// the scaled matrix is [ 0 1 2 -2; 0 0 4 0; 0 0 0 0; 0 0 9 3],
// so the row L2 norms are [3 4 0 sqrt(90)] and the column L2 norms are [0 1
// sqrt(101) sqrt(13)]. Rescaling divides the scaling vectors by sqrt(norms).
TEST(L2RuizRescaling, OneIteration) {
ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
VectorXd row_scaling_vec(4), col_scaling_vec(4);
row_scaling_vec << 1, 2, 1, 3;
col_scaling_vec << 0, 1, 2, -1;
L2NormRescaling(lp, row_scaling_vec, col_scaling_vec);
EXPECT_THAT(row_scaling_vec, ElementsAre(1.0 / std::pow(3.0, 0.5), 1.0, 1.0,
3.0 / std::pow(90.0, 0.25)));
EXPECT_THAT(col_scaling_vec, ElementsAre(0.0, 1.0, 2.0 / std::pow(101, 0.25),
-1.0 / std::pow(13.0, 0.25)));
}
// The test matrix is [2 3], so the row L2 norms are [sqrt(13)] and the column
// L2 norms are [2 3]. Rescaling divides the scaling vectors by sqrt(norms).
TEST(L2RuizRescaling, OneIterationNonSquare) {
QuadraticProgram test_lp(/*num_variables=*/2, /*num_constraints=*/1);
std::vector<Eigen::Triplet<double, int64_t>> triplets = {{0, 0, 2.0},
{0, 1, 3.0}};
test_lp.constraint_matrix.setFromTriplets(triplets.begin(), triplets.end());
ShardedQuadraticProgram lp(std::move(test_lp), /*num_threads=*/2,
/*num_shards=*/2);
VectorXd row_scaling_vec = VectorXd::Ones(1);
VectorXd col_scaling_vec = VectorXd::Ones(2);
L2NormRescaling(lp, row_scaling_vec, col_scaling_vec);
EXPECT_THAT(row_scaling_vec, ElementsAre(1.0 / std::pow(13.0, 0.25)));
EXPECT_THAT(col_scaling_vec,
ElementsAre(1.0 / std::sqrt(2.0), 1.0 / std::sqrt(3.0)));
}
// With many iterations of LInfRuizRescaling, the scaled matrix should converge
// to have col LInf norm 1 and row LInf norm 1.
TEST(LInfRuizRescaling, Convergence) {
ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
VectorXd row_scaling_vec(4), col_scaling_vec(4);
VectorXd col_norm(4), row_norm(4);
row_scaling_vec << 1, 1, 1, 1;
col_scaling_vec << 1, 1, 1, 1;
LInfRuizRescaling(lp, /*num_iterations=*/20, row_scaling_vec,
col_scaling_vec);
col_norm = ScaledColLInfNorm(lp.Qp().constraint_matrix, row_scaling_vec,
col_scaling_vec, lp.ConstraintMatrixSharder());
row_norm = ScaledColLInfNorm(lp.TransposedConstraintMatrix(), col_scaling_vec,
row_scaling_vec,
lp.TransposedConstraintMatrixSharder());
EXPECT_THAT(row_norm, EigenArrayNear<double>({1.0, 1.0, 1.0, 1.0}, 1.0e-4));
EXPECT_THAT(col_norm, EigenArrayNear<double>({1.0, 1.0, 1.0, 1.0}, 1.0e-4));
}
// This applies one round of l_inf and one round of L2 rescaling.
// The test_lp matrix is [ 2 1 1 2; 1 0 1 0; 4 0 0 0; 0 0 1.5 -1],
// so the row LInf norms are [2 1 4 1.5] and column LInf norms are [4 1 1.5 2].
// l_inf divides by sqrt(norms), giving
// [0.7071 0.7071 0.5773 1; 0.5 0 0.8165 0; 1 0 0 0; 0 0 1 -0.5773]
// which has row L2 norms [1.5275 0.957429 1 1.1547] and col L2 norms
// [1.3229 0.7071 1.4142 1.1547]. The resulting scaling vectors are
// 1/sqrt((l_inf norms).*(l2 norms)).
TEST(ApplyRescaling, ApplyRescalingWorksForTestLp) {
ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
ScalingVectors scaling = ApplyRescaling(
RescalingOptions{.l_inf_ruiz_iterations = 1, .l2_norm_rescaling = true},
lp);
EXPECT_THAT(scaling.row_scaling_vec,
EigenArrayNear<double>(
{1.0 / sqrt(2.0 * 1.5275), 1.0 / sqrt(1.0 * 0.9574),
1.0 / sqrt(4.0 * 1.0), 1.0 / sqrt(1.5 * 1.1547)},
1.0e-4));
EXPECT_THAT(scaling.col_scaling_vec,
EigenArrayNear<double>(
{1.0 / sqrt(4.0 * 1.3229), 1.0 / sqrt(1.0 * 0.7071),
1.0 / sqrt(1.5 * 1.4142), 1.0 / sqrt(2.0 * 1.1547)},
1.0e-4));
}
TEST(ComputePrimalGradientTest, CorrectForLp) {
// The choice of two shards is intentional, to help catch bugs in the sharded
// computations.
ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
VectorXd primal_solution(4), dual_solution(4);
primal_solution << 0.0, 0.0, 0.0, 3.0;
dual_solution << -1.0, 0.0, 1.0, 1.0;
const LagrangianPart primal_part = ComputePrimalGradient(
lp, primal_solution, lp.TransposedConstraintMatrix() * dual_solution);
// Using notation consistent with
// https://developers.google.com/optimization/lp/pdlp_math.
// c - A'y
EXPECT_THAT(primal_part.gradient,
ElementsAre(5.5 - 2.0, -2.0 + 1.0, -1.0 - 0.5, 1.0 + 3.0));
// c'x - y'Ax.
EXPECT_DOUBLE_EQ(primal_part.value, 3.0 + 9.0);
}
TEST(ComputeDualGradientTest, CorrectForLp) {
// The choice of two shards is intentional, to help catch bugs in the sharded
// computations.
ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
VectorXd primal_solution(4), dual_solution(4);
primal_solution << 0.0, 0.0, 0.0, 3.0;
dual_solution << -1.0, 0.0, 1.0, 1.0;
const LagrangianPart dual_part = ComputeDualGradient(
lp, dual_solution, lp.Qp().constraint_matrix * primal_solution);
// Using notation consistent with
// https://developers.google.com/optimization/lp/pdlp_math.
// active_constraint_right_hand_side - Ax
EXPECT_THAT(dual_part.gradient,
ElementsAre(12.0 - 6.0, 7.0, -4.0, -1.0 + 3.0));
// y'active_constraint_right_hand_side
EXPECT_DOUBLE_EQ(dual_part.value, 12.0 * -1.0 + -4.0 * 1.0 + -1.0 * 1.0);
}
TEST(ComputeDualGradientTest, CorrectOnTwoSidedConstraints) {
QuadraticProgram qp = TestLp();
// Makes the constraints all two-sided. The primal solution is feasible in
// the first constraint, below the lower bound of the second constraint, and
// above the upper bound of the third constraint.
qp.constraint_lower_bounds[0] = 4;
qp.constraint_lower_bounds[1] = 5;
qp.constraint_upper_bounds[2] = -1;
ShardedQuadraticProgram sharded_qp(std::move(qp), /*num_threads=*/2,
/*num_shards=*/2);
VectorXd primal_solution(4), dual_solution(4);
primal_solution << 0.0, 0.0, 0.0, 3.0;
dual_solution << 0.0, 0.0, 0.0, -1.0;
const LagrangianPart dual_part =
ComputeDualGradient(sharded_qp, dual_solution,
sharded_qp.Qp().constraint_matrix * primal_solution);
// Using notation consistent with
// https://developers.google.com/optimization/lp/pdlp_math.
// active_constraint_right_hand_side - Ax
EXPECT_THAT(dual_part.gradient,
ElementsAre(0.0, 5.0 - 0.0, -1.0 - 0.0, 1.0 + 3.0));
// y'active_constraint_right_hand_side
EXPECT_DOUBLE_EQ(dual_part.value, 1.0 * -1.0);
}
TEST(HasValidBoundsTest, SmallInvalidLp) {
ShardedQuadraticProgram lp(SmallInvalidProblemLp(), /*num_threads=*/2,
/*num_shards=*/2);
bool is_valid = HasValidBounds(lp);
EXPECT_FALSE(is_valid);
}
TEST(HasValidBoundsTest, SmallValidLp) {
ShardedQuadraticProgram lp(SmallPrimalInfeasibleLp(), /*num_threads=*/2,
/*num_shards=*/2);
bool is_valid = HasValidBounds(lp);
EXPECT_TRUE(is_valid);
}
TEST(ComputePrimalGradientTest, CorrectForQp) {
ShardedQuadraticProgram qp(TestDiagonalQp1(), /*num_threads=*/2,
/*num_shards=*/2);
VectorXd primal_solution(2), dual_solution(1);
primal_solution << 1.0, 2.0;
dual_solution << -2.0;
const LagrangianPart primal_part = ComputePrimalGradient(
qp, primal_solution, qp.TransposedConstraintMatrix() * dual_solution);
// Using notation consistent with
// https://developers.google.com/optimization/lp/pdlp_math.
// c - A'y + Qx
EXPECT_THAT(primal_part.gradient,
ElementsAre(-1.0 + 2.0 + 4.0, -1.0 + 2.0 + 2.0));
// (1/2) x'Qx + c'x - y'Ax.
EXPECT_DOUBLE_EQ(primal_part.value, 4.0 - 3.0 + 2.0 * 3.0);
}
TEST(ComputeDualGradientTest, CorrectForQp) {
ShardedQuadraticProgram qp(TestDiagonalQp1(), /*num_threads=*/2,
/*num_shards=*/2);
VectorXd primal_solution(2), dual_solution(1);
primal_solution << 1.0, 2.0;
dual_solution << -2.0;
const LagrangianPart dual_part = ComputeDualGradient(
qp, dual_solution, qp.Qp().constraint_matrix * primal_solution);
// Using notation consistent with
// https://developers.google.com/optimization/lp/pdlp_math.
// active_constraint_right_hand_side - Ax
EXPECT_THAT(dual_part.gradient, ElementsAre(1.0 - (1.0 + 2.0)));
// y'active_constraint_right_hand_side
EXPECT_DOUBLE_EQ(dual_part.value, -2.0);
}
TEST(EstimateSingularValuesTest, CorrectForTestLp) {
ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
// The test_lp matrix is [ 2 1 1 2; 1 0 1 0; 4 0 0 0; 0 0 1.5 -1].
std::mt19937 random(1);
auto result = EstimateMaximumSingularValueOfConstraintMatrix(
lp, absl::nullopt, absl::nullopt,
/*desired_relative_error=*/0.01,
/*failure_probability=*/0.001, random);
EXPECT_NEAR(result.singular_value, 4.76945, 0.01);
EXPECT_LT(result.num_iterations, 300);
}
TEST(EstimateSingularValuesTest, CorrectForTestLpWithActivePrimalSubspace) {
ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
VectorXd primal_solution(4);
// Chosen so x_1 is at its bound, and all other variables are not at bounds.
primal_solution << 0.0, -2.0, 0.0, 3.0;
// The test_lp matrix is [ 2 1 1 2; 1 0 1 0; 4 0 0 0; 0 0 1.5 -1],
// so the projected matrix is [ 2 1 2; 1 1 0; 4 0 0; 0 1.5 -1].
std::mt19937 random(1);
auto result = EstimateMaximumSingularValueOfConstraintMatrix(
lp, primal_solution, absl::nullopt, /*desired_relative_error=*/0.01,
/*failure_probability=*/0.001, random);
EXPECT_NEAR(result.singular_value, 4.73818, 0.01);
EXPECT_LT(result.num_iterations, 300);
}
TEST(EstimateSingularValuesTest, CorrectForTestLpWithActiveDualSubspace) {
ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
VectorXd dual_solution(4);
// Chosen so the second dual is at its bound, and all other duals are not at
// bounds.
dual_solution << 1.0, 0.0, 1.0, 3.0;
// The test_lp matrix is [ 2 1 1 2; 1 0 1 0; 4 0 0 0; 0 0 1.5 -1],
// so the projected matrix is [ 2 1 1 2; 4 0 0 0; 0 0 1.5 -1].
std::mt19937 random(1);
auto result = EstimateMaximumSingularValueOfConstraintMatrix(
lp, absl::nullopt, dual_solution, /*desired_relative_error=*/0.01,
/*failure_probability=*/0.001, random);
EXPECT_NEAR(result.singular_value, 4.64203, 0.01);
EXPECT_LT(result.num_iterations, 300);
}
TEST(EstimateSingularValuesTest, CorrectForTestLpWithBothActiveSubspaces) {
ShardedQuadraticProgram lp(TestLp(), /*num_threads=*/2, /*num_shards=*/2);
VectorXd primal_solution(4), dual_solution(4);
// Chosen so x_1 is at its bound, and all other variables are not at bounds.
primal_solution << 0.0, -2.0, 0.0, 3.0;
// Chosen so the second dual is at its bound, and all other duals are not at
// bounds.
dual_solution << 1.0, 0.0, 1.0, 3.0;
// The test_lp matrix is [ 2 1 1 2; 1 0 1 0; 4 0 0 0; 0 0 1.5 -1],
// so the projected matrix is [ 2 1 2; 4 0 0; 0 1.5 -1].
std::mt19937 random(1);
auto result = EstimateMaximumSingularValueOfConstraintMatrix(
lp, primal_solution, dual_solution, /*desired_relative_error=*/0.01,
/*failure_probability=*/0.001, random);
EXPECT_NEAR(result.singular_value, 4.60829, 0.01);
EXPECT_LT(result.num_iterations, 300);
}
TEST(ProjectToPrimalVariableBoundsTest, TestLp) {
ShardedQuadraticProgram qp(TestLp(), /*num_threads=*/2,
/*num_shards=*/2);
VectorXd primal(4);
primal << -3, -3, 5, 5;
ProjectToPrimalVariableBounds(qp, primal);
EXPECT_THAT(primal, ElementsAre(-3, -2, 5, 3.5));
}
TEST(ProjectToDualVariableBoundsTest, TestLp) {
ShardedQuadraticProgram qp(TestLp(), /*num_threads=*/2,
/*num_shards=*/2);
VectorXd dual(4);
dual << 1, 1, -1, -1;
ProjectToDualVariableBounds(qp, dual);
EXPECT_THAT(dual, ElementsAre(1, 0, 0, -1));
}
} // namespace
} // namespace operations_research::pdlp

View File

@@ -0,0 +1,155 @@
// Copyright 2010-2021 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/sharded_quadratic_program.h"
#include <cstdint>
#include <memory>
#include <utility>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/memory/memory.h"
#include "absl/strings/string_view.h"
#include "ortools/base/logging.h"
#include "ortools/base/threadpool.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/pdlp/sharder.h"
namespace operations_research::pdlp {
namespace {
// Logs a warning if the given matrix has more than density_limit non-zeros in
// a single column.
void WarnIfMatrixUnbalanced(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
absl::string_view matrix_name, int64_t density_limit) {
if (matrix.cols() == 0) return;
int64_t worst_column = 0;
for (int64_t col = 0; col < matrix.cols(); ++col) {
if (matrix.col(col).nonZeros() > matrix.col(worst_column).nonZeros()) {
worst_column = col;
}
}
if (matrix.col(worst_column).nonZeros() > density_limit) {
// TODO(user): fix this automatically in presolve instead of asking the
// user to do it.
LOG(WARNING)
<< "The " << matrix_name << " has "
<< matrix.col(worst_column).nonZeros() << " non-zeros in its "
<< worst_column
<< "th column. For best parallelization all rows and columns should "
"have at most order "
<< density_limit
<< " non-zeros. Consider rewriting the QP to split the corresponding "
"variable or constraint.";
}
}
} // namespace
ShardedQuadraticProgram::ShardedQuadraticProgram(QuadraticProgram qp,
const int num_threads,
const int num_shards)
: qp_(std::move(qp)),
transposed_constraint_matrix_(qp_.constraint_matrix.transpose()),
thread_pool_(num_threads == 1
? nullptr
: absl::make_unique<ThreadPool>("PDLP", num_threads)),
constraint_matrix_sharder_(qp_.constraint_matrix, num_shards,
thread_pool_.get()),
transposed_constraint_matrix_sharder_(transposed_constraint_matrix_,
num_shards, thread_pool_.get()),
primal_sharder_(qp_.variable_lower_bounds.size(), num_shards,
thread_pool_.get()),
dual_sharder_(qp_.constraint_lower_bounds.size(), num_shards,
thread_pool_.get()) {
CHECK_GE(num_threads, 1);
CHECK_GE(num_shards, num_threads);
if (num_threads > 1) {
thread_pool_->StartWorkers();
const int64_t work_per_iteration = qp_.constraint_matrix.nonZeros() +
qp_.variable_lower_bounds.size() +
qp_.constraint_lower_bounds.size();
const int64_t column_density_limit = work_per_iteration / num_threads;
WarnIfMatrixUnbalanced(qp_.constraint_matrix, "constraint matrix",
column_density_limit);
WarnIfMatrixUnbalanced(transposed_constraint_matrix_,
"transposed constraint matrix",
column_density_limit);
}
}
namespace {
// Multiply each entry of the matrix by the corresponding element of
// row_scaling_vec and col_scaling_vec, i.e., matrix[i,j] *= row_scaling_vec[i]
// * col_scaling_vec[j].
void ScaleMatrix(
const Eigen::VectorXd& col_scaling_vec,
const Eigen::VectorXd& row_scaling_vec, const Sharder& sharder,
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix) {
CHECK_EQ(matrix.cols(), col_scaling_vec.size());
CHECK_EQ(matrix.rows(), row_scaling_vec.size());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
auto matrix_shard = shard(matrix);
auto col_scaling_vec_shard = shard(col_scaling_vec);
for (int64_t col_num = 0; col_num < shard(matrix).outerSize(); ++col_num) {
for (decltype(matrix_shard)::InnerIterator it(matrix_shard, col_num); it;
++it) {
it.valueRef() *=
row_scaling_vec[it.row()] * col_scaling_vec_shard[it.col()];
}
}
});
}
} // namespace
void ShardedQuadraticProgram::RescaleQuadraticProgram(
const Eigen::VectorXd& col_scaling_vec,
const Eigen::VectorXd& row_scaling_vec) {
CHECK_EQ(PrimalSize(), col_scaling_vec.size());
CHECK_EQ(DualSize(), row_scaling_vec.size());
primal_sharder_.ParallelForEachShard([&](const Sharder::Shard& shard) {
CHECK((shard(col_scaling_vec).array() > 0.0).all());
shard(qp_.objective_vector) =
shard(qp_.objective_vector).cwiseProduct(shard(col_scaling_vec));
shard(qp_.variable_lower_bounds) =
shard(qp_.variable_lower_bounds).cwiseQuotient(shard(col_scaling_vec));
shard(qp_.variable_upper_bounds) =
shard(qp_.variable_upper_bounds).cwiseQuotient(shard(col_scaling_vec));
if (!IsLinearProgram(qp_)) {
shard(qp_.objective_matrix->diagonal()) =
shard(qp_.objective_matrix->diagonal())
.cwiseProduct(
shard(col_scaling_vec).cwiseProduct(shard(col_scaling_vec)));
}
});
dual_sharder_.ParallelForEachShard([&](const Sharder::Shard& shard) {
CHECK((shard(row_scaling_vec).array() > 0.0).all());
shard(qp_.constraint_lower_bounds) =
shard(qp_.constraint_lower_bounds).cwiseProduct(shard(row_scaling_vec));
shard(qp_.constraint_upper_bounds) =
shard(qp_.constraint_upper_bounds).cwiseProduct(shard(row_scaling_vec));
});
ScaleMatrix(col_scaling_vec, row_scaling_vec, constraint_matrix_sharder_,
qp_.constraint_matrix);
ScaleMatrix(row_scaling_vec, col_scaling_vec,
transposed_constraint_matrix_sharder_,
transposed_constraint_matrix_);
}
} // namespace operations_research::pdlp

View File

@@ -0,0 +1,91 @@
// Copyright 2010-2021 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.
#ifndef PDLP_SHARDED_QUADRATIC_PROGRAM_H_
#define PDLP_SHARDED_QUADRATIC_PROGRAM_H_
#include <cstdint>
#include <memory>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "ortools/base/threadpool.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/pdlp/sharder.h"
namespace operations_research::pdlp {
// This class stores:
// - A QuadraticProgram (QP)
// - A transposed version of the QP's constraint matrix
// - A thread pool
// - Various Sharder objects for doing sharded matrix and vector computations.
class ShardedQuadraticProgram {
public:
// Requires num_shards >= num_threads >= 1.
// Note that the qp is intentionally passed by value.
ShardedQuadraticProgram(QuadraticProgram qp, int num_threads, int num_shards);
// Movable but not copyable.
ShardedQuadraticProgram(const ShardedQuadraticProgram&) = delete;
ShardedQuadraticProgram& operator=(const ShardedQuadraticProgram&) = delete;
ShardedQuadraticProgram(ShardedQuadraticProgram&&) = default;
ShardedQuadraticProgram& operator=(ShardedQuadraticProgram&&) = default;
const QuadraticProgram& Qp() const { return qp_; }
// Returns a reference to the transpose of the QP's constraint matrix.
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>&
TransposedConstraintMatrix() const {
return transposed_constraint_matrix_;
}
// Returns a Sharder intended for the columns of the QP's constraint matrix.
const Sharder& ConstraintMatrixSharder() const {
return constraint_matrix_sharder_;
}
// Returns a Sharder intended for the rows of the QP's constraint matrix.
const Sharder& TransposedConstraintMatrixSharder() const {
return transposed_constraint_matrix_sharder_;
}
// Returns a Sharder intended for primal vectors.
const Sharder& PrimalSharder() const { return primal_sharder_; }
// Returns a Sharder intended for dual vectors.
const Sharder& DualSharder() const { return dual_sharder_; }
int64_t PrimalSize() const { return qp_.variable_lower_bounds.size(); }
int64_t DualSize() const { return qp_.constraint_lower_bounds.size(); }
// Rescale the QP (including objective, variable bounds, constraint bounds,
// constraint matrix, and transposed constraint matrix) based on
// col_scaling_vec and row_scaling_vec. That is, rescale the problem so that
// each variable is rescaled as variable[i] <- variable[i] /
// col_scaling_vec[i], and the j-th constraint is multiplied by
// row_scaling_vec[j]. col_scaling_vec and row_scaling_vec must be positive.
void RescaleQuadraticProgram(const Eigen::VectorXd& col_scaling_vec,
const Eigen::VectorXd& row_scaling_vec);
private:
QuadraticProgram qp_;
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>
transposed_constraint_matrix_;
std::unique_ptr<ThreadPool> thread_pool_;
Sharder constraint_matrix_sharder_;
Sharder transposed_constraint_matrix_sharder_;
Sharder primal_sharder_;
Sharder dual_sharder_;
};
} // namespace operations_research::pdlp
#endif // PDLP_SHARDED_QUADRATIC_PROGRAM_H_

View File

@@ -0,0 +1,84 @@
// Copyright 2010-2021 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/sharded_quadratic_program.h"
#include <limits>
#include "Eigen/Core"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/pdlp/quadratic_program.h"
#include "ortools/pdlp/sharder.h"
#include "ortools/pdlp/test_util.h"
namespace operations_research::pdlp {
namespace {
using ::testing::ElementsAre;
const double kInfinity = std::numeric_limits<double>::infinity();
TEST(ShardedQuadraticProgramTest, BasicTest) {
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestDiagonalQp1(), num_threads,
num_shards);
const int primal_size = 2;
const int dual_size = 1;
EXPECT_EQ(sharded_qp.DualSize(), dual_size);
EXPECT_EQ(sharded_qp.PrimalSize(), primal_size);
EXPECT_THAT(ToDense(sharded_qp.TransposedConstraintMatrix()),
EigenArrayEq<double>({{1}, {1}}));
EXPECT_EQ(sharded_qp.ConstraintMatrixSharder().NumElements(), primal_size);
EXPECT_EQ(sharded_qp.DualSharder().NumElements(), dual_size);
EXPECT_EQ(sharded_qp.PrimalSharder().NumElements(), primal_size);
EXPECT_EQ(sharded_qp.TransposedConstraintMatrixSharder().NumElements(),
dual_size);
}
TEST(RescaleProblem, BasicTest) {
// The original qp is:
// min 4x_1^2 + x_2^2 - x_1 - x_2 +5
// s.t. -inf <= x_1 + x_2 <=1,
// 1<=x_1<=2, -2<=x_2<=4.
// After rescaling with row_scaling_vec [0.5] and col_scaling_vec [1,0.5],
// the new qp becomes:
// min 4x_1^2 + 0.25x_2^2 - x_1 - 0.5x_2 +5
// s.t. -inf <= 0.5x_1 + 0.25x_2 <=1,
// 1<=x_1<=2, -4<=x_2<=8.
const int num_threads = 2;
const int num_shards = 10;
ShardedQuadraticProgram sharded_qp(TestDiagonalQp1(), num_threads,
num_shards);
Eigen::VectorXd col_scaling_vec(2);
Eigen::VectorXd row_scaling_vec(1);
col_scaling_vec << 1, 0.5;
row_scaling_vec << 0.5;
sharded_qp.RescaleQuadraticProgram(col_scaling_vec, row_scaling_vec);
EXPECT_THAT(sharded_qp.Qp().constraint_lower_bounds, ElementsAre(-kInfinity));
EXPECT_THAT(sharded_qp.Qp().constraint_upper_bounds, ElementsAre(0.5));
EXPECT_THAT(sharded_qp.Qp().variable_lower_bounds, ElementsAre(1, -4));
EXPECT_THAT(sharded_qp.Qp().variable_upper_bounds, ElementsAre(2, 8));
EXPECT_THAT(sharded_qp.Qp().objective_vector, ElementsAre(-1, -0.5));
EXPECT_THAT(ToDense(sharded_qp.Qp().constraint_matrix),
EigenArrayEq<double>({{0.5, 0.25}}));
EXPECT_THAT(ToDense(sharded_qp.TransposedConstraintMatrix()),
EigenArrayEq<double>({{0.5}, {0.25}}));
EXPECT_THAT(sharded_qp.Qp().objective_matrix->diagonal(),
EigenArrayEq<double>({4, 0.25}));
}
} // namespace
} // namespace operations_research::pdlp

320
ortools/pdlp/sharder.cc Normal file
View File

@@ -0,0 +1,320 @@
// Copyright 2010-2021 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/sharder.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <functional>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/synchronization/blocking_counter.h"
#include "absl/time/time.h"
#include "ortools/base/logging.h"
#include "ortools/base/mathutil.h"
#include "ortools/base/threadpool.h"
#include "ortools/base/timer.h"
namespace operations_research::pdlp {
using ::Eigen::VectorXd;
Sharder::Sharder(const int64_t num_elements, const int num_shards,
ThreadPool* const thread_pool,
const std::function<int64_t(int64_t)>& element_mass)
: thread_pool_(thread_pool) {
CHECK_GE(num_elements, 0);
if (num_elements == 0) {
shard_starts_.push_back(0);
return;
}
CHECK_GE(num_shards, 1);
int64_t overall_mass = 0;
for (int64_t elem = 0; elem < num_elements; ++elem) {
overall_mass += element_mass(elem);
}
shard_starts_.push_back(0);
int64_t this_shard_mass = element_mass(0);
for (int64_t elem = 1; elem < num_elements; ++elem) {
int64_t this_elem_mass = element_mass(elem);
if (this_shard_mass + (this_elem_mass / 2) >= overall_mass / num_shards) {
// this elem starts a new shard
shard_masses_.push_back(this_shard_mass);
shard_starts_.push_back(elem);
this_shard_mass = this_elem_mass;
} else {
this_shard_mass += this_elem_mass;
}
}
shard_starts_.push_back(num_elements);
shard_masses_.push_back(this_shard_mass);
CHECK_EQ(NumShards(), shard_masses_.size());
}
Sharder::Sharder(const int64_t num_elements, const int num_shards,
ThreadPool* const thread_pool)
: thread_pool_(thread_pool) {
CHECK_GE(num_elements, 0);
if (num_elements == 0) {
shard_starts_.push_back(0);
return;
}
CHECK_GE(num_shards, 1);
shard_starts_.reserve(num_shards + 1);
shard_masses_.reserve(num_shards);
for (int shard = 0; shard < num_shards; ++shard) {
const int64_t this_shard_start = ((num_elements * shard) / num_shards);
const int64_t next_shard_start =
((num_elements * (shard + 1)) / num_shards);
if (next_shard_start - this_shard_start > 0) {
shard_starts_.push_back(this_shard_start);
shard_masses_.push_back(next_shard_start - this_shard_start);
}
}
shard_starts_.push_back(num_elements);
CHECK_EQ(NumShards(), shard_masses_.size());
}
Sharder::Sharder(const Sharder& other_sharder, const int64_t num_elements)
// The std::max() protects against other_sharder.NumShards() == 0, which
// will happen if other_sharder had num_elements == 0.
: Sharder(num_elements, std::max(1, other_sharder.NumShards()),
other_sharder.thread_pool_) {}
void Sharder::ParallelForEachShard(
const std::function<void(const Shard&)>& func) const {
if (thread_pool_) {
absl::BlockingCounter counter(NumShards());
VLOG(2) << "Starting ParallelForEachShard()";
for (int shard_num = 0; shard_num < NumShards(); ++shard_num) {
thread_pool_->Schedule([&, shard_num]() {
WallTimer timer;
if (VLOG_IS_ON(2)) {
timer.Start();
}
func(Shard(shard_num, this));
if (VLOG_IS_ON(2)) {
timer.Stop();
VLOG(2) << "Shard " << shard_num << " with " << ShardSize(shard_num)
<< " elements and " << ShardMass(shard_num)
<< " mass finished with "
<< ShardMass(shard_num) /
std::max(int64_t{1}, absl::ToInt64Microseconds(
timer.GetDuration()))
<< " mass/usec.";
}
counter.DecrementCount();
});
}
counter.Wait();
VLOG(2) << "Done ParallelForEachShard()";
} else {
for (int shard_num = 0; shard_num < NumShards(); ++shard_num) {
func(Shard(shard_num, this));
}
}
}
double Sharder::ParallelSumOverShards(
const std::function<double(const Shard&)>& func) const {
VectorXd local_sums(NumShards());
ParallelForEachShard([&](const Sharder::Shard& shard) {
local_sums[shard.Index()] = func(shard);
});
return local_sums.sum();
}
bool Sharder::ParallelTrueForAllShards(
const std::function<bool(const Shard&)>& func) const {
// Recall std::vector<bool> is not thread-safe.
std::vector<int> local_result(NumShards());
ParallelForEachShard([&](const Sharder::Shard& shard) {
local_result[shard.Index()] = static_cast<int>(func(shard));
});
return std::all_of(local_result.begin(), local_result.end(),
[](const int v) { return static_cast<bool>(v); });
}
VectorXd TransposedMatrixVectorProduct(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const VectorXd& vector, const Sharder& sharder) {
CHECK_EQ(vector.size(), matrix.rows());
VectorXd answer(matrix.cols());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
shard(answer) = shard(matrix).transpose() * vector;
});
return answer;
}
void AddScaledVector(const double scale, const VectorXd& increment,
const Sharder& sharder, VectorXd& dest) {
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
shard(dest) += scale * shard(increment);
});
}
void AssignVector(const VectorXd& vec, const Sharder& sharder, VectorXd& dest) {
dest.resize(vec.size());
sharder.ParallelForEachShard(
[&](const Sharder::Shard& shard) { shard(dest) = shard(vec); });
}
VectorXd CloneVector(const VectorXd& vec, const Sharder& sharder) {
VectorXd dest;
AssignVector(vec, sharder, dest);
return dest;
}
// Like vector = vector.cwiseProduct(scale).
void CoefficientWiseProductInPlace(const VectorXd& scale,
const Sharder& sharder, VectorXd& dest) {
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
shard(dest) = shard(dest).cwiseProduct(shard(scale));
});
}
// Like vector = vector.cwiseQuotient(scale).
void CoefficientWiseQuotientInPlace(const VectorXd& scale,
const Sharder& sharder, VectorXd& dest) {
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
shard(dest) = shard(dest).cwiseQuotient(shard(scale));
});
}
double Dot(const VectorXd& v1, const VectorXd& v2, const Sharder& sharder) {
return sharder.ParallelSumOverShards(
[&](const Sharder::Shard& shard) { return shard(v1).dot(shard(v2)); });
}
double LInfNorm(const VectorXd& vector, const Sharder& sharder) {
VectorXd local_max(sharder.NumShards());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
local_max[shard.Index()] = shard(vector).lpNorm<Eigen::Infinity>();
});
return local_max.lpNorm<Eigen::Infinity>();
}
double L1Norm(const VectorXd& vector, const Sharder& sharder) {
return sharder.ParallelSumOverShards(
[&](const Sharder::Shard& shard) { return shard(vector).lpNorm<1>(); });
}
double SquaredNorm(const VectorXd& vector, const Sharder& sharder) {
return sharder.ParallelSumOverShards(
[&](const Sharder::Shard& shard) { return shard(vector).squaredNorm(); });
}
double Norm(const VectorXd& vector, const Sharder& sharder) {
return std::sqrt(SquaredNorm(vector, sharder));
}
double SquaredDistance(const VectorXd& vector1, const VectorXd& vector2,
const Sharder& sharder) {
return sharder.ParallelSumOverShards([&](const Sharder::Shard& shard) {
return (shard(vector1) - shard(vector2)).squaredNorm();
});
}
double Distance(const VectorXd& vector1, const VectorXd& vector2,
const Sharder& sharder) {
return std::sqrt(SquaredDistance(vector1, vector2, sharder));
}
double ScaledLInfNorm(const VectorXd& vector, const VectorXd& scale,
const Sharder& sharder) {
VectorXd local_max(sharder.NumShards());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
local_max[shard.Index()] =
shard(vector).cwiseProduct(shard(scale)).lpNorm<Eigen::Infinity>();
});
return local_max.lpNorm<Eigen::Infinity>();
}
double ScaledSquaredNorm(const VectorXd& vector, const VectorXd& scale,
const Sharder& sharder) {
return sharder.ParallelSumOverShards([&](const Sharder::Shard& shard) {
return shard(vector).cwiseProduct(shard(scale)).squaredNorm();
});
}
double ScaledNorm(const VectorXd& vector, const VectorXd& scale,
const Sharder& sharder) {
return std::sqrt(ScaledSquaredNorm(vector, scale, sharder));
}
VectorXd ScaledColLInfNorm(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const VectorXd& row_scaling_vec, const VectorXd& col_scaling_vec,
const Sharder& sharder) {
CHECK_EQ(matrix.cols(), col_scaling_vec.size());
CHECK_EQ(matrix.rows(), row_scaling_vec.size());
VectorXd answer(matrix.cols());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
auto matrix_shard = shard(matrix);
auto col_scaling_shard = shard(col_scaling_vec);
for (int64_t col_num = 0; col_num < shard(matrix).outerSize(); ++col_num) {
double max = 0.0;
for (decltype(matrix_shard)::InnerIterator it(matrix_shard, col_num); it;
++it) {
max = std::max(max, std::abs(it.value() * row_scaling_vec[it.row()]));
}
shard(answer)[col_num] = max * std::abs(col_scaling_shard[col_num]);
}
});
return answer;
}
VectorXd ScaledColL2Norm(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const VectorXd& row_scaling_vec, const VectorXd& col_scaling_vec,
const Sharder& sharder) {
CHECK_EQ(matrix.cols(), col_scaling_vec.size());
CHECK_EQ(matrix.rows(), row_scaling_vec.size());
VectorXd answer(matrix.cols());
sharder.ParallelForEachShard([&](const Sharder::Shard& shard) {
auto matrix_shard = shard(matrix);
auto col_scaling_shard = shard(col_scaling_vec);
for (int64_t col_num = 0; col_num < shard(matrix).outerSize(); ++col_num) {
double sum_of_squares = 0.0;
for (decltype(matrix_shard)::InnerIterator it(matrix_shard, col_num); it;
++it) {
sum_of_squares +=
MathUtil::Square(it.value() * row_scaling_vec[it.row()]);
}
shard(answer)[col_num] =
std::sqrt(sum_of_squares) * std::abs(col_scaling_shard[col_num]);
}
});
return answer;
}
bool IsDiagonal(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const Sharder& sharder) {
return sharder.ParallelTrueForAllShards([&](const Sharder::Shard& shard) {
auto matrix_shard = shard(matrix);
const int64_t col_offset = sharder.ShardStart(shard.Index());
for (int64_t col_idx = 0; col_idx < matrix_shard.outerSize(); ++col_idx) {
for (decltype(matrix_shard)::InnerIterator it(matrix_shard, col_idx); it;
++it) {
if (it.row() != (col_offset + it.col())) return false;
}
}
return true;
});
}
} // namespace operations_research::pdlp

330
ortools/pdlp/sharder.h Normal file
View File

@@ -0,0 +1,330 @@
// Copyright 2010-2021 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.
#ifndef PDLP_SHARDER_H_
#define PDLP_SHARDER_H_
#include <cstdint>
#include <functional>
#include <type_traits>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "ortools/base/logging.h"
#include "ortools/base/threadpool.h"
namespace operations_research::pdlp {
// This class represents a way to shard elements for multi-threading. The
// elements may be entries of a vector or the columns of a (column-major)
// matrix. The shards are selected to have roughly the same mass, where the mass
// of an entry depends on the constructor used. See the free functions below and
// in the .cc file for example usage.
class Sharder {
public:
// These are public aliases for convenience. They will change only if there
// are breaking changes in Eigen.
using ConstSparseColumnBlock = ::Eigen::Block<
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>,
/*BlockRows=*/Eigen::Dynamic, /*BlockCols=*/Eigen::Dynamic,
/*InnerPanel=*/true>;
using SparseColumnBlock =
::Eigen::Block<Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>,
/*BlockRows=*/Eigen::Dynamic, /*BlockCols=*/Eigen::Dynamic,
/*InnerPanel=*/true>;
// This class extracts a particular shard of vectors or matrices passed to it.
// See ParallelForEachShard().
// Caution: Like absl::Span, Shard::operator() returns mutable or immutable
// views into the vector or matrix argument. The underlying object must
// outlive the view.
// Extra Caution: The const& arguments for the immutable views can bind to
// temporary objects, e.g., shard(3*a) will create a view into the "3*a"
// object that will be destroyed immediately after the shard is created.
class Shard {
public:
// Returns this shard of the given vector.
Eigen::VectorBlock<const Eigen::VectorXd> operator()(
const Eigen::VectorXd& vector) const {
CHECK_EQ(vector.size(), parent_.NumElements());
return vector.segment(parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
}
// Returns this shard of the given vector in mutable form.
Eigen::VectorBlock<Eigen::VectorXd> operator()(
Eigen::VectorXd& vector) const {
CHECK_EQ(vector.size(), parent_.NumElements());
return vector.segment(parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
}
// Returns this shard of the given VectorBlock.
Eigen::VectorBlock<const Eigen::VectorXd> operator()(
Eigen::VectorBlock<const Eigen::VectorXd> vector) const {
CHECK_EQ(vector.size(), parent_.NumElements());
return Eigen::VectorBlock<const Eigen::VectorXd>(
vector.nestedExpression(),
vector.startRow() + parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
}
// Returns this shard of the given VectorBlock in mutable form.
Eigen::VectorBlock<Eigen::VectorXd> operator()(
Eigen::VectorBlock<Eigen::VectorXd> vector) const {
CHECK_EQ(vector.size(), parent_.NumElements());
return Eigen::VectorBlock<Eigen::VectorXd>(
vector.nestedExpression(),
vector.startRow() + parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
}
// Returns this shard of the given DiagonalMatrix. Note that the shard is
// a *square* diagonal matrix, not a block of columns of original length.
auto operator()(const Eigen::DiagonalMatrix<double, Eigen::Dynamic>& diag)
const -> decltype(diag.diagonal().segment(0, 0).asDiagonal()) {
CHECK_EQ(diag.diagonal().size(), parent_.NumElements());
return diag.diagonal()
.segment(parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_))
.asDiagonal();
}
// Returns this shard of the columns of the given matrix.
ConstSparseColumnBlock operator()(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix)
const {
CHECK_EQ(matrix.cols(), parent_.NumElements());
auto result = matrix.middleCols(parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
// This is a guard against implicit conversions, because the return type
// of middleCols is not 100% clear from the documentation.
static_assert(
std::is_same<decltype(result), ConstSparseColumnBlock>::value,
"The return type of middleCols changed!");
return result;
}
// Returns this shard of the columns of the given matrix in mutable form.
SparseColumnBlock operator()(
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix) const {
CHECK_EQ(matrix.cols(), parent_.NumElements());
auto result = matrix.middleCols(parent_.ShardStart(shard_num_),
parent_.ShardSize(shard_num_));
// This is a guard against implicit conversions, because the return type
// of middleCols is not 100% clear from the documentation.
static_assert(std::is_same<decltype(result), SparseColumnBlock>::value,
"The return type of middleCols changed!");
return result;
}
// A non-negative identifier for this shard, less than the parent Sharder's
// NumShards().
int Index() const { return shard_num_; }
private:
friend class Sharder;
Shard(int shard_num, const Sharder* parent)
: shard_num_(shard_num), parent_(*parent) {
CHECK_NE(parent, nullptr);
CHECK_GE(shard_num, 0);
CHECK_LT(shard_num, parent->NumShards());
}
const int shard_num_;
const Sharder& parent_;
};
// Creates a Sharder for problems with `num_elements` elements and mass of
// each element given by `element_mass`. Each shard will have roughly the same
// mass. The number of shards in the resulting Sharder will be approximately
// `num_shards` but may differ. The `thread_pool` will be used for parallel
// operations executed by e.g. ParallelForEachShard(). The `thread_pool` may
// be nullptr, which means work will be executed in the same thread. If
// `thread_pool` is not nullptr, the underlying object is not owned and must
// outlive the Sharder.
Sharder(int64_t num_elements, int num_shards, ThreadPool* thread_pool,
const std::function<int64_t(int64_t)>& element_mass);
// Creates a Sharder for problems with `num_elements` elements and unit mass.
// This constructor exploits having all element mass equal to 1 to take time
// proportional to num_shards instead of num_elements. Also see the comments
// above the first constructor.
Sharder(int64_t num_elements, int num_shards, ThreadPool* thread_pool);
// Creates a Sharder for processing the given matrix. The elements correspond
// to columns of the matrix and have mass linear in the number of non-zeros.
// Also see the comments above the first constructor.
Sharder(const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
int num_shards, ThreadPool* thread_pool)
: Sharder(matrix.cols(), num_shards, thread_pool, [&matrix](int64_t col) {
return 1 + 1 * matrix.col(col).nonZeros();
}) {}
// Constructs a Sharder with the same thread pool as `other_sharder`, for
// problems with `num_elements` elements and unit mass. The number of shards
// will be approximately the same as that of `other_sharder`. Also see the
// comments on the first constructor.
Sharder(const Sharder& other_sharder, int64_t num_elements);
// Sharders may be copied or moved.
// Moved-from objects may be in an invalid state. The only methods that may be
// called on a moved-from object are the destructor or operator=.
Sharder(const Sharder& other) = default;
Sharder(Sharder&& other) = default;
Sharder& operator=(const Sharder& other) = default;
Sharder& operator=(Sharder&& other) = default;
int NumShards() const { return static_cast<int>(shard_starts_.size()) - 1; }
// The number of elements that are split into shards.
int64_t NumElements() const { return shard_starts_.back(); }
int64_t ShardSize(int shard) const {
CHECK_GE(shard, 0);
CHECK_LT(shard, NumShards());
return shard_starts_[shard + 1] - shard_starts_[shard];
}
int64_t ShardStart(int shard) const {
CHECK_GE(shard, 0);
CHECK_LT(shard, NumShards());
return shard_starts_[shard];
}
int64_t ShardMass(int shard) const {
CHECK_GE(shard, 0);
CHECK_LT(shard, NumShards());
return shard_masses_[shard];
}
// Runs a functor on each of the shards.
void ParallelForEachShard(
const std::function<void(const Shard&)>& func) const;
// Runs a functor on each of the shards and sums the results.
double ParallelSumOverShards(
const std::function<double(const Shard&)>& func) const;
// Runs a functor on each of the shards. Returns true iff all shards returned
// true.
bool ParallelTrueForAllShards(
const std::function<bool(const Shard&)>& func) const;
// Public for testing only.
const std::vector<int64_t>& ShardStartsForTesting() const {
return shard_starts_;
}
private:
// Size: NumShards() + 1. The first entry is 0 and the last entry is
// NumElements(). The entries are sorted in increasing order and are unique.
// Note that {0} is valid and indicates zero elements split into zero shards.
std::vector<int64_t> shard_starts_;
// Size: NumShards(). The mass of each shard.
std::vector<int64_t> shard_masses_;
// NOT owned. May be nullptr.
ThreadPool* thread_pool_;
};
// Like matrix.transpose() * vector but executed in parallel using the given
// Sharder. The Sharder's size must match the matrix's number of columns. To
// ensure good parallelization the matrix passed here should have (roughly) the
// same location of non-zeros as the matrix passed to the Sharder's
// constructor.
Eigen::VectorXd TransposedMatrixVectorProduct(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const Eigen::VectorXd& vector, const Sharder& Sharder);
////////////////////////////////////////////////////////////////////////////////
// The following functions use a Sharder to compute a vector operation in
// parallel. The Sharder should have the same size as the vector(s). For best
// performance the Sharder should have been created with the Sharder(int64_t,
// int, ThreadPool*) constructor.
////////////////////////////////////////////////////////////////////////////////
// Like dest += scale * increment
void AddScaledVector(double scale, const Eigen::VectorXd& increment,
const Sharder& sharder, Eigen::VectorXd& dest);
// Like dest = vec. dest is resized if needed.
void AssignVector(const Eigen::VectorXd& vec, const Sharder& sharder,
Eigen::VectorXd& dest);
// Returns a copy of vec.
Eigen::VectorXd CloneVector(const Eigen::VectorXd& vec, const Sharder& sharder);
// Like dest = dest.cwiseProduct(scale).
void CoefficientWiseProductInPlace(const Eigen::VectorXd& scale,
const Sharder& sharder,
Eigen::VectorXd& dest);
// Like dest = dest.cwiseQuotient(scale).
void CoefficientWiseQuotientInPlace(const Eigen::VectorXd& scale,
const Sharder& sharder,
Eigen::VectorXd& dest);
// Like v1.dot(v2)
double Dot(const Eigen::VectorXd& v1, const Eigen::VectorXd& v2,
const Sharder& sharder);
// Like vector.lpNorm<Eigen::Infinity>(), a.k.a. LInf norm.
double LInfNorm(const Eigen::VectorXd& vector, const Sharder& sharder);
// Like vector.lpNorm<1>(), a.k.a. L_1 norm.
double L1Norm(const Eigen::VectorXd& vector, const Sharder& sharder);
// Like vector.squaredNorm()
double SquaredNorm(const Eigen::VectorXd& vector, const Sharder& sharder);
// Like vector.norm()
double Norm(const Eigen::VectorXd& vector, const Sharder& sharder);
// Like (vector1 - vector2).squaredNorm()
double SquaredDistance(const Eigen::VectorXd& vector1,
const Eigen::VectorXd& vector2, const Sharder& sharder);
// Like (vector1 - vector2).norm()
double Distance(const Eigen::VectorXd& vector1, const Eigen::VectorXd& vector2,
const Sharder& sharder);
// ScaledL1Norm is omitted because it's not needed (yet).
// LInf norm of a rescaled vector, i.e.,
// vector.cwiseProduct(scale).lpNorm<Eigen::Infinity>().
double ScaledLInfNorm(const Eigen::VectorXd& vector,
const Eigen::VectorXd& scale, const Sharder& sharder);
// Squared L2 norm of a rescaled vector, i.e.,
// vector.cwiseProduct(scale).squaredNorm().
double ScaledSquaredNorm(const Eigen::VectorXd& vector,
const Eigen::VectorXd& scale, const Sharder& sharder);
// L2 norm of a rescaled vector, i.e., vector.cwiseProduct(scale).norm().
double ScaledNorm(const Eigen::VectorXd& vector, const Eigen::VectorXd& scale,
const Sharder& sharder);
////////////////////////////////////////////////////////////////////////////////
// The functions below compute norms of the columns of a scaled matrix. The
// (i,j) entry of the scaled matrix equals matrix[i,j] * row_scaling_vec[i] *
// col_scaling_vec[j]. To ensure good parallelization the matrix passed here
// should have (roughly) the same location of non-zeros as the matrix passed to
// the Sharder's constructor. The Sharder's size must match the matrix's number
// of columns.
////////////////////////////////////////////////////////////////////////////////
// Computes the LInf norm of each column of a scaled matrix.
Eigen::VectorXd ScaledColLInfNorm(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const Eigen::VectorXd& row_scaling_vec,
const Eigen::VectorXd& col_scaling_vec, const Sharder& sharder);
// Computes the L2 norm of each column of a scaled matrix.
Eigen::VectorXd ScaledColL2Norm(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const Eigen::VectorXd& row_scaling_vec,
const Eigen::VectorXd& col_scaling_vec, const Sharder& sharder);
// Checks if a matrix is diagonal.
bool IsDiagonal(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix,
const Sharder& sharder);
} // namespace operations_research::pdlp
#endif // PDLP_SHARDER_H_

View File

@@ -0,0 +1,496 @@
// Copyright 2010-2021 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/sharder.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <numeric>
#include <random>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/random/distributions.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/base/logging.h"
#include "ortools/base/mathutil.h"
#include "ortools/base/threadpool.h"
namespace operations_research::pdlp {
namespace {
using ::Eigen::DiagonalMatrix;
using ::Eigen::VectorXd;
using ::testing::DoubleNear;
using ::testing::ElementsAre;
using ::testing::Test;
using Shard = Sharder::Shard;
// Returns a sparse representation of the matrix
// 7 -0.5 . .
// 1 . 3 2
// -1 . . 5
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> TestSparseMatrix() {
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> mat(3, 4);
mat.coeffRef(0, 0) = 7;
mat.coeffRef(0, 1) = -0.5;
mat.coeffRef(1, 0) = 1;
mat.coeffRef(1, 2) = 3;
mat.coeffRef(1, 3) = 2;
mat.coeffRef(2, 0) = -1;
mat.coeffRef(2, 3) = 5;
mat.makeCompressed();
return mat;
}
// A random matrix with a power law distribution of non-zeros per col.
// Specifically col i has order n/(i+1) non-zeros in expectation.
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> LargeSparseMatrix(
const int64_t size) {
// Deterministic RNG.
std::mt19937 rand(48709241);
std::vector<Eigen::Triplet<double, int64_t>> triplets;
for (int64_t col = 0; col < size; ++col) {
int64_t row = -1;
while (row < size) {
row += absl::Uniform(rand, 1, col + 2);
if (row < size) {
double value = absl::Uniform(rand, 1, 10);
triplets.emplace_back(row, col, value);
}
}
}
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> mat(size, size);
mat.setFromTriplets(triplets.begin(), triplets.end());
return mat;
}
// Verify that the given sharder is consistent and has shards of reasonable
// mass. Requires target_num_shards > 0 and !element_masses.empty().
void VerifySharder(const Sharder& sharder, const int target_num_shards,
const std::vector<int64_t>& element_masses) {
int64_t num_elements = element_masses.size();
int num_shards = sharder.NumShards();
ASSERT_EQ(sharder.NumElements(), num_elements);
ASSERT_GE(num_elements, 1);
ASSERT_GE(num_shards, 1);
int64_t elements_so_far = 0;
for (int shard = 0; shard < num_shards; ++shard) {
int64_t shard_start = sharder.ShardStart(shard);
EXPECT_EQ(shard_start, elements_so_far) << " in shard: " << shard;
int64_t shard_mass = 0;
EXPECT_GE(sharder.ShardSize(shard), 1) << " in shard: " << shard;
EXPECT_GE(sharder.ShardMass(shard), 1) << " in shard: " << shard;
for (int64_t i = 0; i < sharder.ShardSize(shard); ++i) {
shard_mass += element_masses[shard_start + i];
}
EXPECT_EQ(shard_mass, sharder.ShardMass(shard)) << " in shard: " << shard;
elements_so_far += sharder.ShardSize(shard);
}
EXPECT_EQ(elements_so_far, num_elements);
EXPECT_LE(num_shards, 2 * target_num_shards);
ASSERT_GE(target_num_shards, 1);
const int64_t overall_mass =
std::accumulate(element_masses.begin(), element_masses.end(), int64_t{0});
const int64_t max_element_mass =
*std::max_element(element_masses.begin(), element_masses.end());
const int64_t upper_mass_limit = std::max(
max_element_mass,
MathUtil::CeilOfRatio(max_element_mass, int64_t{2}) +
MathUtil::CeilOfRatio(overall_mass, int64_t{target_num_shards}));
const int64_t lower_mass_limit =
overall_mass / target_num_shards -
MathUtil::CeilOfRatio(max_element_mass, int64_t{2});
for (int shard = 0; shard < sharder.NumShards(); ++shard) {
EXPECT_LE(sharder.ShardMass(shard), upper_mass_limit)
<< " in shard: " << shard;
if (shard + 1 < sharder.NumShards()) {
EXPECT_GE(sharder.ShardMass(shard), lower_mass_limit)
<< " in shard: " << shard;
}
}
}
TEST(SharderTest, SharderFromMatrix) {
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> mat =
TestSparseMatrix();
Sharder sharder(mat, /*num_shards=*/2, nullptr);
VerifySharder(sharder, 2, {4, 2, 2, 3});
}
TEST(SharderTest, UniformSharder) {
Sharder sharder(/*num_elements=*/10, /*num_shards=*/3, nullptr);
VerifySharder(sharder, 3, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1});
}
TEST(SharderTest, UniformSharderFromOtherSharder) {
Sharder other_sharder(/*num_elements=*/5, /*num_shards=*/3, nullptr);
Sharder sharder(other_sharder, /*num_elements=*/10);
VerifySharder(sharder, other_sharder.NumShards(),
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1});
}
TEST(SharderTest, UniformSharderExcessiveShards) {
Sharder sharder(/*num_elements=*/5, /*num_shards=*/7, nullptr);
EXPECT_THAT(sharder.ShardStartsForTesting(), ElementsAre(0, 1, 2, 3, 4, 5));
VerifySharder(sharder, 7, {1, 1, 1, 1, 1});
}
TEST(SharderTest, UniformSharderOneShard) {
Sharder sharder(/*num_elements=*/5, /*num_shards=*/1, nullptr);
EXPECT_THAT(sharder.ShardStartsForTesting(), ElementsAre(0, 5));
VerifySharder(sharder, 1, {1, 1, 1, 1, 1});
}
TEST(SharderTest, UniformSharderOneElementVector) {
Sharder sharder(/*num_elements=*/1, /*num_shards=*/5, nullptr);
EXPECT_THAT(sharder.ShardStartsForTesting(), ElementsAre(0, 1));
VerifySharder(sharder, 5, {1});
}
TEST(SharderTest, UniformSharderZeroElementVector) {
Sharder sharder(/*num_elements=*/0, /*num_shards=*/3, nullptr);
EXPECT_THAT(sharder.ShardStartsForTesting(), ElementsAre(0));
EXPECT_EQ(sharder.NumShards(), 0);
EXPECT_EQ(sharder.NumElements(), 0);
sharder.ParallelForEachShard([](const Shard& /*shard*/) {
LOG(FATAL) << "There are no shards so this shouldn't be called.";
});
}
TEST(SharderTest, UniformSharderFromOtherZeroElementSharder) {
Sharder empty_sharder(/*num_elements=*/0, /*num_shards=*/3, nullptr);
EXPECT_THAT(empty_sharder.ShardStartsForTesting(), ElementsAre(0));
EXPECT_EQ(empty_sharder.NumShards(), 0);
EXPECT_EQ(empty_sharder.NumElements(), 0);
Sharder sharder(empty_sharder, /*num_elements=*/5);
EXPECT_THAT(sharder.ShardStartsForTesting(), ElementsAre(0, 5));
VerifySharder(sharder, 1, {1, 1, 1, 1, 1});
}
TEST(ParallelSumOverShards, SmallExample) {
VectorXd vec(3);
vec << 1, 2, 3;
Sharder sharder(vec.size(), /*num_shards=*/2, nullptr);
const double sum = sharder.ParallelSumOverShards(
[&vec](const Shard& shard) { return shard(vec).sum(); });
EXPECT_EQ(sum, 6.0);
}
TEST(ParallelSumOverShards, SmallExampleUsingVectorBlock) {
VectorXd vec(3);
vec << 1, 2, 3;
auto vec_block = vec.segment(1, 2);
Sharder sharder(vec_block.size(), /*num_shards=*/2, nullptr);
const double sum = sharder.ParallelSumOverShards(
[&vec_block](const Shard& shard) { return shard(vec_block).sum(); });
EXPECT_EQ(sum, 5.0);
}
TEST(ParallelSumOverShards, SmallExampleUsingConstVectorBlock) {
VectorXd vec(3);
vec << 1, 2, 3;
const VectorXd& const_vec = vec;
auto vec_block = const_vec.segment(1, 2);
Sharder sharder(vec_block.size(), /*num_shards=*/2, nullptr);
const double sum = sharder.ParallelSumOverShards(
[&vec_block](const Shard& shard) { return shard(vec_block).sum(); });
EXPECT_EQ(sum, 5.0);
}
TEST(ParallelSumOverShards, SmallExampleUsingDiagonalMatrix) {
DiagonalMatrix<double, Eigen::Dynamic> diag{{1, 2, 3}};
Sharder sharder(diag.cols(), /*num_shards=*/2, nullptr);
const double sum = sharder.ParallelSumOverShards(
[&diag](const Shard& shard) { return shard(diag).diagonal().sum(); });
EXPECT_EQ(sum, 6.0);
}
TEST(ParallelSumOverShards, SmallExampleUsingDiagonalMatrixMultiplication) {
DiagonalMatrix<double, Eigen::Dynamic> diag{{1, 2, 3}};
VectorXd vec{{1, 1, 1}};
VectorXd answer(3);
Sharder sharder(diag.cols(), /*num_shards=*/2, nullptr);
sharder.ParallelForEachShard(
[&](const Shard& shard) { shard(answer) = shard(diag) * shard(vec); });
EXPECT_THAT(answer, ElementsAre(1.0, 2.0, 3.0));
}
TEST(ParallelTrueForAllShards, SmallTrueExample) {
VectorXd vec(3);
vec << 1, 2, 3;
Sharder sharder(vec.size(), /*num_shards=*/2, nullptr);
const bool result = sharder.ParallelTrueForAllShards(
[&vec](const Shard& shard) { return (shard(vec).array() > 0.0).all(); });
EXPECT_TRUE(result);
}
TEST(ParallelTrueForAllShards, SmallFalseExample) {
VectorXd vec(3);
vec << 1, 2, 3;
Sharder sharder(vec.size(), /*num_shards=*/2, nullptr);
const bool result = sharder.ParallelTrueForAllShards(
[&vec](const Shard& shard) { return (shard(vec).array() < 2.5).all(); });
EXPECT_FALSE(result);
}
TEST(MatrixVectorProductTest, SmallExample) {
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> mat =
TestSparseMatrix();
Sharder sharder(mat, /*num_shards=*/3, nullptr);
VectorXd vec(3);
vec << 1, 2, 3;
VectorXd ans = TransposedMatrixVectorProduct(mat, vec, sharder);
EXPECT_THAT(ans, ElementsAre(6.0, -0.5, 6.0, 19));
}
TEST(AddScaledVectorTest, SmallExample) {
Sharder sharder(3, /*num_shards=*/2, nullptr);
VectorXd vec1(3), vec2(3);
vec1 << 4, 5, 20;
vec2 << 1, 7, 3;
AddScaledVector(2.0, vec2, sharder, /*dest=*/vec1);
EXPECT_THAT(vec1, ElementsAre(6, 19, 26));
}
TEST(AssignVectorTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec1, vec2(3);
vec2 << 1, 7, 3;
AssignVector(vec2, sharder, /*dest=*/vec1);
EXPECT_THAT(vec1, ElementsAre(1, 7, 3));
}
TEST(CloneVectorTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec(3);
vec << 1, 7, 3;
EXPECT_THAT(CloneVector(vec, sharder), ElementsAre(1, 7, 3));
}
TEST(CoefficientWiseProductInPlaceTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec1(3), vec2(3);
vec1 << 4, 5, 20;
vec2 << 1, 2, 3;
CoefficientWiseProductInPlace(/*scale=*/vec2, sharder,
/*dest=*/vec1);
EXPECT_THAT(vec1, ElementsAre(4, 10, 60));
}
TEST(CoefficientWiseQuotientInPlaceTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec1(3), vec2(3);
vec1 << 4, 6, 20;
vec2 << 1, 2, 5;
CoefficientWiseQuotientInPlace(/*scale=*/vec2, sharder,
/*dest=*/vec1);
EXPECT_THAT(vec1, ElementsAre(4, 3, 4));
}
TEST(DotTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec1(3), vec2(3);
vec1 << 1, 2, 3;
vec2 << 4, 5, 6;
double ans = Dot(vec1, vec2, sharder);
EXPECT_THAT(ans, DoubleNear(4 + 10 + 18, 1.0e-13));
}
TEST(LInfNormTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec(3);
vec << -1, 2, -3;
double ans = LInfNorm(vec, sharder);
EXPECT_EQ(ans, 3);
}
TEST(LInfNormTest, EmptyExample) {
Sharder sharder(/*num_elements=*/0, /*num_shards=*/2, nullptr);
VectorXd vec(0);
double ans = LInfNorm(vec, sharder);
EXPECT_EQ(ans, 0);
}
TEST(L1NormTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec(3);
vec << -1, 2, -3;
double ans = L1Norm(vec, sharder);
EXPECT_EQ(ans, 6);
}
TEST(L1NormTest, EmptyExample) {
Sharder sharder(/*num_elements=*/0, /*num_shards=*/2, nullptr);
VectorXd vec(0);
double ans = L1Norm(vec, sharder);
EXPECT_EQ(ans, 0);
}
TEST(SquaredNormTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec(3);
vec << 1, 2, 3;
double ans = SquaredNorm(vec, sharder);
EXPECT_THAT(ans, DoubleNear(1 + 4 + 9, 1.0e-13));
}
TEST(NormTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec(3);
vec << 1, 2, 3;
double ans = Norm(vec, sharder);
EXPECT_THAT(ans, DoubleNear(std::sqrt(1 + 4 + 9), 1.0e-13));
}
TEST(SquaredDistanceTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec1(3);
VectorXd vec2(3);
vec1 << 1, 1, 1;
vec2 << 1, 2, 3;
double ans = SquaredDistance(vec1, vec2, sharder);
EXPECT_THAT(ans, DoubleNear(5, 1.0e-13));
}
TEST(DistanceTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec1(3);
VectorXd vec2(3);
vec1 << 1, 1, 1;
vec2 << 1, 2, 3;
double ans = Distance(vec1, vec2, sharder);
EXPECT_THAT(ans, DoubleNear(std::sqrt(5), 1.0e-13));
}
TEST(ScaledLInfNormTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec(3);
VectorXd scale(3);
vec << -1, 2, -3;
scale << 4, 6, 1;
double ans = ScaledLInfNorm(vec, scale, sharder);
EXPECT_EQ(ans, 12);
}
TEST(ScaledSquaredNormTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec(3);
VectorXd scale(3);
vec << -1, 2, -3;
scale << 4, 6, 1;
double ans = ScaledSquaredNorm(vec, scale, sharder);
EXPECT_EQ(ans, 169);
}
TEST(ScaledNormTest, SmallExample) {
Sharder sharder(/*num_elements=*/3, /*num_shards=*/2, nullptr);
VectorXd vec(3);
VectorXd scale(3);
vec << -1, 2, -3;
scale << 4, 6, 1;
double ans = ScaledNorm(vec, scale, sharder);
EXPECT_EQ(ans, std::sqrt(169));
}
TEST(ScaledColLInfNorm, SmallExample) {
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> mat =
TestSparseMatrix();
Sharder sharder(mat, /*num_shards=*/3, nullptr);
VectorXd row_scaling_vec(3);
VectorXd col_scaling_vec(4);
row_scaling_vec << 1, -2, 1;
col_scaling_vec << 1, 2, -1, -1;
VectorXd answer =
ScaledColLInfNorm(mat, row_scaling_vec, col_scaling_vec, sharder);
EXPECT_THAT(answer, ElementsAre(7, 1, 6, 5));
}
TEST(ScaledColL2Norm, SmallExample) {
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> mat =
TestSparseMatrix();
Sharder sharder(mat, /*num_shards=*/3, nullptr);
VectorXd row_scaling_vec(3);
VectorXd col_scaling_vec(4);
row_scaling_vec << 1, -2, 1;
col_scaling_vec << 1, 2, -1, -1;
VectorXd answer =
ScaledColL2Norm(mat, row_scaling_vec, col_scaling_vec, sharder);
EXPECT_THAT(answer, ElementsAre(std::sqrt(54), 1.0, 6.0, std::sqrt(41)));
}
TEST(IsDiagonal, DiagonalSquareMatrix) {
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> mat(4, 4);
std::vector<Eigen::Triplet<double, int64_t>> matrix_triplets = {
{0, 0, 1.0}, {1, 1, 2.5}, {3, 3, -3}};
mat.setFromTriplets(matrix_triplets.begin(), matrix_triplets.end());
Sharder sharder(mat, /*num_shards=*/3, nullptr);
EXPECT_TRUE(IsDiagonal(mat, sharder));
}
TEST(IsDiagonal, DiagonalRectangularMatrix) {
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> mat(3, 5);
std::vector<Eigen::Triplet<double, int64_t>> matrix_triplets = {
{0, 0, 2}, {1, 1, -1}, {2, 2, 3}};
mat.setFromTriplets(matrix_triplets.begin(), matrix_triplets.end());
Sharder sharder(mat, /*num_shards=*/3, nullptr);
EXPECT_TRUE(IsDiagonal(mat, sharder));
}
TEST(IsDiagonal, NonDiagonalSquareMatrix) {
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> mat(3, 3);
std::vector<Eigen::Triplet<double, int64_t>> matrix_triplets = {
{0, 0, 2}, {0, 1, -1}, {1, 0, -1}, {2, 2, 1}};
mat.setFromTriplets(matrix_triplets.begin(), matrix_triplets.end());
Sharder sharder(mat, /*num_shards=*/3, nullptr);
EXPECT_FALSE(IsDiagonal(mat, sharder));
}
class VariousSizesTest : public testing::TestWithParam<int64_t> {};
TEST_P(VariousSizesTest, LargeMatVec) {
const int64_t size = GetParam();
Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> mat =
LargeSparseMatrix(size);
const int num_threads = 5;
const int shards_per_thread = 3;
ThreadPool pool("MatrixVectorProductTest", num_threads);
pool.StartWorkers();
Sharder sharder(mat, shards_per_thread * num_threads, &pool);
VectorXd rhs = VectorXd::Random(size);
VectorXd direct = mat.transpose() * rhs;
VectorXd threaded = TransposedMatrixVectorProduct(mat, rhs, sharder);
EXPECT_LE((direct - threaded).norm(), 1.0e-8);
}
TEST_P(VariousSizesTest, LargeVectors) {
const int64_t size = GetParam();
const int num_threads = 5;
ThreadPool pool("SquaredNormTest", num_threads);
pool.StartWorkers();
Sharder sharder(size, num_threads, &pool);
VectorXd vec = VectorXd::Random(size);
const double direct = vec.squaredNorm();
const double threaded = SquaredNorm(vec, sharder);
EXPECT_THAT(threaded, DoubleNear(direct, size * 1.0e-14));
}
INSTANTIATE_TEST_SUITE_P(VariousSizesTestInstantiation, VariousSizesTest,
testing::Values(10, 1000, 100 * 1000));
} // namespace
} // namespace operations_research::pdlp

View File

@@ -0,0 +1,377 @@
// Copyright 2010-2021 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.
// These proto messages are for collecting solve statistics, e.g., during
// experiments.
syntax = "proto2";
package operations_research.pdlp;
import "ortools/pdlp/solvers.proto";
// Easy-to-compute statistics for the quadratic program.
message QuadraticProgramStats {
optional int64 num_variables = 1;
optional int64 num_constraints = 2;
// Minimum row and column infinity norms of the constraint matrix. If the
// constraint matrix is empty, the values returned are 0.0.
optional double constraint_matrix_col_min_l_inf_norm = 3;
optional double constraint_matrix_row_min_l_inf_norm = 4;
optional int64 constraint_matrix_num_nonzeros = 5;
// Max/min/mean of absolute values of nonzero elements in constraint matrix.
// Note that the maximum absolute value is also equal to the maximal row and
// column infinity norms of the constraint matrix. If the constraint matrix
// is empty, the values returned are 0 for the maximum and minimum, and
// NaN for the average.
optional double constraint_matrix_abs_max = 6;
optional double constraint_matrix_abs_min = 7;
optional double constraint_matrix_abs_avg = 8;
// Statistics of the combined vector of the constraint lower and upper bounds.
// Given parallel lower and upper bounds vectors, the "combined bounds" vector
// takes the maximum absolute value of each pair of bounds, ignoring all non-
// finite values. The comment in solvers.proto:TerminationCriteria provides an
// example of the combined bounds vector.
// If there are no constraints, the values returned are 0 for the
// maximum, minimum, and l2 norm and NaN for the average.
optional double combined_bounds_max = 9;
optional double combined_bounds_min = 10;
optional double combined_bounds_avg = 11;
optional double combined_bounds_l2_norm = 24;
// Number of finite variable bound gaps, which are the elementwise difference
// between the upper and lower bounds on primal feasible solutions.
optional int64 variable_bound_gaps_num_finite = 12;
// Max/min/mean over all finite variable bound gaps. When there are no finite
// gaps, the values returned are 0 for the maximum and minimum and NaN for the
// average.
optional double variable_bound_gaps_max = 13;
optional double variable_bound_gaps_min = 14;
optional double variable_bound_gaps_avg = 15;
// Statistics of the objective vector.
optional double objective_vector_abs_max = 16;
optional double objective_vector_abs_min = 17;
optional double objective_vector_abs_avg = 18;
optional double objective_vector_l2_norm = 23;
optional int64 objective_matrix_num_nonzeros = 19;
// Max/min/mean of absolute values of nonzero elements of the objective
// matrix. If the objective matrix is empty, the returned values are 0.0, 0.0
// and NaN respectively.
optional double objective_matrix_abs_max = 20;
optional double objective_matrix_abs_min = 21;
optional double objective_matrix_abs_avg = 22;
}
// Specifies whether a restart was performed on a given iteration.
enum RestartChoice {
RESTART_CHOICE_UNSPECIFIED = 0;
// No restart on this iteration.
RESTART_CHOICE_NO_RESTART = 1;
// The weighted average of iterates is cleared and reset to the current point.
// Note that from a mathematical perspective this can be equivalently viewed
// as restarting the algorithm but picking the restart point to be the current
// iterate.
RESTART_CHOICE_WEIGHTED_AVERAGE_RESET = 2;
// The algorithm is restarted at the average of iterates since the last
// restart.
RESTART_CHOICE_RESTART_TO_AVERAGE = 3;
}
// Identifies the type of point used to compute the fields in a given proto; see
// ConvergenceInformation and InfeasibilityInformation.
enum PointType {
POINT_TYPE_UNSPECIFIED = 0;
// Current iterate (x_k, y_k).
POINT_TYPE_CURRENT_ITERATE = 1;
// Difference of iterates (x_{k+1} - x_k, y_{k+1} - y_k).
POINT_TYPE_ITERATE_DIFFERENCE = 2;
// Average of iterates since the last restart.
POINT_TYPE_AVERAGE_ITERATE = 3;
// There is no corresponding point.
POINT_TYPE_NONE = 4;
// Output of presolver.
POINT_TYPE_PRESOLVER_SOLUTION = 5;
}
// Information measuring how close a candidate is to establishing feasibility
// and optimality; see also TerminationCriteria.
message ConvergenceInformation {
// Type of the candidate point described by this ConvergenceInformation.
optional PointType candidate_type = 1;
// The primal objective. The primal need not be feasible.
optional double primal_objective = 2;
// The dual objective. The dual need not be feasible. The dual objective
// includes the contributions from reduced costs.
optional double dual_objective = 3;
// If possible (e.g., when all primal variables have lower and upper bounds),
// a correct dual bound. The value is negative infinity if no corrected dual
// bound is available.
optional double corrected_dual_objective = 4;
// The maximum violation of any primal constraint, i.e., the l_∞ norm of the
// violations.
optional double l_inf_primal_residual = 5;
// The l_2 norm of the violations of primal constraints.
optional double l2_primal_residual = 6;
// The maximum violation of any dual constraint, i.e., the l_∞ norm of the
// violations.
optional double l_inf_dual_residual = 7;
// The l_2 norm of the violations of dual constraints.
optional double l2_dual_residual = 8;
// The maximum absolute value of the primal variables, i.e., the l_∞ norm.
// This is useful to detect when the primal iterates are diverging. Divergence
// of the primal variables could be an algorithmic issue, or indicate that the
// dual is infeasible.
optional double l_inf_primal_variable = 14;
// The l_2 norm of the primal variables.
optional double l2_primal_variable = 15;
// The maximum absolute value of the dual variables, i.e., the l_∞ norm. This
// is useful to detect when the dual iterates are diverging. Divergence of the
// dual variables could be an algorithmic issue, or indicate the primal is
// infeasible.
optional double l_inf_dual_variable = 16;
// The l_2 norm of the dual variables.
optional double l2_dual_variable = 17;
reserved 9, 10, 11, 12, 13, 18, 19, 20, 21, 22, 23;
}
// Information measuring how close a point is to establishing primal or dual
// infeasibility (i.e. has no solution); see also TerminationCriteria.
message InfeasibilityInformation {
// Let x_ray be the algorithm's estimate of the primal extreme ray where x_ray
// is a vector scaled such that its infinity norm is one. A simple and typical
// choice of x_ray is x_ray = x / | x |_∞ where x is the current primal
// iterate. For this value compute the maximum absolute error in the primal
// linear program with the right hand side and finite variable bounds set to
// zero. This error refers to both the linear constraints and sign constraints
// on the ray.
optional double max_primal_ray_infeasibility = 1;
// The value of the linear part of the primal objective (ignoring additive
// constants) evaluated at x_ray, i.e., c' * x_ray where c is the objective
// coefficient vector.
optional double primal_ray_linear_objective = 2;
// The l_∞ norm of the vector resulting from taking the quadratic matrix from
// primal objective and multiplying it by the primal variables. For linear
// programming problems this is zero.
optional double primal_ray_quadratic_norm = 3;
// Let (y_ray, r_ray) be the algorithm's estimate of the dual and reduced cost
// extreme ray where (y_ray, r_ray) is a vector scaled such that its infinity
// norm is one. A simple and typical choice of y_ray is
// (y_ray, r_ray) = (y, r) / max(| y |_∞, | r |_∞) where y is the
// current dual iterate and r is the current dual reduced costs.
// Consider the quadratic program we are solving but with the objective (both
// quadratic and linear terms) set to zero. This forms a linear program
// (label this linear program (1)) with no objective. Take the dual of (1) and
// compute the maximum absolute value of the constraint error for
// (y_ray, r_ray) to obtain the value of max_dual_ray_infeasibility.
optional double max_dual_ray_infeasibility = 4;
// The objective of the linear program labeled (1) in the previous paragraph.
optional double dual_ray_objective = 5;
// Type of the point used to compute the InfeasibilityInformation.
optional PointType candidate_type = 6;
reserved 7, 8;
}
message PointMetadata {
// Type of the point that this metadata corresponds to.
optional PointType point_type = 1;
// Projections of the primal solution onto random planes.
repeated double random_primal_projections = 2 [packed = true];
// Projections of the dual solution onto random planes.
repeated double random_dual_projections = 3 [packed = true];
// The number of primal variables that are not at their bounds.
optional int64 active_primal_variable_count = 4;
// The number of dual variables that are not at their bounds.
optional int64 active_dual_variable_count = 5;
// The number of primal variables that have a different bound status than they
// did at the last restart.
optional int64 active_primal_variable_change = 6;
// The number of dual variables that have a different bound status than they
// did at the last restart.
optional int64 active_dual_variable_change = 7;
}
// All values in IterationStats assume that the primal quadratic program is a
// minimization problem and the dual is a maximization problem. Problems should
// be transformed to this form if they are not already in this form. The dual
// vector is defined to be the vector of multipliers on the linear constraints,
// that is, excluding dual multipliers on variable bounds (reduced costs).
message IterationStats {
// The iteration number at which these stats were recorded. By convention,
// iteration counts start at 1, and the stats correspond to the solution
// *after* the iteration. Therefore stats from iteration 0 are the stats at
// the starting point.
optional int32 iteration_number = 1;
// A set of statistics measuring how close a point is to establishing primal
// and dual feasibility and optimality. This field is repeated since there
// might be several different points that are considered.
repeated ConvergenceInformation convergence_information = 2;
// A set of statistics measuring how close a point is to establishing primal
// or dual infeasibility (i.e., has no solution). This field is repeated since
// there might be several different points that could establish infeasibility.
repeated InfeasibilityInformation infeasibility_information = 3;
// Auxiliary statistics for each type of point.
repeated PointMetadata point_metadata = 11;
// The cumulative number of passes through the KKT matrix since the start of
// the solve. One pass is a multply by the constraint matrix, its transpose
// and the matrix that defines the quadratic part of the objective.
//
// For example, each iteration of mirror saddle prox contributes 2.0 to this
// sum. This is a float because it can include fractional passes through the
// data. For example, in an active set method we may only use a submatrix with
// 20% of the nonzeros of the KKT matrix at each iteration in which case 0.2
// would be added to the total.
optional double cumulative_kkt_matrix_passes = 4;
// The total number of rejected steps (e.g., within a line search procedure)
// since the start of the solve.
optional int32 cumulative_rejected_steps = 5;
// The amount of time passed since we started solving the problem (see solver
// log solve_time_sec) which records total time.
optional double cumulative_time_sec = 6;
// The kind of restart that occurred at this iteration, or NO_RESTART if a
// restart did not occur.
optional RestartChoice restart_used = 7;
// Step size used at this iteration. Note that the step size used for the
// primal update is step_size / primal_weight, while the one used for the dual
// update is step_size * primal_weight.
optional double step_size = 8;
// Primal weight controlling the relation between primal and dual step sizes.
// See field 'step_size' for a detailed description.
optional double primal_weight = 9;
map<string, double> method_specific_stats = 10;
}
enum TerminationReason {
TERMINATION_REASON_UNSPECIFIED = 0;
TERMINATION_REASON_OPTIMAL = 1;
// Note in this situation the dual could be either unbounded or infeasible.
TERMINATION_REASON_PRIMAL_INFEASIBLE = 2;
// Note in this situation the primal could be either unbounded or infeasible.
TERMINATION_REASON_DUAL_INFEASIBLE = 3;
TERMINATION_REASON_TIME_LIMIT = 4;
TERMINATION_REASON_ITERATION_LIMIT = 5;
TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT = 8;
TERMINATION_REASON_NUMERICAL_ERROR = 6;
// Indicates that the solver detected invalid problem data, e.g., inconsistent
// bounds.
TERMINATION_REASON_INVALID_PROBLEM = 9;
// Indicates that an invalid value for the parameters was detected.
TERMINATION_REASON_INVALID_PARAMETER = 10;
TERMINATION_REASON_OTHER = 7;
// Primal or dual infeasibility was detected (e.g. by presolve) but no
// certificate is available.
TERMINATION_REASON_PRIMAL_OR_DUAL_INFEASIBLE = 11;
}
message SolveLog {
// The name of the optimization problem.
optional string instance_name = 1;
// If solved with PDLP, the parameters for this solve.
optional PrimalDualHybridGradientParams params = 14;
// The reason that the solve terminated.
optional TerminationReason termination_reason = 3;
// Optional extra information about the termination reason.
optional string termination_string = 4;
// The total number of iterations during the solve.
optional int32 iteration_count = 5;
// Time for preprocessing (everything before iteration 0). This is also
// included in solve_time_sec.
optional double preprocessing_time_sec = 13;
// The runtime of the solve. Note: This should not be used for comparing
// methods unless care is taken to control for noise in runtime measurement.
optional double solve_time_sec = 6;
// The IterationStats for the final iteration of the solver.
// NOTE: Regardless of preprocessing (i.e. scaling or presolve) the optimality
// or infeasibility information is evaluated with respect to the original
// problem.
optional IterationStats solution_stats = 8;
// The type of the output point that the solver returned. The quality of the
// point is reported in the corresponding entry of
// solution_stats.convergence_information and/or
// solution_stats.infeasibility_information. If termination_reason is
// TERMINATION_REASON_OPTIMAL, it's guaranteed that the corresponding entry of
// solution_stats.convergence_information satisfies the optimality conditions.
// Similarly, if termination_reason is either
// TERMINATION_REASON_PRIMAL_INFEASIBLE or TERMINATION_REASON_DUAL_INFEASIBLE
// the corresponding entry of solution_stats.infeasibility_information
// satisifes conditions for declaring primal or dual infeasibility,
// respectively.
// If termination_reason is anything else, e.g. TERMINATION_REASON_TIME_LIMIT
// or TERMINATION_REASON_PRIMAL_OR_DUAL_INFEASIBLE, the solution may not
// satisfy the optimality or infeasibility conditions.
optional PointType solution_type = 10;
// A history of iteration stats for the solve. The iteration_number fields
// should be in increasing order. The frequency at which these stats should be
// recorded is not specified. This field is "more" optional than the others
// because it often significantly increases the size of the message, and
// because the information may not be available for third-party solvers.
repeated IterationStats iteration_stats = 7;
// Statistics of the original problem.
optional QuadraticProgramStats original_problem_stats = 11;
// Statistics of the problem after preprocessing.
optional QuadraticProgramStats preprocessed_problem_stats = 12;
reserved 2, 9;
}

319
ortools/pdlp/solvers.proto Normal file
View File

@@ -0,0 +1,319 @@
// Copyright 2010-2021 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.
syntax = "proto2";
package operations_research.pdlp;
import "ortools/glop/parameters.proto";
enum OptimalityNorm {
OPTIMALITY_NORM_UNSPECIFIED = 0;
OPTIMALITY_NORM_L_INF = 1; // The infinity norm.
OPTIMALITY_NORM_L2 = 2; // The Euclidean norm.
}
// A description of solver termination criteria. The criteria are defined in
// terms of the quantities recorded in IterationStats in solve_log.proto.
// Relevant readings on infeasibility certificates:
// (1) https://docs.mosek.com/modeling-cookbook/qcqo.html provides references
// explaining why the primal rays imply dual infeasibility and dual rays imply
// primal infeasibility.
// (2) The termination criteria for Mosek's linear programming optimizer
// https://docs.mosek.com/9.0/pythonfusion/solving-linear.html.
// (3) The termination criteria for OSQP is in section 3.3 of
// https://web.stanford.edu/~boyd/papers/pdf/osqp.pdf.
// (4) The termination criteria for SCS is in section 3.5 of
// https://arxiv.org/pdf/1312.3039.pdf.
message TerminationCriteria {
// The norm that we are measuring the optimality criteria in.
optional OptimalityNorm optimality_norm = 1 [default = OPTIMALITY_NORM_L2];
// Define CombineBounds(l,u) as a function that takes two equal-length vectors
// and returns a vector whose elements are max{|l_i|, |u_i|} where non-finite
// values of l_i and u_i are replaced with zero.
// For example, CombineBounds([-2, -Inf, 5], [1, 10, 5]) == [2, 10, 5].
// Recalling the definitions from
// https://developers.google.com/optimization/lp/pdlp_math, c is the linear
// objective vector, l^c are the constraint lower bounds, and u^c are the
// constraint upper bounds. Define b^c = CombineBounds(l^c, u^c).
// Let p correspond to the norm we are using as specified by optimality_norm.
// If the algorithm terminates with termination_reason =
// TERMINATION_REASON_OPTIMAL then
//
// | primal_objective - dual_objective | <= eps_optimal_absolute +
// eps_optimal_relative * ( | primal_objective | + | dual_objective | )
// norm(primal_residual, p) <= eps_optimal_absolute + eps_optimal_relative *
// norm(b, p)
// norm(dual_residual, p) <= eps_optimal_absolute + eps_optimal_relative *
// norm(c, p)
// It is possible to prove that a solution satisfying the above conditions
// also satisfies SCS's optimality conditions (see link above) with ϵ_pri =
// ϵ_dual = ϵ_gap = eps_optimal_absolute = eps_optimal_relative. (ϵ_pri,
// ϵ_dual, and ϵ_gap are SCS's parameters).
// Absolute tolerance on the duality gap, primal feasibility, and dual
// feasibility.
optional double eps_optimal_absolute = 2 [default = 1.0e-6];
// Relative tolerance on the duality gap, primal feasibility, and dual
// feasibility.
optional double eps_optimal_relative = 3 [default = 1.0e-6];
// If the following two conditions hold we say that we have obtained an
// approximate dual ray, which is an approximate certificate of primal
// infeasibility.
// (1) dual_ray_objective > 0,
// (2) max_dual_ray_infeasibility / dual_ray_objective <=
// eps_primal_infeasible.
optional double eps_primal_infeasible = 4 [default = 1.0e-8];
// If the following three conditions hold we say we have obtained an
// approximate primal ray, which is an approximate certificate of dual
// infeasibility.
// (1) primal_ray_linear_objective < 0,
// (2) max_primal_ray_infeasibility / (-primal_ray_linear_objective) <=
// eps_dual_infeasible
// (3) primal_ray_quadratic_norm / (-primal_ray_linear_objective) <=
// eps_dual_infeasible.
optional double eps_dual_infeasible = 5 [default = 1.0e-8];
// If termination_reason = TERMINATION_REASON_TIME_LIMIT then the solver has
// taken at least time_sec_limit time.
optional double time_sec_limit = 6 [default = inf];
// If termination_reason = TERMINATION_REASON_ITERATION_LIMIT then the solver
// has taken at least iterations_limit iterations.
optional int32 iteration_limit = 7 [default = 2147483647];
// If termination_reason = TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT then
// cumulative_kkt_matrix_passes is at least kkt_pass_limit.
optional double kkt_matrix_pass_limit = 8 [default = inf];
}
message AdaptiveLinesearchParams {
// At the end of each iteration, regardless of whether the step was accepted
// or not, the adaptive rule updates the step_size as the minimum of two
// potential step sizes defined by the following two exponents.
// The step size reduction exponent defines a step size given by
// (1 - (total_steps_attempted + 1)^(-step_size_reduction_exponent)) *
// step_size_limit where step_size_limit is the maximum allowed step size at
// the current iteration.
optional double step_size_reduction_exponent = 1 [default = 0.3];
// The step size growth exponent defines a step size given by (1 +
// (total_steps_attempted + 1)^(-step_size_growth_exponent)) * step_size_.
optional double step_size_growth_exponent = 2 [default = 0.6];
}
message MalitskyPockParams {
// At every inner iteration the algorithm can decide to accept the step size
// or to update it to step_size = step_size_downscaling_factor * step_size.
// This parameter should lie between 0 and 1. The default is the value used in
// Malitsky and Pock (2016).
optional double step_size_downscaling_factor = 1 [default = 0.7];
// Contraction factor used in the linesearch condition of Malitsky and Pock.
// A step size is accepted if primal_weight * primal_stepsize *
// norm(constraint_matrix' * (next_dual - current_dual)) is less
// than linearsearch_contraction_factor * norm(next_dual - current_dual).
// The default is the value used in Malitsky and Pock (2016).
optional double linesearch_contraction_factor = 2 [default = 0.99];
// Malitsky and Pock linesearch rule permits an arbitrary choice of the first
// step size guess within an interval [m, M]. This parameter determines where
// in that interval to pick the step size. In particular, the next stepsize is
// given by m + step_size_interpolation*(M - m). The default is the value used
// in Malitsky and Pock (2016).
optional double step_size_interpolation = 3 [default = 1];
}
// Parameters for PrimalDualHybridGradient() in primal_dual_hybrid_gradient.h
message PrimalDualHybridGradientParams {
enum RestartStrategy {
RESTART_STRATEGY_UNSPECIFIED = 0;
// No restarts are performed. The average solution is cleared every major
// iteration, but the current solution is not changed.
NO_RESTARTS = 1;
// On every major iteration, the current solution is reset to the average
// since the last major iteration.
EVERY_MAJOR_ITERATION = 2;
// A heuristic that adaptively decides on every major iteration whether to
// restart (this is forced approximately on increasing powers-of-two
// iterations), and if so to the current or to the average, based on
// reduction in a potential function. The rule more or less follows the
// description of the adaptive restart scheme in
// https://arxiv.org/pdf/2106.04756.pdf.
ADAPTIVE_HEURISTIC = 3;
// A distance-based restarting scheme that restarts the algorithm whenever
// an appropriate potential function is reduced sufficiently. This check
// happens at every major iteration.
// TODO(user): Cite paper for the restart strategy and definition of the
// potential function, when available.
ADAPTIVE_DISTANCE_BASED = 4;
}
enum LinesearchRule {
LINESEARCH_RULE_UNSPECIFIED = 0;
// Applies the heuristic rule presented in Section 3.1 of
// https://arxiv.org/pdf/2106.04756.pdf (further generalized to QP). There
// is not a proof of convergence for it. It is usually the fastest in
// practice but sometimes behaves poorly.
ADAPTIVE_LINESEARCH_RULE = 1;
// Applies Malitsky & Pock linesearch rule. This guarantees an
// ergodic O(1/N) convergence rate https://arxiv.org/pdf/1608.08883.pdf.
// This is provably convergent, but only supports linear problems and
// quadratic problems with a diagonal objective matrix.
MALITSKY_POCK_LINESEARCH_RULE = 2;
// Uses a constant step size corresponding to an estimate of the maximum
// singular value of the constraint matrix.
CONSTANT_STEP_SIZE_RULE = 3;
}
optional TerminationCriteria termination_criteria = 1;
// The number of threads to use. Must be positive.
optional int32 num_threads = 2 [default = 1];
// For more efficient parallel computation, the matrices and vectors are
// divided (virtually) into num_shards shards. Results are computed
// independently for each shard and then combined. As a consequence, the order
// of computation, and hence floating point roundoff, depends on the number of
// shards so reproducible results require using the same value for num_shards.
// However, for efficiency num_shards should a be at least num_threads, and
// preferably at least 4*num_threads to allow better load balancing. If
// num_shards is positive, the computation will use that many shards.
// Otherwise a default that depends on num_threads will be used.
optional int32 num_shards = 27 [default = 0];
// If true, the iteration_stats field of the SolveLog output will be populated
// at every iteration. Note that we only compute solution statistics at
// termination checks. Setting this parameter to true may substantially
// increase the size of the output.
optional bool record_iteration_stats = 3;
// The frequency at which extra work is performed to make major algorithmic
// decisions, e.g., performing restarts and updating the primal weight. Major
// iterations also trigger a termination check. For best performance using
// the NO_RESTARTS or EVERY_MAJOR_ITERATION rule, one should perform a
// log-scale grid search over this parameter. ADAPTIVE_HEURISTIC is mostly
// insensitive to this value.
optional int32 major_iteration_frequency = 4 [default = 64];
// The frequency (based on a counter reset every major iteration) to check for
// termination (involves extra work) and log iteration stats. Termination
// checks do not affect algorithmic progress unless termination is triggered.
optional int32 termination_check_frequency = 5 [default = 64];
optional RestartStrategy restart_strategy = 6 [default = ADAPTIVE_HEURISTIC];
// This parameter controls exponential smoothing of log(primal_weight) when a
// primal weight update occurs (i.e., when the ratio of primal and dual step
// sizes is adjusted). At 0.0, the primal weight will be frozen at its initial
// value and there will be no dynamic updates in the algorithm. At 1.0, there
// is no smoothing in the updates.
optional double primal_weight_update_smoothing = 7 [default = 0.5];
// The initial value of the primal weight (i.e., the ratio of primal and dual
// step sizes). The primal weight remains fixed throughout the solve if
// primal_weight_update_smoothing = 0.0. If unset, the default is the ratio of
// the norm of the objective vector to the L2 norm of the combined constraint
// bounds vector (as defined above). If this ratio is not finite and positive,
// then the default is 1.0 instead.
optional double initial_primal_weight = 8;
message PresolveOptions {
// If true runs glop's presolver on the given instance prior to solving.
// Note that convergence criteria are interpreted with respect to the
// presolved problem. Certificates may not be available if presolve detects
// infeasibility.
optional bool use_glop = 1;
// Parameters to control glop's presolver. Only used when use_glop is true.
// These are merged with and override Bisco's defaults.
optional operations_research.glop.GlopParameters glop_parameters = 2;
}
optional PresolveOptions presolve_options = 16;
// Number of L_infinity Ruiz rescaling iterations to apply to the constraint
// matrix. Zero disables this rescaling pass.
optional int32 l_inf_ruiz_iterations = 9 [default = 5];
// If true, applies L_2 norm rescaling after the Ruiz rescaling. Heuristically
// this has been found to help convergence.
optional bool l2_norm_rescaling = 10 [default = true];
// For ADAPTIVE_HEURISTIC and ADAPTIVE_DISTANCE_BASED only: A relative
// reduction in the potential function by this amount always triggers a
// restart. Must be between 0.0 and 1.0.
optional double sufficient_reduction_for_restart = 11 [default = 0.1];
// For ADAPTIVE_HEURISTIC only: A relative reduction in the potential function
// by this amount triggers a restart if, additionally, the quality of the
// iterates appears to be getting worse. The value must be in the interval
// [sufficient_reduction_for_restart, 1). Smaller values make restarts less
// frequent, and larger values make them more frequent.
optional double necessary_reduction_for_restart = 17 [default = 0.9];
// Linesearch rule applied at each major iteration.
optional LinesearchRule linesearch_rule = 12
[default = ADAPTIVE_LINESEARCH_RULE];
optional AdaptiveLinesearchParams adaptive_linesearch_parameters = 18;
optional MalitskyPockParams malitsky_pock_parameters = 19;
// Scaling factor applied to the initial step size (all step sizes if
// linesearch_rule == CONSTANT_STEP_SIZE_RULE).
optional double initial_step_size_scaling = 25 [default = 1.0];
reserved 13, 14, 15;
// Seeds for generating (pseudo-)random projections of iterates during
// termination checks. For each seed, the projection of the primal and dual
// solutions onto random planes in primal and dual space will be computed and
// added the IterationStats if record_iteration_stats is true. The random
// planes generated will be determined by the seeds, the primal and dual
// dimensions, and num_threads.
repeated double random_projection_seeds = 20 [packed = true];
// When computing relative feasibility norms, constraint bounds with absolute
// value at least this threshold are treated as infinite, and hence not
// included in the relative feasibility norm.
// NOTE: This affects only the relative convergence criteria (and problem
// statistics LOG()ed at the start of the run). A smaller value makes the
// relative convergence criteria stronger.
optional double infinite_constraint_bound_threshold = 22 [default = inf];
// When solving QPs with diagonal objective matrices, this option can be
// turned on to enable an experimental solver that avoids linearization of the
// quadratic term. The `diagonal_qp_solver_accuracy` parameter controls the
// solve accuracy.
// TODO(user): Turn this option on by default for quadratic
// programs after numerical evaluation.
optional bool use_diagonal_qp_trust_region_solver = 23 [default = false];
// The solve tolerance of the experimental trust region solver for diagonal
// QPs, controlling the accuracy of binary search over a one-dimensional
// scaling parameter. Smaller values imply smaller relative error of the final
// solution vector.
// TODO(user): Find an expression for the final relative error.
optional double diagonal_qp_trust_region_solver_tolerance = 24
[default = 1e-8];
reserved 21;
}

View File

@@ -0,0 +1,159 @@
// Copyright 2010-2021 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/solvers_proto_validation.h"
#include "absl/status/status.h"
#include "ortools/base/status_macros.h"
#include "ortools/pdlp/solvers.pb.h"
namespace operations_research::pdlp {
using ::absl::InvalidArgumentError;
using ::absl::OkStatus;
absl::Status ValidateTerminationCriteria(const TerminationCriteria& criteria) {
if (criteria.optimality_norm() != OPTIMALITY_NORM_L_INF &&
criteria.optimality_norm() != OPTIMALITY_NORM_L2) {
return InvalidArgumentError("invalid value for optimality_norm");
}
if (criteria.eps_optimal_absolute() < 0) {
return InvalidArgumentError("eps_optimal_absolute must be non-negative");
}
if (criteria.eps_optimal_relative() < 0) {
return InvalidArgumentError("eps_optimal_relative must be non-negative");
}
if (criteria.eps_primal_infeasible() < 0) {
return InvalidArgumentError("eps_primal_infeasible must be non-negative");
}
if (criteria.eps_dual_infeasible() < 0) {
return InvalidArgumentError("eps_dual_infeasible must be non-negative");
}
if (criteria.time_sec_limit() < 0) {
return InvalidArgumentError("time_sec_limit must be non-negative");
}
if (criteria.iteration_limit() < 0) {
return InvalidArgumentError("iteration_limit must be non-negative");
}
if (criteria.kkt_matrix_pass_limit() < 0) {
return InvalidArgumentError("kkt_matrix_pass_limit must be non-negative");
}
return OkStatus();
}
absl::Status ValidateAdaptiveLinesearchParams(
const AdaptiveLinesearchParams& params) {
if (params.step_size_reduction_exponent() <= 0) {
return InvalidArgumentError(
"step_size_reduction_exponent must be positive");
}
if (params.step_size_growth_exponent() <= 0) {
return InvalidArgumentError("step_size_growth_exponent must be positive");
}
return OkStatus();
}
absl::Status ValidateMalitskyPockParams(const MalitskyPockParams& params) {
if (params.step_size_downscaling_factor() <= 0 ||
params.step_size_downscaling_factor() >= 1) {
return InvalidArgumentError(
"step_size_downscaling_factor must be between 0 and 1 exclusive");
}
if (params.linesearch_contraction_factor() <= 0 ||
params.linesearch_contraction_factor() >= 1) {
return InvalidArgumentError(
"linesearch_contraction_factor must be between 0 and 1 exclusive");
}
if (params.step_size_interpolation() < 0) {
return InvalidArgumentError("step_size_interpolation must be non-negative");
}
return OkStatus();
}
absl::Status ValidatePrimalDualHybridGradientParams(
const PrimalDualHybridGradientParams& params) {
RETURN_IF_ERROR(ValidateTerminationCriteria(params.termination_criteria()))
<< "termination_criteria invalid";
if (params.num_threads() <= 0) {
return InvalidArgumentError("num_threads must be positive");
}
if (params.major_iteration_frequency() <= 0) {
return InvalidArgumentError("major_iteration_frequency must be positive");
}
if (params.termination_check_frequency() <= 0) {
return InvalidArgumentError("termination_check_frequency must be positive");
}
if (params.restart_strategy() !=
PrimalDualHybridGradientParams::NO_RESTARTS &&
params.restart_strategy() !=
PrimalDualHybridGradientParams::EVERY_MAJOR_ITERATION &&
params.restart_strategy() !=
PrimalDualHybridGradientParams::ADAPTIVE_HEURISTIC &&
params.restart_strategy() !=
PrimalDualHybridGradientParams::ADAPTIVE_DISTANCE_BASED) {
return InvalidArgumentError("invalid restart_strategy");
}
if (params.primal_weight_update_smoothing() < 0 ||
params.primal_weight_update_smoothing() > 1) {
return InvalidArgumentError(
"primal_weight_update_smoothing must be between 0 and 1 inclusive");
}
if (params.has_initial_primal_weight() &&
params.initial_primal_weight() <= 0) {
return InvalidArgumentError(
"initial_primal_weight must be positive if specified");
}
if (params.l_inf_ruiz_iterations() < 0) {
return InvalidArgumentError("l_inf_ruiz_iterations must be non-negative");
}
if (params.sufficient_reduction_for_restart() <= 0 ||
params.sufficient_reduction_for_restart() >= 1) {
return InvalidArgumentError(
"sufficient_reduction_for_restart must be between 0 and 1 exclusive");
}
if (params.necessary_reduction_for_restart() <
params.sufficient_reduction_for_restart() ||
params.necessary_reduction_for_restart() >= 1) {
return InvalidArgumentError(
"necessary_reduction_for_restart must be in the interval "
"[sufficient_reduction_for_restart, 1)");
}
if (params.linesearch_rule() !=
PrimalDualHybridGradientParams::ADAPTIVE_LINESEARCH_RULE &&
params.linesearch_rule() !=
PrimalDualHybridGradientParams::MALITSKY_POCK_LINESEARCH_RULE &&
params.linesearch_rule() !=
PrimalDualHybridGradientParams::CONSTANT_STEP_SIZE_RULE) {
return InvalidArgumentError("invalid linesearch_rule");
}
RETURN_IF_ERROR(
ValidateAdaptiveLinesearchParams(params.adaptive_linesearch_parameters()))
<< "adaptive_linesearch_parameters invalid";
RETURN_IF_ERROR(ValidateMalitskyPockParams(params.malitsky_pock_parameters()))
<< "malitsky_pock_parameters invalid";
if (params.initial_step_size_scaling() <= 0.0) {
return InvalidArgumentError("initial_step_size_scaling must be positive");
}
if (params.infinite_constraint_bound_threshold() < 0.0) {
return InvalidArgumentError(
"infinite_constraint_bound_threshold must be non-negative");
}
if (params.diagonal_qp_trust_region_solver_tolerance() < 0.0) {
return InvalidArgumentError(
"diagonal_qp_trust_region_solver_tolerance must be non-negative");
}
return OkStatus();
}
} // namespace operations_research::pdlp

View File

@@ -0,0 +1,44 @@
// Copyright 2010-2021 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.
// Validation utilities for solvers.proto.
#ifndef PDLP_SOLVERS_PROTO_VALIDATION_H_
#define PDLP_SOLVERS_PROTO_VALIDATION_H_
#include "absl/status/status.h"
#include "ortools/pdlp/solvers.pb.h"
namespace operations_research::pdlp {
// Returns InvalidArgument if the proto contains invalid values. Returns
// OkStatus otherwise.
absl::Status ValidateTerminationCriteria(const TerminationCriteria& criteria);
// Returns InvalidArgument if the proto contains invalid values. Returns
// OkStatus otherwise.
absl::Status ValidateAdaptiveLinesearchParams(
const AdaptiveLinesearchParams& params);
// Returns InvalidArgument if the proto contains invalid values. Returns
// OkStatus otherwise.
absl::Status ValidateMalitskyPockParams(const MalitskyPockParams& params);
// Returns InvalidArgument if the proto contains invalid values. Returns
// OkStatus otherwise.
absl::Status ValidatePrimalDualHybridGradientParams(
const PrimalDualHybridGradientParams& params);
} // namespace operations_research::pdlp
#endif // PDLP_SOLVERS_PROTO_VALIDATION_H_

View File

@@ -0,0 +1,334 @@
// Copyright 2010-2021 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/solvers_proto_validation.h"
#include "absl/status/status.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/pdlp/solvers.pb.h"
namespace operations_research::pdlp {
namespace {
using ::testing::HasSubstr;
TEST(ValidateTerminationCriteria, DefaultIsValid) {
TerminationCriteria criteria;
const absl::Status status = ValidateTerminationCriteria(criteria);
EXPECT_TRUE(status.ok()) << status;
}
TEST(ValidateTerminationCriteria, BadOptimalityNorm) {
TerminationCriteria criteria;
criteria.set_optimality_norm(OPTIMALITY_NORM_UNSPECIFIED);
const absl::Status status = ValidateTerminationCriteria(criteria);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("optimality_norm"));
}
TEST(ValidateTerminationCriteria, BadEpsOptimalAbsolute) {
TerminationCriteria criteria;
criteria.set_eps_optimal_absolute(-1.0);
const absl::Status status = ValidateTerminationCriteria(criteria);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("eps_optimal_absolute"));
}
TEST(ValidateTerminationCriteria, BadEpsOptimalRelative) {
TerminationCriteria criteria;
criteria.set_eps_optimal_relative(-1.0);
const absl::Status status = ValidateTerminationCriteria(criteria);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("eps_optimal_relative"));
}
TEST(ValidateTerminationCriteria, BadEpsPriamlInfeasible) {
TerminationCriteria criteria;
criteria.set_eps_primal_infeasible(-1.0);
const absl::Status status = ValidateTerminationCriteria(criteria);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("eps_primal_infeasible"));
}
TEST(ValidateTerminationCriteria, BadEpsDualInfeasible) {
TerminationCriteria criteria;
criteria.set_eps_dual_infeasible(-1.0);
const absl::Status status = ValidateTerminationCriteria(criteria);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("eps_dual_infeasible"));
}
TEST(ValidateTerminationCriteria, BadTimeSecLimit) {
TerminationCriteria criteria;
criteria.set_time_sec_limit(-1.0);
const absl::Status status = ValidateTerminationCriteria(criteria);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("time_sec_limit"));
}
TEST(ValidateTerminationCriteria, BadIterationLimit) {
TerminationCriteria criteria;
criteria.set_iteration_limit(-1);
const absl::Status status = ValidateTerminationCriteria(criteria);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("iteration_limit"));
}
TEST(ValidateTerminationCriteria, BadKktMatrixPassLimit) {
TerminationCriteria criteria;
criteria.set_kkt_matrix_pass_limit(-1.0);
const absl::Status status = ValidateTerminationCriteria(criteria);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("kkt_matrix_pass_limit"));
}
TEST(ValidateAdaptiveLinesearchParams, DefaultIsValid) {
AdaptiveLinesearchParams params;
const absl::Status status = ValidateAdaptiveLinesearchParams(params);
EXPECT_TRUE(status.ok()) << status;
}
TEST(ValidateAdaptiveLinesearchParams, BadReductionExponent) {
AdaptiveLinesearchParams params;
params.set_step_size_reduction_exponent(0.0);
const absl::Status status = ValidateAdaptiveLinesearchParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("step_size_reduction_exponent"));
}
TEST(ValidateAdaptiveLinesearchParams, BadGrowthExponent) {
AdaptiveLinesearchParams params;
params.set_step_size_growth_exponent(0.0);
const absl::Status status = ValidateAdaptiveLinesearchParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("step_size_growth_exponent"));
}
TEST(ValidateMalitskyPockParams, DefaultIsValid) {
MalitskyPockParams params;
const absl::Status status = ValidateMalitskyPockParams(params);
EXPECT_TRUE(status.ok()) << status;
}
TEST(ValidateMalitskyPockParams, BadDownscalingFactor) {
MalitskyPockParams params0;
params0.set_step_size_downscaling_factor(0.0);
const absl::Status status0 = ValidateMalitskyPockParams(params0);
EXPECT_EQ(status0.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status0.message(), HasSubstr("step_size_downscaling_factor"));
MalitskyPockParams params1;
params1.set_step_size_downscaling_factor(1.0);
const absl::Status status1 = ValidateMalitskyPockParams(params1);
EXPECT_EQ(status1.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status1.message(), HasSubstr("step_size_downscaling_factor"));
}
TEST(ValidateMalitskyPockParams, BadConstractionFactor) {
MalitskyPockParams params0;
params0.set_linesearch_contraction_factor(0.0);
const absl::Status status0 = ValidateMalitskyPockParams(params0);
EXPECT_EQ(status0.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status0.message(), HasSubstr("linesearch_contraction_factor"));
MalitskyPockParams params1;
params1.set_linesearch_contraction_factor(1.0);
const absl::Status status1 = ValidateMalitskyPockParams(params1);
EXPECT_EQ(status1.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status1.message(), HasSubstr("linesearch_contraction_factor"));
}
TEST(ValidateMalitskyPockParams, BadStepSizeInterpolation) {
MalitskyPockParams params;
params.set_step_size_interpolation(-1.0);
const absl::Status status = ValidateMalitskyPockParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("step_size_interpolation"));
}
TEST(ValidatePrimalDualHybridGradientParams, DefaultIsValid) {
PrimalDualHybridGradientParams params;
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_TRUE(status.ok()) << status;
}
TEST(ValidatePrimalDualHybridGradientParams, BadTerminationCriteria) {
PrimalDualHybridGradientParams params;
params.mutable_termination_criteria()->set_eps_dual_infeasible(-1);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("eps_dual_infeasible"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadNumThreads) {
PrimalDualHybridGradientParams params;
params.set_num_threads(0);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("num_threads"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadMajorIterationFrequency) {
PrimalDualHybridGradientParams params;
params.set_major_iteration_frequency(0);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("major_iteration_frequency"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadTerminationCheckFrequency) {
PrimalDualHybridGradientParams params;
params.set_termination_check_frequency(0);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("termination_check_frequency"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadRestartStrategy) {
PrimalDualHybridGradientParams params;
params.set_restart_strategy(
PrimalDualHybridGradientParams::RESTART_STRATEGY_UNSPECIFIED);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("restart_strategy"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadPrimalWeightUpdateSmoothing) {
PrimalDualHybridGradientParams params_high;
params_high.set_primal_weight_update_smoothing(1.1);
const absl::Status status_high =
ValidatePrimalDualHybridGradientParams(params_high);
EXPECT_EQ(status_high.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status_high.message(),
HasSubstr("primal_weight_update_smoothing"));
PrimalDualHybridGradientParams params_low;
params_low.set_primal_weight_update_smoothing(-0.1);
const absl::Status status_low =
ValidatePrimalDualHybridGradientParams(params_low);
EXPECT_EQ(status_low.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status_low.message(),
HasSubstr("primal_weight_update_smoothing"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadInitialPrimalWeight) {
PrimalDualHybridGradientParams params;
params.set_initial_primal_weight(-1);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("initial_primal_weight"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadLInfRuizIterations) {
PrimalDualHybridGradientParams params;
params.set_l_inf_ruiz_iterations(-1);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("l_inf_ruiz_iterations"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadSufficientReductionForRestart) {
PrimalDualHybridGradientParams params_high;
params_high.set_sufficient_reduction_for_restart(1.0);
const absl::Status status_high =
ValidatePrimalDualHybridGradientParams(params_high);
EXPECT_EQ(status_high.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status_high.message(),
HasSubstr("sufficient_reduction_for_restart"));
PrimalDualHybridGradientParams params_low;
params_low.set_sufficient_reduction_for_restart(0.0);
const absl::Status status_low =
ValidatePrimalDualHybridGradientParams(params_low);
EXPECT_EQ(status_low.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status_low.message(),
HasSubstr("sufficient_reduction_for_restart"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadNecessaryReductionForRestart) {
PrimalDualHybridGradientParams params_high;
params_high.set_necessary_reduction_for_restart(1.0);
const absl::Status status_high =
ValidatePrimalDualHybridGradientParams(params_high);
EXPECT_EQ(status_high.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status_high.message(),
HasSubstr("necessary_reduction_for_restart"));
PrimalDualHybridGradientParams params_low;
params_low.set_sufficient_reduction_for_restart(0.5);
params_low.set_necessary_reduction_for_restart(0.4);
const absl::Status status_low =
ValidatePrimalDualHybridGradientParams(params_low);
EXPECT_EQ(status_low.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status_low.message(),
HasSubstr("necessary_reduction_for_restart"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadLinesearchRule) {
PrimalDualHybridGradientParams params;
params.set_linesearch_rule(
PrimalDualHybridGradientParams::LINESEARCH_RULE_UNSPECIFIED);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("linesearch_rule"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadAdaptiveLinesearchParameters) {
PrimalDualHybridGradientParams params;
params.mutable_adaptive_linesearch_parameters()
->set_step_size_reduction_exponent(-1);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("step_size_reduction_exponent"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadMalitskyPockParameters) {
PrimalDualHybridGradientParams params;
params.mutable_malitsky_pock_parameters()->set_linesearch_contraction_factor(
-1);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("linesearch_contraction_factor"));
}
TEST(ValidatePrimalDualHybridGradientParams, BadInitialStepSizeScaling) {
PrimalDualHybridGradientParams params;
params.set_initial_step_size_scaling(-1.0);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(), HasSubstr("initial_step_size_scaling"));
}
TEST(ValidatePrimalDualHybridGradientParams,
BadInfiniteConstraintBoundThreshold) {
PrimalDualHybridGradientParams params;
params.set_infinite_constraint_bound_threshold(-1.0);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(),
HasSubstr("infinite_constraint_bound_threshold"));
}
TEST(ValidatePrimalDualHybridGradientParams,
BadDiagonalTrustRegionSolverTolerance) {
PrimalDualHybridGradientParams params;
params.set_diagonal_qp_trust_region_solver_tolerance(-1.0);
const absl::Status status = ValidatePrimalDualHybridGradientParams(params);
EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
EXPECT_THAT(status.message(),
HasSubstr("diagonal_qp_trust_region_solver_tolerance"));
}
} // namespace
} // namespace operations_research::pdlp

175
ortools/pdlp/termination.cc Normal file
View File

@@ -0,0 +1,175 @@
// Copyright 2010-2021 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/termination.h"
#include <cmath>
#include <limits>
#include "absl/types/optional.h"
#include "ortools/base/logging.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/solvers.pb.h"
namespace operations_research::pdlp {
namespace {
bool OptimalityCriteriaMet(const OptimalityNorm optimality_norm,
const double abs_tol, const double rel_tol,
const ConvergenceInformation& stats,
const QuadraticProgramBoundNorms& bound_norms) {
const double abs_obj =
std::abs(stats.primal_objective()) + std::abs(stats.dual_objective());
const double gap =
std::abs(stats.primal_objective() - stats.dual_objective());
double primal_err;
double primal_err_baseline;
double dual_err;
double dual_err_baseline;
switch (optimality_norm) {
case OPTIMALITY_NORM_L_INF:
primal_err = stats.l_inf_primal_residual();
primal_err_baseline = bound_norms.l_inf_norm_constraint_bounds;
dual_err = stats.l_inf_dual_residual();
dual_err_baseline = bound_norms.l_inf_norm_primal_linear_objective;
break;
case OPTIMALITY_NORM_L2:
primal_err = stats.l2_primal_residual();
primal_err_baseline = bound_norms.l2_norm_constraint_bounds;
dual_err = stats.l2_dual_residual();
dual_err_baseline = bound_norms.l2_norm_primal_linear_objective;
break;
default:
LOG(FATAL) << "Invalid optimality_norm value "
<< OptimalityNorm_Name(optimality_norm);
}
return dual_err <= abs_tol + rel_tol * dual_err_baseline &&
primal_err <= abs_tol + rel_tol * primal_err_baseline &&
std::isfinite(abs_obj) && gap <= abs_tol + rel_tol * abs_obj;
}
// Checks if the criteria for primal infeasibility are approximately
// satisfied; see https://developers.google.com/optimization/lp/pdlp_math for
// more details.
bool PrimalInfeasibilityCriteriaMet(double eps_primal_infeasible,
const InfeasibilityInformation& stats) {
if (stats.dual_ray_objective() <= 0.0) return false;
return stats.max_dual_ray_infeasibility() / stats.dual_ray_objective() <=
eps_primal_infeasible;
}
// Checks if the criteria for dual infeasibility are approximately satisfied;
// see https://developers.google.com/optimization/lp/pdlp_math for more details.
bool DualInfeasibilityCriteriaMet(double eps_dual_infeasible,
const InfeasibilityInformation& stats) {
if (stats.primal_ray_linear_objective() >= 0.0) return false;
return (stats.max_primal_ray_infeasibility() /
-stats.primal_ray_linear_objective() <=
eps_dual_infeasible) &&
(stats.primal_ray_quadratic_norm() /
-stats.primal_ray_linear_objective() <=
eps_dual_infeasible);
}
} // namespace
absl::optional<TerminationReasonAndPointType> CheckTerminationCriteria(
const TerminationCriteria& criteria, const IterationStats& stats,
const QuadraticProgramBoundNorms& bound_norms,
const bool force_numerical_termination) {
for (const auto& convergence_stats : stats.convergence_information()) {
if (OptimalityCriteriaMet(
criteria.optimality_norm(), criteria.eps_optimal_absolute(),
criteria.eps_optimal_relative(), convergence_stats, bound_norms)) {
return TerminationReasonAndPointType{
.reason = TERMINATION_REASON_OPTIMAL,
.type = convergence_stats.candidate_type()};
}
}
for (const auto& infeasibility_stats : stats.infeasibility_information()) {
if (PrimalInfeasibilityCriteriaMet(criteria.eps_primal_infeasible(),
infeasibility_stats)) {
return TerminationReasonAndPointType{
.reason = TERMINATION_REASON_PRIMAL_INFEASIBLE,
.type = infeasibility_stats.candidate_type()};
}
if (DualInfeasibilityCriteriaMet(criteria.eps_dual_infeasible(),
infeasibility_stats)) {
return TerminationReasonAndPointType{
.reason = TERMINATION_REASON_DUAL_INFEASIBLE,
.type = infeasibility_stats.candidate_type()};
}
}
if (stats.iteration_number() >= criteria.iteration_limit()) {
return TerminationReasonAndPointType{
.reason = TERMINATION_REASON_ITERATION_LIMIT, .type = POINT_TYPE_NONE};
}
if (stats.cumulative_kkt_matrix_passes() >=
criteria.kkt_matrix_pass_limit()) {
return TerminationReasonAndPointType{
.reason = TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT,
.type = POINT_TYPE_NONE};
}
if (stats.cumulative_time_sec() >= criteria.time_sec_limit()) {
return TerminationReasonAndPointType{
.reason = TERMINATION_REASON_TIME_LIMIT, .type = POINT_TYPE_NONE};
}
if (force_numerical_termination) {
return TerminationReasonAndPointType{
.reason = TERMINATION_REASON_NUMERICAL_ERROR, .type = POINT_TYPE_NONE};
}
return absl::nullopt;
}
QuadraticProgramBoundNorms BoundNormsFromProblemStats(
const QuadraticProgramStats& stats) {
return {
.l2_norm_primal_linear_objective = stats.objective_vector_l2_norm(),
.l2_norm_constraint_bounds = stats.combined_bounds_l2_norm(),
.l_inf_norm_primal_linear_objective = stats.objective_vector_abs_max(),
.l_inf_norm_constraint_bounds = stats.combined_bounds_max()};
}
RelativeConvergenceInformation ComputeRelativeResiduals(
const double eps_optimal_absolute, const double eps_optimal_relative,
const QuadraticProgramBoundNorms& norms,
const ConvergenceInformation& stats) {
const double eps_ratio = eps_optimal_relative == 0.0
? std::numeric_limits<double>::infinity()
: eps_optimal_absolute / eps_optimal_relative;
RelativeConvergenceInformation info;
info.relative_l_inf_primal_residual =
stats.l_inf_primal_residual() /
(eps_ratio + norms.l_inf_norm_constraint_bounds);
info.relative_l2_primal_residual =
stats.l2_primal_residual() /
(eps_ratio + norms.l2_norm_constraint_bounds);
info.relative_l_inf_dual_residual =
stats.l_inf_dual_residual() /
(eps_ratio + norms.l_inf_norm_primal_linear_objective);
info.relative_l2_dual_residual =
stats.l2_dual_residual() /
(eps_ratio + norms.l2_norm_primal_linear_objective);
const double abs_obj =
std::abs(stats.primal_objective()) + std::abs(stats.dual_objective());
const double gap = stats.primal_objective() - stats.dual_objective();
info.relative_optimality_gap = gap / (eps_ratio + abs_obj);
return info;
}
} // namespace operations_research::pdlp

View File

@@ -0,0 +1,94 @@
// Copyright 2010-2021 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.
#ifndef PDLP_TERMINATION_H_
#define PDLP_TERMINATION_H_
#include "absl/types/optional.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/solvers.pb.h"
namespace operations_research::pdlp {
struct TerminationReasonAndPointType {
TerminationReason reason;
PointType type;
};
// Information about the quadratic program's primal and dual bounds that are
// needed to evaluate relative convergence criteria.
struct QuadraticProgramBoundNorms {
double l2_norm_primal_linear_objective;
double l2_norm_constraint_bounds;
double l_inf_norm_primal_linear_objective;
double l_inf_norm_constraint_bounds;
};
// Checks if any termination criteria are satisfied by the solution state
// described by the IterationStats instance stats (see definitions of
// termination criteria in solvers.proto). bound_norms provides the instance-
// dependent data required for the relative convergence criteria. Returns a
// termination reason and a point type if so (if multiple are satisfied, the
// optimality and infeasibility conditions are checked first). If
// force_numerical_termination is true, returns NUMERICAL_ERROR if no other
// criteria are satisfied. The return value is empty in any other case. If the
// output is not empty, the PointType indicates which entry prompted
// termination. If no entry prompted termination, e.g. NUMERICAL_ERROR or
// ITERATION_LIMIT is returned, then the PointType is set to POINT_TYPE_NONE.
// NOTE: This function assumes that the solution used to compute the stats
// satisfies the primal and dual variable bounds; see
// https://developers.google.com/optimization/lp/pdlp_math#dual_variable_bounds.
absl::optional<TerminationReasonAndPointType> CheckTerminationCriteria(
const TerminationCriteria& criteria, const IterationStats& stats,
const QuadraticProgramBoundNorms& bound_norms,
bool force_numerical_termination = false);
// Extracts the norms needed for the termination criteria from the full problem
// statistics.
QuadraticProgramBoundNorms BoundNormsFromProblemStats(
const QuadraticProgramStats& stats);
// Metrics for tracking progress when relative convergence criteria are used.
// These depend on the ConvergenceInformation, the problem data, and the
// convergence tolerances.
struct RelativeConvergenceInformation {
// Relative versions of the residuals, defined as
// relative_residual = residual / (eps_ratio + norm),
// where
// eps_ratio = eps_optimal_absolute / eps_optimal_relative
// residual = one of the residuals (l{2,_inf}_{primal,dual}_residual)
// norm = the relative norm (l{2,_inf} norm of
// {constraint_bounds,primal_linear_objective} respectively).
// If eps_optimal_relative = 0.0, these will all be 0.0.
//
// If eps_optimal_relative > 0.0, the absolute and relative termination
// criteria translate to relative_residual <= eps_optimal_relative.
double relative_l_inf_primal_residual = 0;
double relative_l2_primal_residual = 0;
double relative_l_inf_dual_residual = 0;
double relative_l2_dual_residual = 0;
// Relative optimality gap:
// (primal_objective - dual_objective) /
// (eps_ratio + |primal_objective| + |dual_objective|)
double relative_optimality_gap = 0;
};
RelativeConvergenceInformation ComputeRelativeResiduals(
double eps_optimal_absolute, double eps_optimal_relative,
const QuadraticProgramBoundNorms& norms,
const ConvergenceInformation& stats);
} // namespace operations_research::pdlp
#endif // PDLP_TERMINATION_H_

View File

@@ -0,0 +1,470 @@
// Copyright 2010-2021 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/termination.h"
#include <cmath>
#include "absl/types/optional.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/base/protobuf_util.h"
#include "ortools/pdlp/solve_log.pb.h"
#include "ortools/pdlp/solvers.pb.h"
namespace operations_research::pdlp {
namespace {
using ::google::protobuf::util::ParseTextOrDie;
using ::testing::FieldsAre;
using ::testing::Optional;
QuadraticProgramBoundNorms TestLpBoundNorms() {
return {.l2_norm_primal_linear_objective = std::sqrt(36.25),
.l2_norm_constraint_bounds = std::sqrt(210.0),
.l_inf_norm_primal_linear_objective = 5.5,
.l_inf_norm_constraint_bounds = 12.0};
}
class TerminationTest : public testing::TestWithParam<OptimalityNorm> {
protected:
void SetUp() override {
test_criteria_ = ParseTextOrDie<TerminationCriteria>(R"pb(
eps_optimal_absolute: 1.0e-4
eps_optimal_relative: 1.0e-4
eps_primal_infeasible: 1.0e-6
eps_dual_infeasible: 1.0e-6
time_sec_limit: 1.0
kkt_matrix_pass_limit: 2000
iteration_limit: 10)pb");
test_criteria_.set_optimality_norm(GetParam());
}
TerminationCriteria test_criteria_;
};
TEST_P(TerminationTest, NoTerminationWithLargeGap) {
IterationStats stats = ParseTextOrDie<IterationStats>(R"pb(
convergence_information {
# Ensures that optimality conditions are not met.
primal_objective: 50.0
dual_objective: -50.0
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, NoTerminationWithEmptyIterationStats) {
IterationStats stats;
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, TerminationWithNumericalError) {
IterationStats stats;
absl::optional<TerminationReasonAndPointType> maybe_result =
CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms(),
/*force_numerical_termination=*/true);
EXPECT_THAT(
maybe_result,
Optional(FieldsAre(TERMINATION_REASON_NUMERICAL_ERROR, POINT_TYPE_NONE)));
}
TEST_P(TerminationTest, TerminationWithTimeLimit) {
const auto stats =
ParseTextOrDie<IterationStats>(R"pb(cumulative_time_sec: 100.0)pb");
absl::optional<TerminationReasonAndPointType> maybe_result =
CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_TIME_LIMIT,
POINT_TYPE_NONE)));
}
TEST_P(TerminationTest, TerminationWithKktMatrixPassLimit) {
const auto stats = ParseTextOrDie<IterationStats>(R"pb(
cumulative_kkt_matrix_passes: 2500)pb");
absl::optional<TerminationReasonAndPointType> maybe_result =
CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
EXPECT_THAT(maybe_result,
Optional(FieldsAre(TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT,
POINT_TYPE_NONE)));
}
TEST_P(TerminationTest, PrimalInfeasibleFromIterateDifference) {
const auto stats = ParseTextOrDie<IterationStats>(R"pb(
infeasibility_information: {
dual_ray_objective: 1.0
max_dual_ray_infeasibility: 1.0e-16
candidate_type: POINT_TYPE_ITERATE_DIFFERENCE
})pb");
absl::optional<TerminationReasonAndPointType> maybe_result =
CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
EXPECT_THAT(maybe_result,
Optional(FieldsAre(TERMINATION_REASON_PRIMAL_INFEASIBLE,
POINT_TYPE_ITERATE_DIFFERENCE)));
}
TEST_P(TerminationTest, NoTerminationWithInfeasibleDualRay) {
const auto stats_infeasible_ray = ParseTextOrDie<IterationStats>(R"pb(
infeasibility_information: {
dual_ray_objective: 1.0
max_dual_ray_infeasibility: 1.0e-5 # Too large
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_infeasible_ray,
TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, NoTerminationWithNegativeDualRayObjective) {
const auto stats_wrong_sign = ParseTextOrDie<IterationStats>(R"pb(
infeasibility_information: {
dual_ray_objective: -1.0 # Wrong sign
max_dual_ray_infeasibility: 0.0
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_wrong_sign,
TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, NoTerminationWithZeroDualRayObjective) {
const auto stats_objective_zero = ParseTextOrDie<IterationStats>(R"pb(
infeasibility_information: {
dual_ray_objective: 0.0
max_dual_ray_infeasibility: 0.0
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_objective_zero,
TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, DualInfeasibleFromAverageIterate) {
const auto stats = ParseTextOrDie<IterationStats>(R"pb(
infeasibility_information: {
primal_ray_linear_objective: -1.0
max_primal_ray_infeasibility: 1.0e-16
candidate_type: POINT_TYPE_AVERAGE_ITERATE
})pb");
absl::optional<TerminationReasonAndPointType> maybe_result =
CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
EXPECT_THAT(maybe_result,
Optional(FieldsAre(TERMINATION_REASON_DUAL_INFEASIBLE,
POINT_TYPE_AVERAGE_ITERATE)));
}
TEST_P(TerminationTest, NoTerminationWithInfeasiblePrimalRay) {
const auto stats_infeasible_ray = ParseTextOrDie<IterationStats>(R"pb(
infeasibility_information: {
primal_ray_linear_objective: -1.0
max_primal_ray_infeasibility: 1.0e-5 # Too large
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_infeasible_ray,
TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, NoTerminationWithPositivePrimalRayObjective) {
const auto stats_wrong_sign = ParseTextOrDie<IterationStats>(R"pb(
infeasibility_information: {
primal_ray_linear_objective: 1.0 # Wrong sign
max_primal_ray_infeasibility: 0.0
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_wrong_sign,
TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, NoTerminationWithZeroPrimalRayObjective) {
const auto stats_objective_zero = ParseTextOrDie<IterationStats>(R"pb(
infeasibility_information: {
primal_ray_linear_objective: 0.0
max_primal_ray_infeasibility: 0.0
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_objective_zero,
TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, Optimal) {
const auto stats = ParseTextOrDie<IterationStats>(R"pb(
convergence_information {
primal_objective: 1.0
dual_objective: 1.0
l_inf_primal_residual: 0.0
l_inf_dual_residual: 0.0
l2_primal_residual: 0.0
l2_dual_residual: 0.0
candidate_type: POINT_TYPE_CURRENT_ITERATE
})pb");
absl::optional<TerminationReasonAndPointType> maybe_result =
CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL,
POINT_TYPE_CURRENT_ITERATE)));
}
TEST_P(TerminationTest, OptimalEvenWithNumericalError) {
const auto stats = ParseTextOrDie<IterationStats>(R"pb(
convergence_information {
primal_objective: 1.0
dual_objective: 1.0
l_inf_primal_residual: 0.0
l_inf_dual_residual: 0.0
l2_primal_residual: 0.0
l2_dual_residual: 0.0
candidate_type: POINT_TYPE_CURRENT_ITERATE
})pb");
// Tests that OPTIMAL overrides NUMERICAL_ERROR when
// force_numerical_termination == true.
absl::optional<TerminationReasonAndPointType> maybe_result =
CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms(),
/*force_numerical_termination=*/true);
EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL,
POINT_TYPE_CURRENT_ITERATE)));
}
TEST_P(TerminationTest, NoTerminationWithBadGap) {
const auto stats_bad_gap = ParseTextOrDie<IterationStats>(R"pb(
convergence_information {
primal_objective: 10.0
dual_objective: 1.0
l_inf_primal_residual: 0.0
l_inf_dual_residual: 0.0
l2_primal_residual: 0.0
l2_dual_residual: 0.0
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_bad_gap,
TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, NoTerminationWithInfiniteGap) {
const auto stats_infinite_gap = ParseTextOrDie<IterationStats>(R"pb(
convergence_information {
primal_objective: 0
dual_objective: -Inf
l_inf_primal_residual: 0.0
l_inf_dual_residual: 0.0
l2_primal_residual: 0.0
l2_dual_residual: 0.0
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_infinite_gap,
TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, NoTerminationWithBadPrimalResidual) {
const auto stats_bad_primal = ParseTextOrDie<IterationStats>(R"pb(
convergence_information {
primal_objective: 1.0
dual_objective: 1.0
l_inf_primal_residual: 1.0
l_inf_dual_residual: 0.0
l2_primal_residual: 1.0
l2_dual_residual: 0.0
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_bad_primal,
TestLpBoundNorms()),
absl::nullopt);
}
TEST_P(TerminationTest, NoTerminationWithBadDualResidual) {
const auto stats_bad_dual = ParseTextOrDie<IterationStats>(R"pb(
convergence_information {
primal_objective: 1.0
dual_objective: 1.0
l_inf_primal_residual: 0.0
l_inf_dual_residual: 1.0
l2_primal_residual: 0.0
l2_dual_residual: 1.0
})pb");
EXPECT_EQ(CheckTerminationCriteria(test_criteria_, stats_bad_dual,
TestLpBoundNorms()),
absl::nullopt);
}
// Tests that optimality is checked with non-strict inequalities, as per the
// definitions in solvers.proto.
TEST_P(TerminationTest, ZeroToleranceZeroError) {
const auto stats = ParseTextOrDie<IterationStats>(R"pb(
convergence_information {
primal_objective: 1.0
dual_objective: 1.0
l_inf_primal_residual: 0.0
l_inf_dual_residual: 0.0
l2_primal_residual: 0.0
l2_dual_residual: 0.0
candidate_type: POINT_TYPE_CURRENT_ITERATE
})pb");
test_criteria_.set_eps_optimal_absolute(0.0);
test_criteria_.set_eps_optimal_relative(0.0);
absl::optional<TerminationReasonAndPointType> maybe_result =
CheckTerminationCriteria(test_criteria_, stats, TestLpBoundNorms());
EXPECT_THAT(maybe_result, Optional(FieldsAre(TERMINATION_REASON_OPTIMAL,
POINT_TYPE_CURRENT_ITERATE)));
}
INSTANTIATE_TEST_SUITE_P(OptNorm, TerminationTest,
testing::Values(OPTIMALITY_NORM_L2,
OPTIMALITY_NORM_L_INF));
TEST(TerminationTest, L2AndLInfDiffer) {
auto test_criteria = ParseTextOrDie<TerminationCriteria>(R"pb(
eps_optimal_relative: 1.0)pb");
// For L2, optimality requires norm(primal_residual, 2) <= 14.49
// For LInf, optimality requires norm(primal_residual, Inf) <= 12.0
struct {
double primal_residual;
absl::optional<TerminationReasonAndPointType> expected_l2;
absl::optional<TerminationReasonAndPointType> expected_l_inf;
} test_configs[] = {
{10.0,
TerminationReasonAndPointType{.reason = TERMINATION_REASON_OPTIMAL,
.type = POINT_TYPE_CURRENT_ITERATE},
TerminationReasonAndPointType{.reason = TERMINATION_REASON_OPTIMAL,
.type = POINT_TYPE_CURRENT_ITERATE}},
{13.0,
TerminationReasonAndPointType{.reason = TERMINATION_REASON_OPTIMAL,
.type = POINT_TYPE_CURRENT_ITERATE},
absl::nullopt},
{15.0, absl::nullopt, absl::nullopt}};
for (const auto& config : test_configs) {
IterationStats stats;
auto* convergence_info = stats.add_convergence_information();
convergence_info->set_primal_objective(1.0);
convergence_info->set_dual_objective(1.0);
convergence_info->set_l2_primal_residual(config.primal_residual);
convergence_info->set_l_inf_primal_residual(config.primal_residual);
convergence_info->set_candidate_type(POINT_TYPE_CURRENT_ITERATE);
test_criteria.set_optimality_norm(OPTIMALITY_NORM_L2);
absl::optional<TerminationReasonAndPointType> maybe_result =
CheckTerminationCriteria(test_criteria, stats, TestLpBoundNorms());
ASSERT_TRUE(maybe_result.has_value() == config.expected_l2.has_value())
<< "primal_residual: " << config.primal_residual;
if (config.expected_l2.has_value()) {
EXPECT_EQ(maybe_result->reason, config.expected_l2->reason);
EXPECT_EQ(maybe_result->type, config.expected_l2->type);
}
test_criteria.set_optimality_norm(OPTIMALITY_NORM_L_INF);
maybe_result =
CheckTerminationCriteria(test_criteria, stats, TestLpBoundNorms());
ASSERT_TRUE(maybe_result.has_value() == config.expected_l_inf.has_value())
<< "primal_residual: " << config.primal_residual;
if (config.expected_l_inf.has_value()) {
EXPECT_EQ(maybe_result->reason, config.expected_l_inf->reason);
EXPECT_EQ(maybe_result->type, config.expected_l_inf->type);
}
}
}
TEST(BoundNormsFromProblemStats, ExtractsBoundNorms) {
const auto qp_stats = ParseTextOrDie<QuadraticProgramStats>(R"pb(
objective_vector_l2_norm: 4.0
combined_bounds_l2_norm: 3.0
objective_vector_abs_max: 1.0
combined_bounds_max: 2.0
)pb");
const QuadraticProgramBoundNorms norms = BoundNormsFromProblemStats(qp_stats);
EXPECT_EQ(norms.l2_norm_primal_linear_objective, 4.0);
EXPECT_EQ(norms.l2_norm_constraint_bounds, 3.0);
EXPECT_EQ(norms.l_inf_norm_primal_linear_objective, 1.0);
EXPECT_EQ(norms.l_inf_norm_constraint_bounds, 2.0);
}
TEST(ComputeRelativeResiduals,
ComputesRelativeResidualsForZeroAbsoluteTolerance) {
ConvergenceInformation stats;
// If the absolute error tolerance is 0.0, the relative residuals are just the
// absolute residuals divided by the corresponding norms (the actual nonzero
// value of the relative error tolerance doesn't matter).
stats.set_primal_objective(10.0);
stats.set_dual_objective(5.0);
stats.set_l_inf_primal_residual(1.0);
stats.set_l2_primal_residual(1.0);
stats.set_l_inf_dual_residual(1.0);
stats.set_l2_dual_residual(1.0);
const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals(
/*eps_optimal_absolute=*/0.0, /*eps_optimal_relative=*/1.0e-6,
TestLpBoundNorms(), stats);
EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 1.0 / 12.0);
EXPECT_EQ(relative_info.relative_l2_primal_residual, 1.0 / std::sqrt(210.0));
EXPECT_EQ(relative_info.relative_l_inf_dual_residual, 1.0 / 5.5);
EXPECT_EQ(relative_info.relative_l2_dual_residual, 1.0 / sqrt(36.25));
// The relative optimality gap should just be the objective difference divided
// by the sum of absolute values (the actual nonzero value of the relative
// error tolerance doesn't matter).
EXPECT_EQ(relative_info.relative_optimality_gap, 5.0 / 15.0);
}
TEST(ComputeRelativeResiduals,
ComputesRelativeResidualsForZeroRelativeTolerance) {
ConvergenceInformation stats;
// If the relative error tolerance is 0.0, all of the relative residuals and
// the relative optimality gap should be 0.0, no matter what the absolute
// error tolerance is.
stats.set_primal_objective(10.0);
stats.set_dual_objective(5.0);
stats.set_l_inf_primal_residual(1.0);
stats.set_l2_primal_residual(1.0);
stats.set_l_inf_dual_residual(1.0);
stats.set_l2_dual_residual(1.0);
const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals(
/*eps_optimal_absolute=*/0.0, /*eps_optimal_relative=*/0.0,
TestLpBoundNorms(), stats);
EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 0.0);
EXPECT_EQ(relative_info.relative_l2_primal_residual, 0.0);
EXPECT_EQ(relative_info.relative_l_inf_dual_residual, 0.0);
EXPECT_EQ(relative_info.relative_l2_dual_residual, 0.0);
EXPECT_EQ(relative_info.relative_optimality_gap, 0.0);
}
TEST(ComputeRelativeResiduals,
ComputesCorrectRelativeResidualsForEqualTolerances) {
ConvergenceInformation stats;
// If the absolute error tolerance and relative error tolerance are equal (and
// nonzero), the relative residuals are the absolute residuals divided by 1.0
// plus the corresponding norms.
stats.set_primal_objective(10.0);
stats.set_dual_objective(5.0);
stats.set_l_inf_primal_residual(1.0);
stats.set_l2_primal_residual(1.0);
stats.set_l_inf_dual_residual(1.0);
stats.set_l2_dual_residual(1.0);
const RelativeConvergenceInformation relative_info = ComputeRelativeResiduals(
/*eps_optimal_absolute=*/1.0e-6, /*eps_optimal_relative=*/1.0e-6,
TestLpBoundNorms(), stats);
EXPECT_EQ(relative_info.relative_l_inf_primal_residual, 1.0 / (1.0 + 12.0));
EXPECT_EQ(relative_info.relative_l2_primal_residual,
1.0 / (1.0 + std::sqrt(210.0)));
EXPECT_EQ(relative_info.relative_l_inf_dual_residual, 1.0 / (1.0 + 5.5));
EXPECT_EQ(relative_info.relative_l2_dual_residual, 1.0 / (1.0 + sqrt(36.25)));
// The relative optimality gap should just be the objective difference divided
// by 1.0 + the sum of absolute values.
EXPECT_EQ(relative_info.relative_optimality_gap, 5.0 / (1.0 + 15.0));
}
} // namespace
} // namespace operations_research::pdlp

289
ortools/pdlp/test_util.cc Normal file
View File

@@ -0,0 +1,289 @@
// Copyright 2010-2021 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/test_util.h"
#include <cstdint>
#include <limits>
#include <string>
#include <vector>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "gmock/gmock.h"
#include "ortools/pdlp/quadratic_program.h"
namespace operations_research::pdlp {
using ::testing::ElementsAre;
constexpr double kInfinity = std::numeric_limits<double>::infinity();
QuadraticProgram TestLp() {
QuadraticProgram lp(4, 4);
lp.constraint_lower_bounds << 12, -kInfinity, -4, -1;
lp.constraint_upper_bounds << 12, 7, kInfinity, 1;
lp.variable_lower_bounds << -kInfinity, -2, -kInfinity, 2.5;
lp.variable_upper_bounds << kInfinity, kInfinity, 6, 3.5;
std::vector<Eigen::Triplet<double, int64_t>> triplets = {
{0, 0, 2}, {0, 1, 1}, {0, 2, 1}, {0, 3, 2}, {1, 0, 1},
{1, 2, 1}, {2, 0, 4}, {3, 2, 1.5}, {3, 3, -1}};
lp.constraint_matrix.setFromTriplets(triplets.begin(), triplets.end());
lp.objective_vector << 5.5, -2, -1, 1;
lp.objective_offset = -14;
return lp;
}
void VerifyTestLp(const QuadraticProgram& qp, bool maximize) {
const double objective_sign = maximize ? -1 : 1;
EXPECT_THAT(objective_sign * qp.objective_offset, testing::DoubleEq(-14));
EXPECT_THAT(objective_sign * qp.objective_vector,
ElementsAre(5.5, -2, -1, 1));
EXPECT_EQ(qp.objective_scaling_factor, objective_sign);
EXPECT_FALSE(qp.objective_matrix.has_value());
EXPECT_THAT(qp.variable_lower_bounds,
ElementsAre(-kInfinity, -2, -kInfinity, 2.5));
EXPECT_THAT(qp.variable_upper_bounds,
ElementsAre(kInfinity, kInfinity, 6, 3.5));
EXPECT_THAT(qp.constraint_lower_bounds, ElementsAre(12, -kInfinity, -4, -1));
EXPECT_THAT(qp.constraint_upper_bounds, ElementsAre(12, 7, kInfinity, 1));
EXPECT_THAT(ToDense(qp.constraint_matrix),
EigenArrayEq<double>(
{{2, 1, 1, 2}, {1, 0, 1, 0}, {4, 0, 0, 0}, {0, 0, 1.5, -1}}));
}
QuadraticProgram TinyLp() {
QuadraticProgram lp(4, 3);
lp.objective_offset = -14;
lp.objective_vector << 5, 2, 1, 1;
lp.constraint_lower_bounds << 12, 7, 1;
lp.constraint_upper_bounds << 12, kInfinity, kInfinity;
lp.variable_lower_bounds << 0, 0, 0, 0;
lp.variable_upper_bounds << 2, 4, 6, 3;
lp.constraint_matrix.coeffRef(0, 0) = 2.0;
lp.constraint_matrix.coeffRef(0, 1) = 1.0;
lp.constraint_matrix.coeffRef(0, 2) = 1.0;
lp.constraint_matrix.coeffRef(0, 3) = 2.0;
lp.constraint_matrix.coeffRef(1, 0) = 1.0;
lp.constraint_matrix.coeffRef(1, 2) = 1.0;
lp.constraint_matrix.coeffRef(2, 2) = 1.0;
lp.constraint_matrix.coeffRef(2, 3) = -1.0;
lp.constraint_matrix.makeCompressed();
return lp;
}
QuadraticProgram CorrelationClusteringLp() {
QuadraticProgram lp(6, 3);
lp.objective_offset = 4;
lp.objective_vector << -1.0, -1.0, 1.0, -1.0, 1.0, -1.0;
lp.constraint_lower_bounds << -1.0, -1.0, -1.0;
lp.constraint_upper_bounds << kInfinity, kInfinity, kInfinity;
lp.variable_lower_bounds << 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
lp.variable_upper_bounds << 1.0, 1.0, 1.0, 1.0, 1.0, 1.0;
lp.constraint_matrix.coeffRef(0, 1) = -1.0;
lp.constraint_matrix.coeffRef(0, 2) = 1.0;
lp.constraint_matrix.coeffRef(0, 5) = -1.0;
lp.constraint_matrix.coeffRef(1, 3) = -1.0;
lp.constraint_matrix.coeffRef(1, 4) = 1.0;
lp.constraint_matrix.coeffRef(1, 5) = -1.0;
lp.constraint_matrix.coeffRef(2, 0) = -1.0;
lp.constraint_matrix.coeffRef(2, 1) = -1.0;
lp.constraint_matrix.coeffRef(2, 3) = 1.0;
lp.constraint_matrix.makeCompressed();
return lp;
}
QuadraticProgram CorrelationClusteringStarLp() {
QuadraticProgram lp(6, 3);
lp.objective_offset = 3;
lp.objective_vector << -1.0, -1.0, -1.0, 1.0, 1.0, 1.0;
lp.constraint_lower_bounds << -1.0, -1.0, -1.0;
lp.constraint_upper_bounds << kInfinity, kInfinity, kInfinity;
lp.variable_lower_bounds << 0.0, 0.0, 0.0, 0.0, 0.0, 0.0;
lp.variable_upper_bounds << 1.0, 1.0, 1.0, 1.0, 1.0, 1.0;
lp.constraint_matrix.coeffRef(0, 0) = -1.0;
lp.constraint_matrix.coeffRef(0, 1) = -1.0;
lp.constraint_matrix.coeffRef(0, 3) = 1.0;
lp.constraint_matrix.coeffRef(1, 0) = -1.0;
lp.constraint_matrix.coeffRef(1, 2) = -1.0;
lp.constraint_matrix.coeffRef(1, 4) = 1.0;
lp.constraint_matrix.coeffRef(2, 1) = -1.0;
lp.constraint_matrix.coeffRef(2, 2) = -1.0;
lp.constraint_matrix.coeffRef(2, 5) = 1.0;
lp.constraint_matrix.makeCompressed();
return lp;
}
namespace {
Eigen::DiagonalMatrix<double, Eigen::Dynamic> ConstructDiagonal(
const std::vector<double>& vec) {
Eigen::DiagonalMatrix<double, Eigen::Dynamic> diag;
diag.resize(vec.size());
for (int i = 0; i < vec.size(); ++i) {
diag.diagonal()[i] = vec[i];
}
return diag;
}
} // namespace
QuadraticProgram TestDiagonalQp1() {
QuadraticProgram qp(2, 1);
qp.constraint_lower_bounds << -kInfinity;
qp.constraint_upper_bounds << 1;
qp.variable_lower_bounds << 1, -2;
qp.variable_upper_bounds << 2, 4;
qp.objective_vector << -1, -1;
qp.objective_offset = 5;
std::vector<Eigen::Triplet<double, int64_t>> constraint_triplets = {
{0, 0, 1}, {0, 1, 1}};
qp.constraint_matrix.setFromTriplets(constraint_triplets.begin(),
constraint_triplets.end());
qp.objective_matrix = ConstructDiagonal({4.0, 1.0});
return qp;
}
QuadraticProgram TestDiagonalQp2() {
QuadraticProgram qp(2, 1);
qp.constraint_lower_bounds << 2;
qp.constraint_upper_bounds << 2;
qp.variable_lower_bounds << 0, 0;
qp.variable_upper_bounds << kInfinity, kInfinity;
qp.objective_vector << -3, -1;
qp.objective_offset = 0;
std::vector<Eigen::Triplet<double, int64_t>> constraint_triplets = {
{0, 0, 1}, {0, 1, -1}};
qp.constraint_matrix.setFromTriplets(constraint_triplets.begin(),
constraint_triplets.end());
qp.objective_matrix = ConstructDiagonal({1.0, 1.0});
return qp;
}
QuadraticProgram TestDiagonalQp3() {
QuadraticProgram qp(3, 2);
qp.constraint_lower_bounds << 1, 4;
qp.constraint_upper_bounds << 1, 4;
qp.variable_lower_bounds << 0, 0, 0;
qp.variable_upper_bounds << kInfinity, kInfinity, kInfinity;
qp.objective_vector << 1, 0, -1;
qp.objective_offset = 0;
std::vector<Eigen::Triplet<double, int64_t>> constraint_triplets = {
{0, 0, 1}, {0, 2, -1}, {1, 0, 2}};
qp.constraint_matrix.setFromTriplets(constraint_triplets.begin(),
constraint_triplets.end());
qp.objective_matrix = ConstructDiagonal({0.0, 1.0, 2.0});
return qp;
}
QuadraticProgram SmallInvalidProblemLp() {
QuadraticProgram lp(2, 1);
lp.constraint_matrix.coeffRef(0, 0) = 1.0;
lp.constraint_matrix.coeffRef(0, 1) = -1.0;
lp.constraint_lower_bounds << 2.0;
lp.constraint_upper_bounds << 1.0;
lp.variable_lower_bounds << 0.0, 0.0;
lp.variable_upper_bounds << kInfinity, kInfinity;
lp.constraint_matrix.makeCompressed();
lp.objective_vector << 1.0, 1.0;
return lp;
}
QuadraticProgram SmallInconsistentVariableBoundsLp() {
QuadraticProgram lp(2, 1);
lp.constraint_matrix.coeffRef(0, 0) = 1.0;
lp.constraint_matrix.coeffRef(0, 1) = -1.0;
lp.constraint_matrix.makeCompressed();
lp.constraint_lower_bounds << -kInfinity;
lp.constraint_upper_bounds << 1.0;
lp.variable_lower_bounds << 2.0, 0.0;
lp.variable_upper_bounds << 1.0, kInfinity;
lp.objective_vector << 1.0, 1.0;
return lp;
}
QuadraticProgram SmallPrimalInfeasibleLp() {
QuadraticProgram lp(2, 2);
lp.constraint_matrix.coeffRef(0, 0) = 1.0;
lp.constraint_matrix.coeffRef(0, 1) = -1.0;
lp.constraint_matrix.coeffRef(1, 0) = -1.0;
lp.constraint_matrix.coeffRef(1, 1) = 1.0;
lp.constraint_lower_bounds << -kInfinity, -kInfinity;
lp.variable_lower_bounds << 0.0, 0.0;
lp.variable_upper_bounds << kInfinity, kInfinity;
lp.constraint_matrix.makeCompressed();
lp.constraint_upper_bounds << 1.0, -2.0;
lp.objective_vector << 1.0, 1.0;
return lp;
}
QuadraticProgram SmallDualInfeasibleLp() {
QuadraticProgram lp = SmallPrimalInfeasibleLp();
lp.constraint_upper_bounds(1) = 2.0;
lp.objective_vector *= -1.0;
return lp;
}
QuadraticProgram SmallPrimalDualInfeasibleLp() {
QuadraticProgram lp = SmallPrimalInfeasibleLp();
lp.objective_vector *= -1.0;
return lp;
}
QuadraticProgram SmallInitializationLp() {
QuadraticProgram lp(2, 2);
lp.constraint_matrix.coeffRef(0, 0) = 1.0;
lp.constraint_matrix.coeffRef(0, 1) = 1.0;
lp.constraint_matrix.coeffRef(1, 0) = 1.0;
lp.constraint_matrix.coeffRef(1, 1) = 2.0;
lp.constraint_lower_bounds << -kInfinity, -kInfinity;
lp.constraint_upper_bounds << 2.0, 2.0;
lp.variable_lower_bounds << 0.5, 0.5;
lp.variable_upper_bounds << 2.0, 2.0;
lp.constraint_matrix.makeCompressed();
lp.objective_vector << -4.0, 0.0;
return lp;
}
QuadraticProgram LpWithoutConstraints() {
QuadraticProgram lp(2, 0);
lp.variable_lower_bounds << 0.0, -kInfinity;
lp.variable_upper_bounds << kInfinity, 0.0;
lp.objective_vector << 4.0, 0.0;
return lp;
}
void VerifyTestQp(const QuadraticProgram& qp, bool maximize) {
const double objective_sign = maximize ? -1 : 1;
EXPECT_EQ(qp.objective_scaling_factor, objective_sign);
EXPECT_THAT(objective_sign * qp.objective_offset, testing::DoubleEq(5));
EXPECT_THAT(objective_sign * qp.objective_vector, ElementsAre(-1, -1));
ASSERT_TRUE(qp.objective_matrix.has_value());
EXPECT_THAT(objective_sign * qp.objective_matrix->diagonal(),
EigenArrayEq<double>({4, 1}));
EXPECT_THAT(qp.variable_lower_bounds, ElementsAre(1, -2));
EXPECT_THAT(qp.variable_upper_bounds, ElementsAre(2, 4));
EXPECT_THAT(qp.constraint_lower_bounds, ElementsAre(-kInfinity));
EXPECT_THAT(qp.constraint_upper_bounds, ElementsAre(1));
EXPECT_THAT(ToDense(qp.constraint_matrix), EigenArrayEq<double>({{1, 1}}));
}
::Eigen::ArrayXXd ToDense(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& sparse_mat) {
return ::Eigen::ArrayXXd(::Eigen::MatrixXd(sparse_mat));
}
} // namespace operations_research::pdlp

417
ortools/pdlp/test_util.h Normal file
View File

@@ -0,0 +1,417 @@
// Copyright 2010-2021 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.
#ifndef PDLP_TEST_UTIL_H_
#define PDLP_TEST_UTIL_H_
#include <cstdint>
#include <sstream>
#include <string>
#include <type_traits>
#include "Eigen/Core"
#include "Eigen/SparseCore"
#include "absl/types/span.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/base/logging.h"
#include "ortools/pdlp/quadratic_program.h"
namespace operations_research::pdlp {
// A small LP with all 4 patterns of which lower and upper bounds on the
// constraints are finite and similarly for the variables.
// min 5.5 x_0 - 2 x_1 - x_2 + x_3 - 14 s.t.
// 2 x_0 + x_1 + x_2 + 2 x_3 = 12
// x_0 + x_2 <= 7
// 4 x_0 >= -4
// -1 <= 1.5 x_2 - x_3 <= 1
// -infinity <= x_0 <= infinity
// -2 <= x_1 <= infinity
// -infinity <= x_2 <= 6
// 2.5 <= x_3 <= 3.5
//
// Optimal solutions:
// Primal: [-1, 8, 1, 2.5]
// Dual: [-2, 0, 2.375, 2.0/3]
// Value: -5.5 - 16 -1 + 2.5 - 14 = -34
QuadraticProgram TestLp();
// Verifies that the given QuadraticProgram equals TestLp().
void VerifyTestLp(const QuadraticProgram& qp, bool maximize = false);
// Returns a small test LP.
// The LP:
// min 5 x_1 + 2 x_2 + x_3 + x_4 - 14 s.t.
// 2 x_1 + x_2 + x_3 + 2 x_4 = 12
// x_1 + x_3 >= 7
// x_3 - x_4 >= 1
// 0 <= x_1 <= 2
// 0 <= x_2 <= 4
// 0 <= x_3 <= 6
// 0 <= x_4 <= 3
//
// Optimum solutions:
// Primal: x_1 = 1, x_2 = 0, x_3 = 6, x_4 = 2. Value: 5 + 0 + 6 + 2 - 14 = -1.
// Dual: [0.5, 4.0, 0.0] Value: 6 + 28 - 3.5*6 - 14 = -1
// Reduced costs: [0.0, 1.5, -3.5, 0.0]
QuadraticProgram TinyLp();
// Returns a correlation clustering LP.
// This is the LP for minimizing disagreements for correlation clustering for
// the 4-vertex graph
// 1 - 3 - 4
// | /
// 2
// In integer solutions x_ij is 1 if i and j are in the same cluster and 0
// otherwise. The 6 variables are in the order
// x_12, x_13, x_14, x_23, x_24, x_34.
// For any distinct i,j,k there's a triangle inequality
// (1-x_ik) <= (1-x_ij) + (1-x_jk)
// i.e.
// -x_ij - x_jk + x_ik >= -1.
// For brevity we only include 3 out of the 12 possible triangle inequalities:
// two needed in the optimal solution and 1 other.
//
// Optimal solutions:
// Primal: [1, 1, 0, 1, 0, 0]
// Dual: Multiple.
// Value: 1.
QuadraticProgram CorrelationClusteringLp();
// Returns another 4-vertex correlation clustering LP.
//
// The variables are x_12, x_13, x_14, x_23, x_24, and x_34.
// This time the graph is a star centered at vertex 1.
// Only the three triangle inequalities that are needed are included."""
// Optimal solutions:
// Primal: [0.5, 0.5, 0.5, 0.0, 0.0, 0.0]
// Dual: [0.5, 0.5, 0.5]
// Value: 1.5
QuadraticProgram CorrelationClusteringStarLp();
// Returns a small test QP.
// min 2 x_0^2 + 0.5 x_1^2 - x_0 - x_1 + 5 s.t.
// x_0 + x_1 <= 1
// 1 <= x_0 <= 2
// -2 <= x_1 <= 4
//
// Optimal solutions:
// Primal: [1.0, 0.0]
// Dual: [-1.0]
// Reduced costs: [4.0, 0.0]
// Value: 2 - 1 + 5 = 6
QuadraticProgram TestDiagonalQp1();
// Returns a small diagonal QP.
// min 0.5 x_0^2 + 0.5 x_1^2 - 3 x_0 - x_1 s.t.
// x_0 - x_1 = 2
// x_0 >= 0
// x_1 >= 0
// Optimal solutions:
// Primal: [3, 1]
// Dual: [0]
// Value: 0
// Reduced costs: [0, 0]
QuadraticProgram TestDiagonalQp2();
// Returns a small diagonal QP.
// min 0.5 x_1^2 + x_2^2 + x_0 - x_2 s.t.
// x_0 - x_2 = 1
// 2x_0 = 4
// x_0, x_1, x_2 >= 0
// Optimal solutions:
// Primal: [2, 0, 1]
// Dual: [-1, 1]
// Value: 3
// Reduced costs: [0, 0, 0]
QuadraticProgram TestDiagonalQp3();
// Returns a small invalid LP.
// min x_0 + x_1 s.t.
// 2.0 <= x_0 - x_1 <= 1.0
// 0.0 <= x_0
// 0.0 <= x_1
QuadraticProgram SmallInvalidProblemLp();
// Returns a small LP that's invalid due to inconsistent variable bounds.
// min x_0 + x_1 s.t.
// x_0 - x_1 <= 1.0
// 2.0 <= x_0 <= 1.0
// 0.0 <= x_1
QuadraticProgram SmallInconsistentVariableBoundsLp();
// Returns a small test LP with infeasible primal.
// min x_0 + x_1 s.t.
// x_0 - x_1 <= 1.0
// -x_0 + x_1 <= -2.0
// 0.0 <= x_0
// 0.0 <= x_1
QuadraticProgram SmallPrimalInfeasibleLp();
// Returns a small test LP with infeasible dual.
// min - x_0 - x_1 s.t.
// x_0 - x_1 <= 1.0
// -x_0 + x_1 <= 2.0
// 0.0 <= x_0
// 0.0 <= x_1
// This is the SmallPrimalInfeasibleLp with the objective vector negated and
// with the second constraint changed to make it feasible.
QuadraticProgram SmallDualInfeasibleLp();
// Returns a small test LP with infeasible primal and dual.
// min - x_0 - x_1 s.t.
// x_0 - x_1 <= 1.0
// -x_0 + x_1 <= -2.0
// 0.0 <= x_0
// 0.0 <= x_1
// This is just the SmallPrimalInfeasibleLp with the objective vector
// negated.
QuadraticProgram SmallPrimalDualInfeasibleLp();
// This is a small lp for which optimality conditions are met by x=(0, 0), y=(0,
// 0) if one doesn't check that x satisfies the variable bounds. Analogously,
// the assignment x=(1, 0), y = -(1, 1) also satisfies the optimality conditions
// if one doesn't check dual variable bounds.
// min -4 x_0 s.t.
// x_0 + x_1 <= 2.0
// x_0 + 2x_1 <= 2.0
// 0.5 <= x_0 <= 2.0
// 0.5 <= x_1 <= 2.0
QuadraticProgram SmallInitializationLp();
// This is a small LP with 2 variables and zero constraints (excluding variable
// bounds), resulting in an empty constraint matrix (zero rows) and empty lower
// and upper constraint bounds.
// min 4 x_0 s.t.
// 0 <= x_0
// x_1 <= 0
QuadraticProgram LpWithoutConstraints();
// Verifies that the given QuadraticProgram equals TestQp().
void VerifyTestQp(const QuadraticProgram& qp, bool maximize = false);
// Converts a sparse matrix into a dense matrix in the format suitable for
// the matcher EigenArrayEq. Example usage:
// EXPECT_THAT(ToDense(sparse_mat), EigenArrayEq<double>({{1, 1}}));
::Eigen::ArrayXXd ToDense(
const Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& sparse_mat);
// gMock matchers for Eigen.
namespace internal {
MATCHER_P(TupleIsNear, tolerance, "is near") {
return std::abs(std::get<0>(arg) - std::get<1>(arg)) <= tolerance;
}
MATCHER(TupleFloatEq, "is almost equal to") {
testing::Matcher<float> matcher = testing::FloatEq(std::get<1>(arg));
return matcher.Matches(std::get<0>(arg));
}
// Convert nested Span to a 2D Eigen Array. Spans are implicitly
// constructable from initializer_lists and vectors, so this conversion is used
// in EigenArrayNear and EigenArrayEq to support syntaxes like
// EXPECT_THAT(array2d, EigenArrayNear<int>({{1, 2}, {3, 4}}, tolerance);
// This conversion creates a copy of the slice data, so it is safe to use the
// result even after the original slices vanish.
template <typename T>
Eigen::Array<T, Eigen::Dynamic, Eigen::Dynamic> EigenArray2DFromNestedSpans(
absl::Span<const absl::Span<const T>> rows) {
Eigen::Array<T, Eigen::Dynamic, Eigen::Dynamic> result(0, rows.size());
if (!rows.empty()) {
result.resize(rows.size(), rows[0].size());
}
for (int i = 0; i < rows.size(); ++i) {
CHECK_EQ(rows[0].size(), rows[i].size());
result.row(i) = Eigen::Map<const Eigen::Array<T, Eigen::Dynamic, 1>>(
&rows[i][0], rows[i].size());
}
return result;
}
// Get a matcher's description as a string. To produce the description for
// EigenEach(inner_matcher), this function is called to get the description of
// inner_matcher.
template <typename LhsType>
std::string GetMatcherDescriptionAsString(
const testing::Matcher<LhsType>& matcher, bool negation) {
std::stringstream ss;
if (negation) {
matcher.DescribeNegationTo(&ss);
} else {
matcher.DescribeTo(&ss);
}
return ss.str();
}
} // namespace internal
// Defines a gMock matcher that tests whether two numeric arrays are
// approximately equal in the sense of maximum absolute difference. The element
// value type may be float, double, or integral types.
//
// Example:
// vector<double> output = ComputeVector();
// vector<double> expected({-1.5333, sqrt(2), M_PI});
// EXPECT_THAT(output, FloatArrayNear(expected, 1e-3));
template <typename ContainerType>
decltype(testing::Pointwise(internal::TupleIsNear(0.0), ContainerType()))
FloatArrayNear(const ContainerType& container, double tolerance) {
return testing::Pointwise(internal::TupleIsNear(tolerance), container);
}
// Defines a gMock matcher acting as an elementwise version of FloatEq() for
// arrays of real floating point types. It tests whether two arrays are
// pointwise equal within 4 units in the last place (ULP) in float precision
// [http://en.wikipedia.org/wiki/Unit_in_the_last_place]. Roughly, 4 ULPs is
// 2^-21 times the absolute value, or 0.00005%. Exceptionally, zero matches
// values with magnitude less than 5.6e-45 (2^-147), infinities match infinities
// of the same sign, and NaNs don't match anything.
//
// Example:
// vector<float> output = ComputeVector();
// vector<float> expected({-1.5333, sqrt(2), M_PI});
// EXPECT_THAT(output, FloatArrayEq(expected));
template <typename ContainerType>
decltype(testing::Pointwise(internal::TupleFloatEq(), ContainerType()))
FloatArrayEq(const ContainerType& container) {
return testing::Pointwise(internal::TupleFloatEq(), container);
}
// Call .eval() on input and convert it to a column major representation.
template <typename EigenType>
Eigen::Array<typename EigenType::Scalar, Eigen::Dynamic, Eigen::Dynamic,
Eigen::ColMajor>
EvalAsColMajorEigenArray(const EigenType& input) {
return input.eval();
}
// Wrap a column major Eigen Array as a Span.
template <typename Scalar>
absl::Span<const Scalar> EigenArrayAsSpan(
const Eigen::Array<Scalar, Eigen::Dynamic, Eigen::Dynamic, Eigen::ColMajor>&
array) {
return absl::Span<const Scalar>(array.data(), array.size());
}
// Gmock matcher to test whether all elements in an array match expected_array
// within the specified tolerance, and print a detailed error message pointing
// to the first mismatched element if they do not. Essentially an elementwise
// version of testing::DoubleNear for Eigen arrays.
//
// Example:
// Eigen::ArrayXf expected = ...
// EXPECT_THAT(actual_arrayxf, EigenArrayNear(expected, 1e-5));
MATCHER_P2(EigenArrayNear, expected_array, tolerance,
"array is near " + testing::PrintToString(expected_array) +
" within tolerance " + testing::PrintToString(tolerance)) {
if (arg.rows() != expected_array.rows() ||
arg.cols() != expected_array.cols()) {
*result_listener << "where shape (" << expected_array.rows() << ", "
<< expected_array.cols() << ") doesn't match ("
<< arg.rows() << ", " << arg.cols() << ")";
return false;
}
// Call .eval() to allow callers to pass in Eigen expressions and possibly
// noncontiguous objects, e.g. Eigen::ArrayXf::Zero(10) or Map with a stride.
// Arrays are represented in column major order for consistent comparison.
auto realized_expected_array = EvalAsColMajorEigenArray(expected_array);
auto realized_actual_array = EvalAsColMajorEigenArray(arg);
return ExplainMatchResult(
FloatArrayNear(EigenArrayAsSpan(realized_expected_array), tolerance),
EigenArrayAsSpan(realized_actual_array), result_listener);
}
// Gmock matcher to test whether all elements in an array match expected_array
// within 4 units of least precision (ULP) in float precision. Essentially an
// elementwise version of testing::FloatEq for Eigen arrays.
//
// Example:
// Eigen::ArrayXf expected = ...
// EXPECT_THAT(actual_arrayxf, EigenArrayEq(expected));
MATCHER_P(EigenArrayEq, expected_array,
"array is almost equal to " +
testing::PrintToString(expected_array)) {
if (arg.rows() != expected_array.rows() ||
arg.cols() != expected_array.cols()) {
*result_listener << "where shape (" << expected_array.rows() << ", "
<< expected_array.cols() << ") doesn't match ("
<< arg.rows() << ", " << arg.cols() << ")";
return false;
}
// Call .eval() to allow callers to pass in Eigen expressions and possibly
// noncontiguous objects, e.g. Eigen::ArrayXf::Zero(10) or Map with a stride.
// Arrays are represented in column major order for consistent comparison.
auto realized_expected_array = EvalAsColMajorEigenArray(expected_array);
auto realized_actual_array = EvalAsColMajorEigenArray(arg);
return ExplainMatchResult(
FloatArrayEq(EigenArrayAsSpan(realized_expected_array)),
EigenArrayAsSpan(realized_actual_array), result_listener);
}
// The next few functions are syntactic sugar for EigenArrayNear and
// EigenArrayEq to allow callers to pass in non-Eigen types that can be
// statically initialized like (nested in the 2D case) initializer_lists, or
// vectors, etc. For example this specialization lets one make calls inlining
// expected_array like:
// EXPECT_THAT(array1d, EigenArrayNear<float>({0.1, 0.2}, tolerance));
// or in the 2D case:
// EXPECT_THAT(array2d, EigenArrayNear<int>({{1, 2}, {3, 4}}, tolerance);
template <typename T>
EigenArrayNearMatcherP2<Eigen::Array<T, Eigen::Dynamic, 1>, double>
EigenArrayNear(absl::Span<const T> data, double tolerance) {
Eigen::Array<T, Eigen::Dynamic, 1> temp_array =
Eigen::Map<const Eigen::Array<T, Eigen::Dynamic, 1>>(&data[0],
data.size());
return EigenArrayNear(temp_array, tolerance);
}
template <typename T>
EigenArrayNearMatcherP2<Eigen::Array<T, Eigen::Dynamic, Eigen::Dynamic>, double>
EigenArrayNear(absl::Span<const absl::Span<const T>> rows, double tolerance) {
return EigenArrayNear(internal::EigenArray2DFromNestedSpans(rows), tolerance);
}
template <typename T>
EigenArrayEqMatcherP<Eigen::Array<T, Eigen::Dynamic, 1>> EigenArrayEq(
absl::Span<const T> data) {
Eigen::Array<T, Eigen::Dynamic, 1> temp_array =
Eigen::Map<const Eigen::Array<T, Eigen::Dynamic, 1>>(&data[0],
data.size());
return EigenArrayEq(temp_array);
}
template <typename T>
EigenArrayEqMatcherP<Eigen::Array<T, Eigen::Dynamic, Eigen::Dynamic>>
EigenArrayEq(absl::Span<const absl::Span<const T>> rows) {
return EigenArrayEq(internal::EigenArray2DFromNestedSpans(rows));
}
} // namespace operations_research::pdlp
namespace Eigen {
// Pretty prints an Eigen::Array on a gunit test failures. See
// https://google.github.io/googletest/advanced.html#teaching-googletest-how-to-print-your-values
template <typename Scalar, int Rows, int Cols, int Options, int MaxRows,
int MaxCols>
void PrintTo(const Array<Scalar, Rows, Cols, Options, MaxRows, MaxCols>& array,
std::ostream* os) {
IOFormat format(StreamPrecision, 0, ", ", ",\n", "[", "]", "[", "]");
*os << "\n" << array.format(format);
}
} // namespace Eigen
#endif // PDLP_TEST_UTIL_H_

View File

@@ -0,0 +1,331 @@
// Copyright 2010-2021 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/test_util.h"
#include <cmath>
#include <deque>
#include <limits>
#include <list>
#include <vector>
#include "Eigen/Core"
#include "absl/types/span.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "ortools/base/logging.h"
namespace operations_research::pdlp {
namespace {
using ::absl::Span;
using ::Eigen::ColMajor;
using ::Eigen::Dynamic;
using ::Eigen::RowMajor;
using ::testing::Matcher;
using ::testing::Not;
TEST(FloatArrayNearTest, TypicalUse) {
std::vector<double> test_vector({0.998, -1.414, 3.142});
std::vector<double> reference_vector({1.0, -M_SQRT2, M_PI});
EXPECT_THAT(test_vector, FloatArrayNear(reference_vector, 1e-2));
EXPECT_THAT(test_vector, Not(FloatArrayNear(reference_vector, 1e-4)));
}
template <typename ContainerType>
class FloatArrayNearContainerTypeTest : public testing::Test {};
using TestContainerTypes = testing::Types<std::vector<float>, std::deque<float>,
std::list<float>, Span<const float>>;
TYPED_TEST_SUITE(FloatArrayNearContainerTypeTest, TestContainerTypes);
TYPED_TEST(FloatArrayNearContainerTypeTest, MatchesApproximately) {
using ContainerType = TypeParam;
auto test_values = {0.505f, 1.0f, -0.992f, 1.995f};
ContainerType test_container(test_values);
auto reference_values = {0.5f, 1.0f, -1.0f, 2.0f};
ContainerType reference_container(reference_values);
const Matcher<ContainerType> m1 = FloatArrayNear(reference_container, 1e-2);
EXPECT_TRUE(m1.Matches(test_container));
const Matcher<ContainerType> m2 = FloatArrayNear(reference_container, 1e-3);
EXPECT_FALSE(m2.Matches(test_container));
}
TYPED_TEST(FloatArrayNearContainerTypeTest, DoesNotMatchWrongSize) {
using ContainerType = TypeParam;
EXPECT_THAT(ContainerType({1.0f, 2.0f}),
Not(FloatArrayNear(ContainerType({1.0f, 2.0f, 3.0f}), 1e-2)));
}
TYPED_TEST(FloatArrayNearContainerTypeTest, DoesNotMatchWrongOrder) {
using ContainerType = TypeParam;
EXPECT_THAT(ContainerType({1.0f, 3.0f, 2.0f}),
Not(FloatArrayNear(ContainerType({1.0f, 2.0f, 3.0f}), 1e-2)));
}
TYPED_TEST(FloatArrayNearContainerTypeTest, DoesNotMatchNaNs) {
using ContainerType = TypeParam;
auto test_values = {1.0f, std::numeric_limits<float>::quiet_NaN()};
ContainerType test_container(test_values);
EXPECT_THAT(test_container,
Not(FloatArrayNear(ContainerType({1.0f, 2.0f}), 1e0)));
EXPECT_THAT(test_container, Not(FloatArrayNear(test_container, 1e0)));
}
TEST(FloatArrayNearTest, WithIntegerElements) {
std::vector<int> test_vector({505, 1000, -992, 1990});
std::vector<int> reference_vector({500, 1000, -1000, 2000});
const Matcher<std::vector<int>> m1 = FloatArrayNear(reference_vector, 10);
EXPECT_TRUE(m1.Matches(test_vector));
const Matcher<std::vector<int>> m2 = FloatArrayNear(reference_vector, 1);
EXPECT_FALSE(m2.Matches(test_vector));
}
TEST(FloatArrayEqTest, TypicalUse) {
std::vector<float> reference_vector({1e6, -M_SQRT2, M_PI});
// Values are within 4 ULPs.
std::vector<float> test_vector({1e6 + 0.25, -1.41421323, 3.14159262});
EXPECT_THAT(test_vector, FloatArrayEq(reference_vector));
// Create a difference of 5 ULPs in the first element.
test_vector[0] = 1e6 + 0.3125;
EXPECT_THAT(test_vector, Not(FloatArrayEq(reference_vector)));
}
template <typename ContainerType>
class FloatArrayEqContainerTypeTest : public testing::Test {};
TYPED_TEST_SUITE(FloatArrayEqContainerTypeTest, TestContainerTypes);
TYPED_TEST(FloatArrayEqContainerTypeTest, MatchesApproximately) {
using ContainerType = TypeParam;
auto reference_values = {-1e6f, 0.0f, 1.0f};
ContainerType reference_container(reference_values);
const Matcher<ContainerType> m = FloatArrayEq(reference_container);
EXPECT_TRUE(m.Matches(reference_container));
EXPECT_TRUE(m.Matches(ContainerType({-1e6 + 0.25, 5e-45, 1.0000002})));
EXPECT_TRUE(m.Matches(ContainerType({-1e6 - 0.25, -5e-45, 0.9999998})));
EXPECT_FALSE(m.Matches(ContainerType({-1e6 + 0.3125, 0.0, 1.0})));
EXPECT_FALSE(m.Matches(ContainerType({-1e6, 1e-44, 1.0})));
EXPECT_FALSE(m.Matches(ContainerType({-1e6, 0.0, 1.0000006})));
}
TYPED_TEST(FloatArrayEqContainerTypeTest, DoesNotMatchWrongSize) {
using ContainerType = TypeParam;
EXPECT_THAT(ContainerType({1.0f, 2.0f}),
Not(FloatArrayEq(ContainerType({1.0f, 2.0f, 3.0f}))));
}
TYPED_TEST(FloatArrayEqContainerTypeTest, DoesNotMatchWrongOrder) {
using ContainerType = TypeParam;
EXPECT_THAT(ContainerType({1.0f, 3.0f, 2.0f}),
Not(FloatArrayEq(ContainerType({1.0f, 2.0f, 3.0f}))));
}
TYPED_TEST(FloatArrayEqContainerTypeTest, DoesNotMatchNaNs) {
using ContainerType = TypeParam;
auto reference_values = {1.0f, std::numeric_limits<float>::quiet_NaN()};
ContainerType reference_container(reference_values);
const Matcher<ContainerType> m = FloatArrayEq(reference_container);
EXPECT_FALSE(m.Matches(reference_container));
EXPECT_FALSE(m.Matches(ContainerType({1.0f, 2.0f})));
}
TYPED_TEST(FloatArrayEqContainerTypeTest, HandlesInfinities) {
using ContainerType = TypeParam;
auto reference_values = {1.0f, std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity()};
ContainerType reference_container(reference_values);
const Matcher<ContainerType> m = FloatArrayEq(reference_container);
EXPECT_TRUE(m.Matches(reference_container));
EXPECT_FALSE(m.Matches(ContainerType({1.0f, 2.0f, 3.0f})));
}
static const double kEps = 1e-6;
TEST(EigenArrayNearTest, ArrayXd) {
const Eigen::ArrayXd expected = Eigen::ArrayXd::Random(4);
Eigen::ArrayXd actual = expected;
EXPECT_THAT(actual, EigenArrayNear(expected, kEps));
EXPECT_THAT(actual, EigenArrayNear(expected, 1e-100));
actual += 100;
EXPECT_THAT(actual, Not(EigenArrayNear(expected, kEps)));
// Wrong shape.
actual.resize(2);
EXPECT_THAT(actual, Not(EigenArrayNear(expected, kEps)));
}
TEST(EigenArrayNearTest, ArrayXdInlinedValues) {
Eigen::ArrayXd actual(3);
actual << 1.0, 2.0, 3.0;
EXPECT_THAT(actual, EigenArrayNear<double>({1.0, 2.0, 3.0}, kEps));
EXPECT_THAT(actual,
EigenArrayNear<double>({1.0, 2.0 + 0.5 * kEps, 3.0}, kEps));
EXPECT_THAT(actual, Not(EigenArrayNear<double>({1.0, 2.0, 5.0}, kEps)));
// Wrong shape.
EXPECT_THAT(actual, Not(EigenArrayNear<double>({1.0, 2.0}, kEps)));
}
TEST(EigenArrayNearTest, EmptyArrayX) {
const Eigen::ArrayXi empty;
EXPECT_THAT(empty, EigenArrayNear(empty, kEps));
// Can pass in an Eigen expression type.
EXPECT_THAT(empty, EigenArrayNear(Eigen::ArrayXi(), kEps));
EXPECT_THAT(empty, Not(EigenArrayNear<int>({1, 2}, kEps)));
EXPECT_THAT(empty, Not(EigenArrayNear(Eigen::ArrayXi::Zero(3), kEps)));
}
TEST(EigenArrayNearTest, ArrayXXf) {
const Eigen::ArrayXXf expected = Eigen::ArrayXXf::Random(4, 5);
Eigen::ArrayXXf actual = expected;
EXPECT_THAT(actual, EigenArrayNear(expected, kEps));
EXPECT_THAT(actual, EigenArrayNear(expected, 1e-100));
actual.row(2) += 100;
EXPECT_THAT(actual, Not(EigenArrayNear(expected, kEps)));
// Wrong shape.
EXPECT_THAT(expected, Not(EigenArrayNear(expected.transpose(), kEps)));
actual.resize(4, 3);
EXPECT_THAT(actual, Not(EigenArrayNear(expected, kEps)));
// Expression type.
actual.resize(3, 2);
actual.col(0) << 1.0, 2.0, 3.0;
actual.col(1) << 4.0, 5.0, 6.0;
std::vector<float> expected_vector({1.0, 2.0, 3.0, 4.0, 5.0, 6.0});
EXPECT_THAT(actual, EigenArrayNear(Eigen::Map<Eigen::ArrayXXf>(
&expected_vector[0], /*rows=*/3,
/*cols=*/2),
kEps));
// Wrong shape.
EXPECT_THAT(actual, Not(EigenArrayNear(Eigen::Map<Eigen::ArrayXXf>(
&expected_vector[0], /*rows=*/3,
/*cols=*/1),
kEps)));
}
TEST(EigenArrayNearTest, DifferentMajor) {
Eigen::Array<float, Dynamic, Dynamic, ColMajor> col_major(2, 3);
col_major << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0;
Eigen::Array<float, Dynamic, Dynamic, RowMajor> row_major(2, 3);
row_major << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0;
CHECK_EQ(col_major(1, 0), row_major(1, 0));
EXPECT_THAT(row_major, EigenArrayNear(col_major, 0.0));
EXPECT_THAT(row_major, EigenArrayNear<float>({{1.0, 2.0, 3.0}, //
{4.0, 5.0, 6.0}},
0.0));
EXPECT_THAT(col_major, EigenArrayNear(row_major, 0.0));
EXPECT_THAT(col_major, EigenArrayNear<float>({{1.0, 2.0, 3.0}, //
{4.0, 5.0, 6.0}},
0.0));
}
TEST(EigenArrayNearTest, ArrayXXfInlinedValues) {
Eigen::ArrayXXf actual(2, 3);
actual.row(0) << 1.0, 2.0, 3.0;
actual.row(1) << 4.0, -5.0, -6.0;
EXPECT_THAT(actual, EigenArrayNear<float>({{1.0, 2.0, 3.0}, //
{4.0, -5.0, -6.0}},
kEps));
EXPECT_THAT(actual, EigenArrayNear<float>(
{{1.0, 2.0, 3.0},
{4.0, -5.0, static_cast<float>(-6.0 - 0.9 * kEps)}},
kEps));
EXPECT_THAT(actual, Not(EigenArrayNear<float>({{1.0, 2.0, 3.0}, //
{4.0, -5.0, -8.0}},
kEps)));
// Wrong shape.
EXPECT_THAT(actual, Not(EigenArrayNear<float>({{1.0, 2.0, 3.0}}, kEps)));
}
TEST(EigenArrayEqTest, ArrayXd) {
const Eigen::ArrayXd expected = Eigen::ArrayXd::Random(4);
Eigen::ArrayXd actual = expected;
EXPECT_THAT(actual, EigenArrayEq(expected));
actual += 100;
EXPECT_THAT(actual, Not(EigenArrayEq(expected)));
// Wrong shape.
actual.resize(2);
EXPECT_THAT(actual, Not(EigenArrayEq(expected)));
}
TEST(EigenArrayEqTest, ArrayXdInlinedValues) {
Eigen::ArrayXd actual(3);
actual << 1.0, 2.0, 3.0;
EXPECT_THAT(actual, EigenArrayEq<double>({1.0, 2.0, 3.0}));
EXPECT_THAT(actual, EigenArrayEq<double>({1.0, 2.0 + 5e-7, 3.0}));
EXPECT_THAT(actual, Not(EigenArrayEq<double>({1.0, 2.0, 5.0})));
// Wrong shape.
EXPECT_THAT(actual, Not(EigenArrayEq<double>({1.0, 2.0})));
}
TEST(EigenArrayEqTest, EmptyArrayX) {
const Eigen::ArrayXi empty;
EXPECT_THAT(empty, EigenArrayEq(empty));
// Can pass in an Eigen expression type.
EXPECT_THAT(empty, EigenArrayEq(Eigen::ArrayXi()));
EXPECT_THAT(empty, Not(EigenArrayEq<int>({1, 2})));
EXPECT_THAT(empty, Not(EigenArrayEq(Eigen::ArrayXi::Zero(3))));
}
TEST(EigenArrayEqTest, ArrayXXf) {
const Eigen::ArrayXXf expected = Eigen::ArrayXXf::Random(4, 5);
Eigen::ArrayXXf actual = expected;
EXPECT_THAT(actual, EigenArrayEq(expected));
actual.row(2) += 100;
EXPECT_THAT(actual, Not(EigenArrayEq(expected)));
// Wrong shape.
EXPECT_THAT(expected, Not(EigenArrayEq(expected.transpose())));
actual.resize(4, 3);
EXPECT_THAT(actual, Not(EigenArrayEq(expected)));
// Expression type.
actual.resize(3, 2);
actual.col(0) << 1.0, 2.0, 3.0;
actual.col(1) << 4.0, 5.0, 6.0;
std::vector<float> expected_vector({1.0, 2.0, 3.0, 4.0, 5.0, 6.0});
EXPECT_THAT(actual, EigenArrayEq(Eigen::Map<Eigen::ArrayXXf>(
&expected_vector[0], /*rows=*/3, /*cols=*/2)));
// Wrong shape.
EXPECT_THAT(actual, Not(EigenArrayEq(Eigen::Map<Eigen::ArrayXXf>(
&expected_vector[0], /*rows=*/3, /*cols=*/1))));
}
TEST(EigenArrayEqTest, ArrayXXfInlinedValues) {
Eigen::ArrayXXf actual(2, 3);
actual.row(0) << 1.0, 2.0, 3.0;
actual.row(1) << 4.0, -5.0, -6.0;
EXPECT_THAT(actual, EigenArrayEq<float>({{1.0, 2.0, 3.0}, //
{4.0, -5.0, -6.0}}));
EXPECT_THAT(actual, EigenArrayEq<float>({{1.0, 2.0, 3.0}, //
{4.0, -5.0, -6.0 - 1e-6}}));
EXPECT_THAT(actual, Not(EigenArrayEq<float>({{1.0, 2.0, 3.0}, //
{4.0, -5.0, -8.0}})));
// Wrong shape.
EXPECT_THAT(actual, Not(EigenArrayEq<float>({{1.0, 2.0, 3.0}})));
}
} // namespace
} // namespace operations_research::pdlp

1200
ortools/pdlp/trust_region.cc Normal file

File diff suppressed because it is too large Load Diff

167
ortools/pdlp/trust_region.h Normal file
View File

@@ -0,0 +1,167 @@
// Copyright 2010-2021 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.
#ifndef PDLP_TRUST_REGION_H_
#define PDLP_TRUST_REGION_H_
#include "Eigen/Core"
#include "ortools/pdlp/sharded_quadratic_program.h"
#include "ortools/pdlp/sharder.h"
namespace operations_research::pdlp {
struct TrustRegionResult {
// The step_size of the solution.
double solution_step_size;
// The value objective_vector' * (solution - center_point)
// when using the linear-time solver for LPs and QPs with objective matrix not
// treated in the prox term. When using the approximate solver for QPs, this
// field contains the value
// 0.5 * (solution - center_point)' * objective_matrix * (
// solution - center_point)
// + objective_vector' * (solution - center_point)
// instead.
double objective_value;
// The solution.
Eigen::VectorXd solution;
};
// Solves the following trust-region problem with bound constraints:
// min_x objective_vector' * (x - center_point)
// s.t. variable_lower_bounds <= x <= variable_upper_bounds
// || x - center_point ||_W <= target_radius
// where ||y||_W = sqrt(sum_i norm_weights[i] * y[i]^2)
// using an exact linear-time method.
// The sharder should have the same size as the number of variables in the
// problem.
// Assumes that there is always a feasible solution, that is, that
// variable_lower_bounds <= center_point <= variable_upper_bounds, and that
// norm_weights > 0, for 0 <= i < sharder.NumElements().
TrustRegionResult SolveTrustRegion(const Eigen::VectorXd& objective_vector,
const Eigen::VectorXd& variable_lower_bounds,
const Eigen::VectorXd& variable_upper_bounds,
const Eigen::VectorXd& center_point,
const Eigen::VectorXd& norm_weights,
double target_radius,
const Sharder& sharder);
// Solves the following trust-region problem with bound constraints:
// min_x (1/2) * (x - center_point)' * Q * (x - center_point)
// + objective_vector' * (x - center_point)
// s.t. variable_lower_bounds <= x <= variable_upper_bounds
// || x - center_point ||_W <= target_radius
// where ||y||_W = sqrt(sum_i norm_weights[i] * y[i]^2).
// It replaces the ball constraint || x - center_point ||_W <= target_radius
// with the equivalent constraint 0.5 * || x - center_point ||_W^2 <= 0.5 *
// target_radius^2 and does a binary search for a Lagrange multiplier for the
// latter constraint that is at most `solve_tolerance * max(1, lambda*)` away
// from the optimum Lagrange multiplier lambda*.
// The sharder should have the same size as the number of variables in the
// problem.
// Assumes that there is always a feasible solution, that is, that
// variable_lower_bounds <= center_point <= variable_upper_bounds, and that
// norm_weights > 0, for 0 <= i < sharder.NumElements().
TrustRegionResult SolveDiagonalTrustRegion(
const Eigen::VectorXd& objective_vector,
const Eigen::VectorXd& objective_matrix_diagonal,
const Eigen::VectorXd& variable_lower_bounds,
const Eigen::VectorXd& variable_upper_bounds,
const Eigen::VectorXd& center_point, const Eigen::VectorXd& norm_weights,
double target_radius, const Sharder& sharder, double solve_tolerance);
// Like SolveDiagonalTrustRegion, but extracts the problem data from a
// ShardedQuadraticProgram and implicitly concatenates the primal and dual parts
// before solving the trust-region subproblem.
TrustRegionResult SolveDiagonalQpTrustRegion(
const ShardedQuadraticProgram& sharded_qp,
const Eigen::VectorXd& primal_solution,
const Eigen::VectorXd& dual_solution,
const Eigen::VectorXd& primal_gradient,
const Eigen::VectorXd& dual_gradient, const double primal_weight,
double target_radius, double solve_tolerance);
struct LocalizedLagrangianBounds {
// The value of the Lagrangian function L(x, y) at the given solution.
double lagrangian_value;
// A lower bound on the Lagrangian value, valid for the given radius.
double lower_bound;
// An upper bound on the Lagrangian value, valid for the given radius.
double upper_bound;
// The radius used when computing the bounds.
double radius;
};
inline double BoundGap(const LocalizedLagrangianBounds& bounds) {
return bounds.upper_bound - bounds.lower_bound;
}
// Defines a norm on a vector partitioned as (x, y) where x is the primal and y
// is the dual. The enum values define a joint norm as a function of ||x||_P and
// ||y||_D, whose definition depends on the context.
enum class PrimalDualNorm {
// The joint norm ||(x,y)||_PD = max{||x||_P, ||y||_D}.
kMaxNorm,
// The joint norm (||(x,y)||_PD)^2 = (||x||_P)^2 + (||y||_D)^2.
kEuclideanNorm
};
// Recall the saddle-point formulation OPT = min_x max_y L(x, y) defined at
// https://developers.google.com/optimization/lp/pdlp_math#saddle-point_formulation.
// This function computes lower and upper bounds on OPT with an additional ball
// or "trust- region" constraint on the domains of x and y.
//
// The bounds are derived from the solution of the following problem:
// min_{x,y} ∇_x L(primal_solution, dual_solution)^T (x - primal_solution) -
// ∇_y L(primal_solution, dual_solution)^T (y - dual_solution)
// subject to ||(x - primal_solution, y - dual_solution)||_PD <= radius,
// where x and y are constrained to their respective bounds and ||(x,y)||_PD is
// defined by primal_dual_norm.
// When use_diagonal_qp_trust_region_solver is true, the solver instead solves
// the following problem:
// min_{x,y} ∇_x L(primal_solution, dual_solution)^T (x - primal_solution) -
// ∇_y L(primal_solution, dual_solution)^T (y - dual_solution) +
// (1 / 2) * (x - primal_solution)^T * objective_matrix * (x - primal_solution),
// subject to ||(x - primal_solution, y - dual_solution)||_PD <= radius.
// use_diagonal_qp_trust_region_solver == true assumes that primal_dual_norm
// is the Euclidean norm and the objective matrix is diagonal.
// See SolveDiagonalTrustRegion() above for the meaning of
// diagonal_qp_trust_region_solver_tolerance.
//
// In the context of primal_dual_norm, the primal norm ||.||_P is defined as
// (||x||_P)^2 = (1 / 2) * primal_weight * ||x||_2^2, and the dual norm ||.||_D
// is defined as
// (||y||_D)^2 = (1 / 2) * (1 / primal_weight) * ||y||_2^2.
//
// Given an optimal solution (x, y) to the above problem, the lower bound is
// computed as L(primal_solution, dual_solution) +
// ∇_x L(primal_solution, dual_solution)^T (x - primal_solution)
// and the upper bound is computed as L(primal_solution, dual_solution) +
// ∇_y L(primal_solution, dual_solution)^T (y - dual_solution).
//
// The bounds are "localized" because they are guaranteed to bound OPT only if
// the ||.||_PD ball contains an optimal solution.
// The primal_product and dual_product arguments optionally specify the values
// of constraint_matrix * primal_solution and constraint_matrix.transpose() *
// dual_solution, respectively. If set to nullptr, they will be computed.
LocalizedLagrangianBounds ComputeLocalizedLagrangianBounds(
const ShardedQuadraticProgram& sharded_qp,
const Eigen::VectorXd& primal_solution,
const Eigen::VectorXd& dual_solution, PrimalDualNorm primal_dual_norm,
double primal_weight, double radius, const Eigen::VectorXd* primal_product,
const Eigen::VectorXd* dual_product,
bool use_diagonal_qp_trust_region_solver,
double diagonal_qp_trust_region_solver_tolerance);
} // namespace operations_research::pdlp
#endif // PDLP_TRUST_REGION_H_

File diff suppressed because it is too large Load Diff