add pdlp to the code base; build it with bazel, call it through MPSolver
This commit is contained in:
20
WORKSPACE
20
WORKSPACE
@@ -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'],
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
328
ortools/linear_solver/pdlp_interface.cc
Normal file
328
ortools/linear_solver/pdlp_interface.cc
Normal 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, ¶meters_);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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
387
ortools/pdlp/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
||||
23
ortools/pdlp/gtest_main.cc
Normal file
23
ortools/pdlp/gtest_main.cc
Normal 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();
|
||||
}
|
||||
552
ortools/pdlp/iteration_stats.cc
Normal file
552
ortools/pdlp/iteration_stats.cc
Normal 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
|
||||
108
ortools/pdlp/iteration_stats.h
Normal file
108
ortools/pdlp/iteration_stats.h
Normal 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_
|
||||
281
ortools/pdlp/iteration_stats_test.cc
Normal file
281
ortools/pdlp/iteration_stats_test.cc
Normal 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
|
||||
2005
ortools/pdlp/primal_dual_hybrid_gradient.cc
Normal file
2005
ortools/pdlp/primal_dual_hybrid_gradient.cc
Normal file
File diff suppressed because it is too large
Load Diff
166
ortools/pdlp/primal_dual_hybrid_gradient.h
Normal file
166
ortools/pdlp/primal_dual_hybrid_gradient.h
Normal 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_
|
||||
1458
ortools/pdlp/primal_dual_hybrid_gradient_test.cc
Normal file
1458
ortools/pdlp/primal_dual_hybrid_gradient_test.cc
Normal file
File diff suppressed because it is too large
Load Diff
366
ortools/pdlp/quadratic_program.cc
Normal file
366
ortools/pdlp/quadratic_program.cc
Normal 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
|
||||
216
ortools/pdlp/quadratic_program.h
Normal file
216
ortools/pdlp/quadratic_program.h
Normal 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_
|
||||
114
ortools/pdlp/quadratic_program_io.cc
Normal file
114
ortools/pdlp/quadratic_program_io.cc
Normal 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
|
||||
51
ortools/pdlp/quadratic_program_io.h
Normal file
51
ortools/pdlp/quadratic_program_io.h
Normal 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_
|
||||
488
ortools/pdlp/quadratic_program_test.cc
Normal file
488
ortools/pdlp/quadratic_program_test.cc
Normal 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
|
||||
797
ortools/pdlp/sharded_optimization_utils.cc
Normal file
797
ortools/pdlp/sharded_optimization_utils.cc
Normal 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
|
||||
191
ortools/pdlp/sharded_optimization_utils.h
Normal file
191
ortools/pdlp/sharded_optimization_utils.h
Normal 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_
|
||||
616
ortools/pdlp/sharded_optimization_utils_test.cc
Normal file
616
ortools/pdlp/sharded_optimization_utils_test.cc
Normal 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
|
||||
155
ortools/pdlp/sharded_quadratic_program.cc
Normal file
155
ortools/pdlp/sharded_quadratic_program.cc
Normal 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
|
||||
91
ortools/pdlp/sharded_quadratic_program.h
Normal file
91
ortools/pdlp/sharded_quadratic_program.h
Normal 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_
|
||||
84
ortools/pdlp/sharded_quadratic_program_test.cc
Normal file
84
ortools/pdlp/sharded_quadratic_program_test.cc
Normal 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
320
ortools/pdlp/sharder.cc
Normal 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
330
ortools/pdlp/sharder.h
Normal 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_
|
||||
496
ortools/pdlp/sharder_test.cc
Normal file
496
ortools/pdlp/sharder_test.cc
Normal 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
|
||||
377
ortools/pdlp/solve_log.proto
Normal file
377
ortools/pdlp/solve_log.proto
Normal 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
319
ortools/pdlp/solvers.proto
Normal 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;
|
||||
}
|
||||
159
ortools/pdlp/solvers_proto_validation.cc
Normal file
159
ortools/pdlp/solvers_proto_validation.cc
Normal 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
|
||||
44
ortools/pdlp/solvers_proto_validation.h
Normal file
44
ortools/pdlp/solvers_proto_validation.h
Normal 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_
|
||||
334
ortools/pdlp/solvers_proto_validation_test.cc
Normal file
334
ortools/pdlp/solvers_proto_validation_test.cc
Normal 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
175
ortools/pdlp/termination.cc
Normal 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
|
||||
94
ortools/pdlp/termination.h
Normal file
94
ortools/pdlp/termination.h
Normal 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_
|
||||
470
ortools/pdlp/termination_test.cc
Normal file
470
ortools/pdlp/termination_test.cc
Normal 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
289
ortools/pdlp/test_util.cc
Normal 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
417
ortools/pdlp/test_util.h
Normal 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_
|
||||
331
ortools/pdlp/test_util_test.cc
Normal file
331
ortools/pdlp/test_util_test.cc
Normal 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
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
167
ortools/pdlp/trust_region.h
Normal 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_
|
||||
1093
ortools/pdlp/trust_region_test.cc
Normal file
1093
ortools/pdlp/trust_region_test.cc
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user