Merge remote-tracking branch 'google/main' into feature/temporary-for-squash

Co-authored-by: Peter MITRI <peter.mitri@rte-france.com>
This commit is contained in:
Florian OMNES
2023-11-20 12:47:39 +01:00
210 changed files with 22698 additions and 1545 deletions

View File

@@ -115,14 +115,14 @@ protobuf_deps()
## Solvers
http_archive(
name = "glpk",
build_file = "//bazel:glpk.BUILD",
build_file = "//bazel:glpk.BUILD.bazel",
sha256 = "4a1013eebb50f728fc601bdd833b0b2870333c3b3e5a816eeba921d95bec6f15",
url = "http://ftp.gnu.org/gnu/glpk/glpk-5.0.tar.gz",
)
http_archive(
name = "bliss",
build_file = "//bazel:bliss.BUILD",
build_file = "//bazel:bliss.BUILD.bazel",
patches = ["//bazel:bliss-0.73.patch"],
sha256 = "f57bf32804140cad58b1240b804e0dbd68f7e6bf67eba8e0c0fa3a62fd7f0f84",
url = "https://github.com/google/or-tools/releases/download/v9.0/bliss-0.73.zip",
@@ -131,7 +131,7 @@ http_archive(
new_git_repository(
name = "scip",
build_file = "//bazel:scip.BUILD",
build_file = "//bazel:scip.BUILD.bazel",
patches = ["//bazel:scip.patch"],
patch_args = ["-p1"],
tag = "v804",
@@ -166,7 +166,7 @@ git_repository(
# pcre source code repository
new_git_repository(
name = "pcre2",
build_file = "//bazel:pcre2.BUILD",
build_file = "//bazel:pcre2.BUILD.bazel",
tag = "pcre2-10.42",
remote = "https://github.com/PCRE2Project/pcre2.git",
)
@@ -180,11 +180,11 @@ new_git_repository(
# edit .gitignore and remove parser.h, parser.c, and swigwarn.swg
# git add Source/CParse/parser.h Source/CParse/parser.c Lib/swigwarn.swg
# git diff --staged Lib Source/CParse > <path to>swig.patch
# Edit swig.BUILD:
# Edit swig.BUILD.bazel:
# edit version
new_git_repository(
name = "swig",
build_file = "//bazel:swig.BUILD",
build_file = "//bazel:swig.BUILD.bazel",
patches = ["//bazel:swig.patch"],
patch_args = ["-p1"],
tag = "v4.1.1",
@@ -217,6 +217,18 @@ load("@ortools_notebook_deps//:requirements.bzl",
install_notebook_deps="install_deps")
install_notebook_deps()
# Absl python library
http_archive(
name = "com_google_absl_py",
repo_mapping = {"@six_archive": "@six"},
sha256 = "0be59b82d65dfa1f995365dcfea2cc57989297b065fda696ef13f30fcc6c8e5b",
strip_prefix = "abseil-py-pypi-v0.15.0",
urls = [
"https://github.com/abseil/abseil-py/archive/refs/tags/pypi-v0.15.0.tar.gz",
],
)
## `pybind11_bazel`
git_repository(
name = "pybind11_bazel",
commit = "fc56ce8a8b51e3dd941139d329b63ccfea1d304b",
@@ -240,6 +252,12 @@ new_git_repository(
remote = "https://github.com/pybind/pybind11_protobuf.git",
)
new_git_repository(
name = "pybind11_abseil",
remote = "https://github.com/pybind/pybind11_abseil.git",
commit = "2c4932ed6f6204f1656e245838f4f5eae69d2e29"
)
load("@pybind11_bazel//:python_configure.bzl", "python_configure")
python_configure(name = "local_config_python", python_version = "3")
bind(

View File

@@ -1,3 +1,16 @@
# Copyright 2010-2022 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.
cc_library(
name = "libbliss",
srcs = [
@@ -27,4 +40,4 @@ cc_library(
],
includes = ["."],
visibility = ["//visibility:public"],
)
)

View File

@@ -1,3 +1,16 @@
# Copyright 2010-2022 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.
cc_library(
name = "glpk",
srcs = glob(

View File

@@ -1,5 +1,6 @@
# OR-Tools code dependencies
absl-py==2.0.0
immutabledict==3.0.0
numpy==1.26.1
protobuf==4.25.0
scipy==1.11.3

View File

@@ -65,6 +65,8 @@ idna==3.4
# anyio
# jsonschema
# requests
immutabledict==3.0.0
# via -r bazel/notebook_requirements.in
ipykernel==6.25.2
# via
# jupyter

View File

@@ -1,5 +1,6 @@
# OR-Tools code dependencies
absl-py==2.0.0
immutabledict==3.0.0
numpy==1.26.1
protobuf==4.25.0
scipy==1.11.3
@@ -12,3 +13,4 @@ black==23.10.1
# Example dependencies
pandas==2.1.2
svgwrite==1.4.3

View File

@@ -14,6 +14,8 @@ distlib==0.3.7
# via virtualenv
filelock==3.12.2
# via virtualenv
immutabledict==3.0.0
# via -r bazel/ortools_requirements.in
mypy==1.6.1
# via -r bazel/ortools_requirements.in
mypy-extensions==1.0.0
@@ -49,6 +51,8 @@ scipy==1.11.3
# via -r bazel/ortools_requirements.in
six==1.16.0
# via python-dateutil
svgwrite==1.4.3
# via -r bazel/ortools_requirements.in
types-protobuf==4.24.0.0
# via mypy-protobuf
typing-extensions==4.8.0

View File

@@ -1,3 +1,16 @@
# Copyright 2010-2022 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.
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")

View File

@@ -1,3 +1,16 @@
# Copyright 2010-2022 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.
exports_files(
["src/lpi/lpi_glop.cpp"],
)

View File

@@ -1,3 +1,16 @@
# Copyright 2010-2022 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.
licenses(["restricted"]) # GPLv3
exports_files(["LICENSE"])

View File

@@ -24,6 +24,8 @@
#if defined(_MSC_VER)
#define WIN32_LEAN_AND_MEAN // disables several conflicting macros
#include <windows.h>
#elif defined(__MINGW32__) || defined(__MINGW64__)
#include <windows.h>
#elif defined(__GNUC__)
#include <dlfcn.h>
#endif
@@ -38,7 +40,7 @@ static constexpr size_t kMaxFunctionsNotFound = 10;
return;
}
#if defined(_MSC_VER)
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
FreeLibrary(static_cast<HINSTANCE>(library_handle_));
#elif defined(__GNUC__)
dlclose(library_handle_);
@@ -47,7 +49,7 @@ static constexpr size_t kMaxFunctionsNotFound = 10;
bool TryToLoad(const std::string& library_name) {
library_name_ = std::string(library_name);
#if defined(_MSC_VER)
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
library_handle_ = static_cast<void*>(LoadLibraryA(library_name.c_str()));
#elif defined(__GNUC__)
library_handle_ = dlopen(library_name.c_str(), RTLD_NOW);
@@ -63,13 +65,14 @@ static constexpr size_t kMaxFunctionsNotFound = 10;
template <typename T>
std::function<T> GetFunction(const char* function_name) {
const void* function_address =
#if defined(_MSC_VER)
static_cast<void*>(GetProcAddress(
static_cast<HINSTANCE>(library_handle_), function_name));
#else
dlsym(library_handle_, function_name);
#endif
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
// On Windows, avoid casting to void*: not supported by MinGW.
FARPROC function_address =
GetProcAddress(static_cast<HINSTANCE>(library_handle_), function_name);
#else // Not Windows.
const void* function_address = dlsym(library_handle_, function_name);
#endif // MinGW.
// We don't really need the full list of missing functions,
// just a few are enough.
if (!function_address && functions_not_found_.size() < kMaxFunctionsNotFound)
@@ -104,11 +107,21 @@ static constexpr size_t kMaxFunctionsNotFound = 10;
template <typename Ret, typename... Args>
struct TypeParser<Ret(Args...)> {
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
// Windows: take a FARPROC as argument.
static std::function<Ret(Args...)> CreateFunction(
const FARPROC function_address) {
return std::function<Ret(Args...)>(
reinterpret_cast<Ret (*)(Args...)>(function_address));
}
#else
// Not Windows: take a void* as argument.
static std::function<Ret(Args...)> CreateFunction(
const void* function_address) {
return std::function<Ret(Args...)>(reinterpret_cast<Ret (*)(Args...)>(
const_cast<void*>(function_address)));
}
#endif
};
};

View File

@@ -14,13 +14,14 @@
#if defined(__GNUC__) && defined(__linux__)
#include <unistd.h>
#endif
#if defined(__APPLE__) && defined(__GNUC__) // Mac OS X
#if defined(__APPLE__) && defined(__GNUC__) // MacOS
#include <mach/mach_init.h>
#include <mach/task.h>
#elif defined(__FreeBSD__) // FreeBSD
#include <sys/resource.h>
#include <sys/time.h>
#elif defined(_MSC_VER) // WINDOWS
// Windows
#elif defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
// clang-format off
#include <windows.h>
#include <psapi.h>
@@ -34,7 +35,7 @@
namespace operations_research {
// GetProcessMemoryUsage
#if defined(__APPLE__) && defined(__GNUC__) // Mac OS X
#if defined(__APPLE__) && defined(__GNUC__) // MacOS
int64_t GetProcessMemoryUsage() {
task_t task = MACH_PORT_NULL;
struct task_basic_info t_info;
@@ -48,7 +49,7 @@ int64_t GetProcessMemoryUsage() {
return resident_memory;
}
#elif defined(__GNUC__) && !defined(__FreeBSD__) && \
!defined(__EMSCRIPTEN__) // LINUX
!defined(__EMSCRIPTEN__) && !defined(_WIN32) // Linux
int64_t GetProcessMemoryUsage() {
unsigned size = 0;
char buf[30];
@@ -60,14 +61,15 @@ int64_t GetProcessMemoryUsage() {
fclose(pf);
return int64_t{1024} * size;
}
#elif defined(__FreeBSD__) // FreeBSD
#elif defined(__FreeBSD__) // FreeBSD
int64_t GetProcessMemoryUsage() {
int who = RUSAGE_SELF;
struct rusage rusage;
getrusage(who, &rusage);
return (int64_t)(int64_t{1024} * rusage.ru_maxrss);
}
#elif defined(_MSC_VER) // WINDOWS
// Windows
#elif defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
int64_t GetProcessMemoryUsage() {
HANDLE hProcess;
PROCESS_MEMORY_COUNTERS pmc;
@@ -82,7 +84,7 @@ int64_t GetProcessMemoryUsage() {
}
return memory;
}
#else // Unknown, returning 0.
#else // Unknown, returning 0.
int64_t GetProcessMemoryUsage() { return 0; }
#endif

View File

@@ -204,6 +204,16 @@ ProblemStatus LPSolver::SolveWithTimeLimit(const LinearProgram& lp,
// Make an internal copy of the problem for the preprocessing.
current_linear_program_.PopulateFromLinearProgram(lp);
// Remove small entries even if presolve is off. This is mainly here to
// avoid floating point underflow. Keeping them can break many invariant like
// a * b == 0 iff a == 0 or b == 0.
//
// Note that our presolve/scaling can potentially create smaller entries than
// this, but the scale should stay reasonable.
//
// TODO(user): If speed matter, we could do that as we copy the program.
current_linear_program_.RemoveNearZeroEntries(parameters_.drop_magnitude());
// Preprocess.
MainLpPreprocessor preprocessor(&parameters_);
preprocessor.SetLogger(&logger_);

View File

@@ -24,7 +24,7 @@ package operations_research.glop;
option java_package = "com.google.ortools.glop";
option java_multiple_files = true;
option csharp_namespace = "Google.OrTools.Glop";
// next id = 70
// next id = 72
message GlopParameters {
// Supported algorithms for scaling:
// EQUILIBRATION - progressive scaling by row and column norms until the
@@ -478,7 +478,13 @@ message GlopParameters {
// shouldn't use super large values in an LP. With the default threshold, even
// evaluating large constraint with variables at their bound shouldn't cause
// any overflow.
optional double max_valid_magnitude = 199 [default = 1e30];
optional double max_valid_magnitude = 70 [default = 1e30];
// Value in the input LP lower than this will be ignored. This is similar to
// drop_tolerance but more aggressive as this is used before scaling. This is
// mainly here to avoid underflow and have simpler invariant in the code, like
// a * b == 0 iff a or b is zero and things like this.
optional double drop_magnitude = 71 [default = 1e-30];
// On some problem like stp3d or pds-100 this makes a huge difference in
// speed and number of iterations of the dual simplex.

View File

@@ -13,6 +13,7 @@
#include "ortools/glop/parameters_validation.h"
#include <cmath>
#include <string>
#include "absl/strings/str_cat.h"
@@ -77,7 +78,16 @@ std::string ValidateParameters(const GlopParameters& params) {
TEST_NON_NEGATIVE(initial_condition_number_threshold);
TEST_NON_NEGATIVE(max_deterministic_time);
TEST_NON_NEGATIVE(max_time_in_seconds);
TEST_NON_NEGATIVE(max_valid_magnitude);
TEST_FINITE_AND_NON_NEGATIVE(max_valid_magnitude);
if (params.max_valid_magnitude() > 1e100) {
return "max_valid_magnitude must be <= 1e100";
}
TEST_FINITE_AND_NON_NEGATIVE(drop_magnitude);
if (params.drop_magnitude() < 1e-100) {
return "drop magnitude must be finite and >= 1e-100";
}
TEST_INTEGER_NON_NEGATIVE(basis_refactorization_period);
TEST_INTEGER_NON_NEGATIVE(devex_weights_reset_period);

View File

@@ -2961,80 +2961,6 @@ MatrixEntry SingletonPreprocessor::GetSingletonRowMatrixEntry(
return MatrixEntry(RowIndex(0), ColIndex(0), 0.0);
}
// --------------------------------------------------------
// RemoveNearZeroEntriesPreprocessor
// --------------------------------------------------------
bool RemoveNearZeroEntriesPreprocessor::Run(LinearProgram* lp) {
SCOPED_INSTRUCTION_COUNT(time_limit_);
RETURN_VALUE_IF_NULL(lp, false);
const ColIndex num_cols = lp->num_variables();
if (num_cols == 0) return false;
// We will use a different threshold for each row depending on its degree.
// We use Fractionals for convenience since they will be used as such below.
const RowIndex num_rows = lp->num_constraints();
DenseColumn row_degree(num_rows, 0.0);
Fractional num_non_zero_objective_coefficients = 0.0;
for (ColIndex col(0); col < num_cols; ++col) {
for (const SparseColumn::Entry e : lp->GetSparseColumn(col)) {
row_degree[e.row()] += 1.0;
}
if (lp->objective_coefficients()[col] != 0.0) {
num_non_zero_objective_coefficients += 1.0;
}
}
// To not have too many parameters, we use the preprocessor_zero_tolerance.
const Fractional allowed_impact = parameters_.preprocessor_zero_tolerance();
// TODO(user): Our criteria ensure that during presolve a primal feasible
// solution will stay primal feasible. However, we have no guarantee on the
// dual-feasibility (because the dual variable values range is not taken into
// account). Fix that? or find a better criteria since it seems that on all
// our current problems, this preprocessor helps and doesn't introduce errors.
const EntryIndex initial_num_entries = lp->num_entries();
int num_zeroed_objective_coefficients = 0;
for (ColIndex col(0); col < num_cols; ++col) {
const Fractional lower_bound = lp->variable_lower_bounds()[col];
const Fractional upper_bound = lp->variable_upper_bounds()[col];
// TODO(user): Write a small class that takes a matrix, its transpose, row
// and column bounds, and "propagate" the bounds as much as possible so we
// can use this better estimate here and remove more near-zero entries.
const Fractional max_magnitude =
std::max(std::abs(lower_bound), std::abs(upper_bound));
if (max_magnitude == kInfinity || max_magnitude == 0) continue;
const Fractional threshold = allowed_impact / max_magnitude;
lp->GetMutableSparseColumn(col)->RemoveNearZeroEntriesWithWeights(
threshold, row_degree);
if (lp->objective_coefficients()[col] != 0.0 &&
num_non_zero_objective_coefficients *
std::abs(lp->objective_coefficients()[col]) <
threshold) {
lp->SetObjectiveCoefficient(col, 0.0);
++num_zeroed_objective_coefficients;
}
}
const EntryIndex num_entries = lp->num_entries();
if (num_entries != initial_num_entries) {
VLOG(1) << "Removed " << initial_num_entries - num_entries
<< " near-zero entries.";
}
if (num_zeroed_objective_coefficients > 0) {
VLOG(1) << "Removed " << num_zeroed_objective_coefficients
<< " near-zero objective coefficients.";
}
// No post-solve is required.
return false;
}
void RemoveNearZeroEntriesPreprocessor::RecoverSolution(
ProblemSolution* solution) const {}
// --------------------------------------------------------
// SingletonColumnSignPreprocessor
// --------------------------------------------------------

View File

@@ -786,31 +786,6 @@ class EmptyConstraintPreprocessor : public Preprocessor {
RowDeletionHelper row_deletion_helper_;
};
// --------------------------------------------------------
// RemoveNearZeroEntriesPreprocessor
// --------------------------------------------------------
// Removes matrix entries that have only a negligible impact on the solution.
// Using the variable bounds, we derive a maximum possible impact, and remove
// the entries whose impact is under a given tolerance.
//
// TODO(user): This preprocessor doesn't work well on badly scaled problems. In
// particular, it will set the objective to zero if all the objective
// coefficients are small! Run it after ScalingPreprocessor or fix the code.
class RemoveNearZeroEntriesPreprocessor : public Preprocessor {
public:
explicit RemoveNearZeroEntriesPreprocessor(const GlopParameters* parameters)
: Preprocessor(parameters) {}
RemoveNearZeroEntriesPreprocessor(const RemoveNearZeroEntriesPreprocessor&) =
delete;
RemoveNearZeroEntriesPreprocessor& operator=(
const RemoveNearZeroEntriesPreprocessor&) = delete;
~RemoveNearZeroEntriesPreprocessor() final = default;
bool Run(LinearProgram* lp) final;
void RecoverSolution(ProblemSolution* solution) const final;
private:
};
// --------------------------------------------------------
// SingletonColumnSignPreprocessor
// --------------------------------------------------------

View File

@@ -2777,6 +2777,9 @@ Status RevisedSimplex::PrimalMinimize(TimeLimit* time_limit) {
}
while (true) {
AdvanceDeterministicTime(time_limit);
if (time_limit->LimitReached()) break;
// TODO(user): we may loop a bit more than the actual number of iteration.
// fix.
IF_STATS_ENABLED(
@@ -2889,11 +2892,7 @@ Status RevisedSimplex::PrimalMinimize(TimeLimit* time_limit) {
// when running with 0 iterations, we still want to report
// ProblemStatus::OPTIMAL or ProblemStatus::PRIMAL_FEASIBLE if it is the
// case at the beginning of the algorithm.
AdvanceDeterministicTime(time_limit);
if (num_iterations_ == parameters_.max_number_of_iterations() ||
time_limit->LimitReached()) {
break;
}
if (num_iterations_ == parameters_.max_number_of_iterations()) break;
Fractional step_length;
RowIndex leaving_row;
@@ -3081,6 +3080,9 @@ Status RevisedSimplex::DualMinimize(bool feasibility_phase,
ColIndex entering_col;
while (true) {
AdvanceDeterministicTime(time_limit);
if (time_limit->LimitReached()) break;
// TODO(user): we may loop a bit more than the actual number of iteration.
// fix.
IF_STATS_ENABLED(
@@ -3306,9 +3308,7 @@ Status RevisedSimplex::DualMinimize(bool feasibility_phase,
// when running with 0 iterations, we still want to report
// ProblemStatus::OPTIMAL or ProblemStatus::PRIMAL_FEASIBLE if it is the
// case at the beginning of the algorithm.
AdvanceDeterministicTime(time_limit);
if (num_iterations_ == parameters_.max_number_of_iterations() ||
time_limit->LimitReached()) {
if (num_iterations_ == parameters_.max_number_of_iterations()) {
IF_STATS_ENABLED(timer.AlsoUpdate(&iteration_stats_.normal));
return Status::OK();
}

View File

@@ -120,7 +120,6 @@ cc_library(
hdrs = ["cliques.h"],
deps = [
"//ortools/base",
"//ortools/base:hash",
"//ortools/base:intops",
"//ortools/base:strong_vector",
"//ortools/util:time_limit",

View File

@@ -20,7 +20,6 @@
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "ortools/base/hash.h"
namespace operations_research {
namespace {

View File

@@ -20,13 +20,15 @@
#include "absl/container/flat_hash_map.h"
#include "absl/functional/bind_front.h"
#include "absl/log/check.h"
#include "ortools/base/adjustable_priority_queue-inl.h"
#include "ortools/base/adjustable_priority_queue.h"
#include "ortools/base/logging.h"
#include "ortools/base/map_util.h"
#include "ortools/base/stl_util.h"
#include "ortools/base/threadpool.h"
#include "ortools/base/timer.h"
#include "ortools/util/zvector.h"
#include "ortools/graph/ebert_graph.h"
namespace operations_research {
@@ -66,7 +68,7 @@ class PathContainerImpl {
// from 'to' to itself is composed of the arc ('to, 'to'), which might not be
// the case if either this arc doesn't exist or if the length of this arc is
// greater than the distance of an alternate path.
// If nodes are not connected, returns StarGraph::kNilNode.
// If nodes are not connected, returns kNilNode.
virtual NodeIndex GetPenultimateNodeInPath(NodeIndex from,
NodeIndex to) const = 0;
@@ -77,7 +79,7 @@ class PathContainerImpl {
// Adds a path tree rooted at node 'from', and to a set of implicit
// destinations:
// - predecessor_in_path_tree[node] is the predecessor of node 'node' in the
// path from 'from' to 'node', or StarGraph::kNilNode if there is no
// path from 'from' to 'node', or kNilNode if there is no
// predecessor (i.e. if 'node' is not in the path tree);
// - distance_to_destination[i] is the distance from 'from' to the i-th
// destination (see Initialize()).
@@ -226,16 +228,26 @@ class DistanceContainer : public PathContainerImpl {
}
NodeIndex GetPenultimateNodeInPath(NodeIndex from,
NodeIndex to) const override {
(void)from;
(void)to;
LOG(FATAL) << "Path not stored.";
return StarGraph::kNilNode;
}
void GetPath(NodeIndex from, NodeIndex to,
std::vector<NodeIndex>* path) const override {
(void)from;
(void)to;
(void)path;
LOG(FATAL) << "Path not stored.";
}
void StoreSingleSourcePaths(
NodeIndex from, const std::vector<NodeIndex>& predecessor_in_path_tree,
const std::vector<PathDistance>& distance_to_destination) override {
// DistanceContainer only stores distances and not predecessors.
(void)predecessor_in_path_tree;
distances_[reverse_sources_[from]] = distance_to_destination;
}
@@ -361,83 +373,6 @@ static_assert(sizeof(NodeEntry) == 16, "node_entry_class_is_not_well_packed");
// using a binary heap-based Dijkstra algorithm.
// TODO(user): Investigate alternate implementation which wouldn't use
// AdjustablePriorityQueue.
template <class GraphType>
void ComputeOneToManyInternal(const GraphType* const graph,
const ZVector<PathDistance>* const arc_lengths,
NodeIndex source,
const std::vector<NodeIndex>* const destinations,
PathContainerImpl* const paths) {
CHECK(graph != nullptr);
CHECK(arc_lengths != nullptr);
CHECK(destinations != nullptr);
CHECK(paths != nullptr);
const int num_nodes = graph->num_nodes();
std::vector<NodeIndex> predecessor(num_nodes, GraphType::kNilNode);
AdjustablePriorityQueue<NodeEntry> priority_queue;
std::vector<NodeEntry> entries(num_nodes);
for (typename GraphType::NodeIterator iterator(*graph); iterator.Ok();
iterator.Next()) {
entries[iterator.Index()].set_node(iterator.Index());
}
// Marking destination node. This is an optimization stopping the search
// when all destinations have been reached.
for (int i = 0; i < destinations->size(); ++i) {
entries[(*destinations)[i]].set_is_destination(true);
}
// In this implementation the distance of a node to itself isn't necessarily
// 0.
// So we push successors of source in the queue instead of the source
// directly which will avoid marking the source.
for (typename GraphType::OutgoingArcIterator iterator(*graph, source);
iterator.Ok(); iterator.Next()) {
const ArcIndex arc = iterator.Index();
const NodeIndex next = graph->Head(arc);
if (InsertOrUpdateEntry(arc_lengths->Value(arc), &entries[next],
&priority_queue)) {
predecessor[next] = source;
}
}
int destinations_remaining = destinations->size();
while (!priority_queue.IsEmpty()) {
NodeEntry* current = priority_queue.Top();
const NodeIndex current_node = current->node();
priority_queue.Pop();
current->set_settled(true);
if (current->is_destination()) {
destinations_remaining--;
if (destinations_remaining == 0) {
break;
}
}
const PathDistance current_distance = current->distance();
for (typename GraphType::OutgoingArcIterator iterator(*graph, current_node);
iterator.Ok(); iterator.Next()) {
const ArcIndex arc = iterator.Index();
const NodeIndex next = graph->Head(arc);
NodeEntry* const entry = &entries[next];
if (!entry->settled()) {
DCHECK_GE(current_distance, 0);
const PathDistance arc_length = arc_lengths->Value(arc);
DCHECK_LE(current_distance, kDisconnectedPathDistance - arc_length);
if (InsertOrUpdateEntry(current_distance + arc_length, entry,
&priority_queue)) {
predecessor[next] = current_node;
}
}
}
}
const int destinations_size = destinations->size();
std::vector<PathDistance> distances(destinations_size,
kDisconnectedPathDistance);
for (int i = 0; i < destinations_size; ++i) {
NodeIndex node = destinations->at(i);
if (entries[node].settled()) {
distances[i] = entries[node].distance();
}
}
paths->StoreSingleSourcePaths(source, predecessor, distances);
}
template <class GraphType>
void ComputeOneToManyInternalOnGraph(
const GraphType* const graph,
@@ -551,64 +486,6 @@ void PathContainer::BuildInMemoryCompactPathContainer(
path_container->container_ = std::make_unique<InMemoryCompactPathContainer>();
}
template <class GraphType>
void ComputeManyToManyShortestPathsWithMultipleThreadsInternal(
const GraphType& graph, const ZVector<PathDistance>& arc_lengths,
const std::vector<NodeIndex>& sources,
const std::vector<NodeIndex>& destinations, int num_threads,
PathContainer* const paths) {
if (graph.num_nodes() > 0) {
CHECK_EQ(graph.num_arcs(),
1 + arc_lengths.max_index() - arc_lengths.min_index())
<< "Number of arcs in graph must match arc length vector size";
// Removing duplicate sources to allow mutex-free implementation (and it's
// more efficient); same with destinations for efficiency reasons.
std::vector<NodeIndex> unique_sources = sources;
gtl::STLSortAndRemoveDuplicates(&unique_sources);
std::vector<NodeIndex> unique_destinations = destinations;
gtl::STLSortAndRemoveDuplicates(&unique_destinations);
WallTimer timer;
timer.Start();
PathContainerImpl* container = paths->GetImplementation();
container->Initialize(unique_sources, unique_destinations,
graph.num_nodes());
{
std::unique_ptr<ThreadPool> pool(
new ThreadPool("OR_Dijkstra", num_threads));
pool->StartWorkers();
for (int i = 0; i < unique_sources.size(); ++i) {
pool->Schedule(absl::bind_front(&ComputeOneToManyInternal<GraphType>,
&graph, &arc_lengths, unique_sources[i],
&unique_destinations, container));
}
}
container->Finalize();
VLOG(2) << "Elapsed time to compute shortest paths: " << timer.Get() << "s";
}
}
template <>
void ComputeManyToManyShortestPathsWithMultipleThreads(
const StarGraph& graph, const ZVector<PathDistance>& arc_lengths,
const std::vector<NodeIndex>& sources,
const std::vector<NodeIndex>& destinations, int num_threads,
PathContainer* const paths) {
ComputeManyToManyShortestPathsWithMultipleThreadsInternal(
graph, arc_lengths, sources, destinations, num_threads, paths);
}
template <>
void ComputeManyToManyShortestPathsWithMultipleThreads(
const ForwardStarGraph& graph, const ZVector<PathDistance>& arc_lengths,
const std::vector<NodeIndex>& sources,
const std::vector<NodeIndex>& destinations, int num_threads,
PathContainer* const paths) {
ComputeManyToManyShortestPathsWithMultipleThreadsInternal(
graph, arc_lengths, sources, destinations, num_threads, paths);
}
// Version on BaseGraph sub-classes.
template <class GraphType>
void ComputeManyToManyShortestPathsWithMultipleThreadsInternal(
const GraphType& graph, const std::vector<PathDistance>& arc_lengths,

View File

@@ -33,8 +33,8 @@
// computations (cf. PathContainer).
//
// Usage example computing all-pair shortest paths on a graph:
// StarGraph graph(...,...);
// ZVector<uint32_t> arc_lengths(...,...);
// StaticGraph graph(...,...);
// std::vector<uint32_t> arc_lengths(...,...);
// ... populate graph and arc lengths ...
// PathContainer container;
// PathContainer::BuildInMemoryCompactPathContainer(&container);
@@ -44,8 +44,8 @@
// &container);
//
// Usage example computing shortest paths between a subset of graph nodes:
// StarGraph graph(...,...);
// ZVector<uint32_t> arc_lengths(...,...);
// StaticGraph graph(...,...);
// std::vector<uint32_t> arc_lengths(...,...);
// ... populate graph and arc lengths ...
// vector<NodeIndex> sources;
// vector<NodeIndex> sinks;
@@ -67,8 +67,8 @@
#include <memory>
#include <vector>
#include "absl/log/check.h"
#include "ortools/base/logging.h"
#include "ortools/base/types.h"
#include "ortools/graph/ebert_graph.h"
#include "ortools/graph/graph.h"
#include "ortools/util/zvector.h"
@@ -78,9 +78,6 @@ namespace operations_research {
// Storing distances on 32 bits to limit memory consumption of distance
// matrices. If distances don't fit on 32 bits, scaling and losing a bit of
// precision should be acceptable in practice.
template <class T>
class ZVector;
typedef uint32_t PathDistance;
const PathDistance kDisconnectedPathDistance =
@@ -187,17 +184,6 @@ void GetGraphNodesFromGraph(const GraphType& graph,
// Resulting shortest paths are stored in a path container 'path_container'.
// Computes shortest paths from the node 'source' to all nodes in the graph.
template <class GraphType>
void ComputeOneToAllShortestPaths(const GraphType& graph,
const ZVector<PathDistance>& arc_lengths,
NodeIndex source,
PathContainer* const path_container) {
std::vector<NodeIndex> all_nodes;
GetGraphNodes<GraphType>(graph, &all_nodes);
ComputeOneToManyShortestPaths(graph, arc_lengths, source, all_nodes,
path_container);
}
template <class GraphType>
void ComputeOneToAllShortestPaths(const GraphType& graph,
const std::vector<PathDistance>& arc_lengths,
@@ -210,17 +196,6 @@ void ComputeOneToAllShortestPaths(const GraphType& graph,
}
// Computes shortest paths from the node 'source' to nodes in 'destinations'.
template <class GraphType>
void ComputeOneToManyShortestPaths(const GraphType& graph,
const ZVector<PathDistance>& arc_lengths,
NodeIndex source,
const std::vector<NodeIndex>& destinations,
PathContainer* const path_container) {
std::vector<NodeIndex> sources(1, source);
ComputeManyToManyShortestPathsWithMultipleThreads(
graph, arc_lengths, sources, destinations, 1, path_container);
}
template <class GraphType>
void ComputeOneToManyShortestPaths(
const GraphType& graph, const std::vector<PathDistance>& arc_lengths,
@@ -232,19 +207,35 @@ void ComputeOneToManyShortestPaths(
graph, arc_lengths, sources, destinations, 1, path_container);
}
// Computes shortest paths from the nodes in 'sources' to all nodes in the
// graph.
// Computes the shortest path from the node 'source' to the node 'destination'
// and returns that path as a vector of nodes. If there is no path from 'source'
// to 'destination', the returned vector is empty.
//
// To get distance information, use ComputeOneToManyShortestPaths with a single
// destination and a `PathContainer` built with `BuildPathDistanceContainer` (if
// you just need the distance) or `BuildInMemoryCompactPathContainer`
// (otherwise).
template <class GraphType>
void ComputeManyToAllShortestPathsWithMultipleThreads(
const GraphType& graph, const ZVector<PathDistance>& arc_lengths,
const std::vector<NodeIndex>& sources, int num_threads,
PathContainer* const path_container) {
std::vector<NodeIndex> all_nodes;
GetGraphNodes<GraphType>(graph, &all_nodes);
std::vector<typename GraphType::NodeIndex> ComputeOneToOneShortestPath(
const GraphType& graph, const std::vector<PathDistance>& arc_lengths,
typename GraphType::NodeIndex source,
typename GraphType::NodeIndex destination) {
std::vector<typename GraphType::NodeIndex> sources(1, source);
std::vector<typename GraphType::NodeIndex> destinations(1, destination);
PathContainer path_container;
PathContainer::BuildInMemoryCompactPathContainer(&path_container);
ComputeManyToManyShortestPathsWithMultipleThreads(
graph, arc_lengths, sources, all_nodes, num_threads, path_container);
graph, arc_lengths, sources, destinations, 1, &path_container);
std::vector<typename GraphType::NodeIndex> path;
path_container.GetPath(source, destination, &path);
return path;
}
// Computes shortest paths from the nodes in 'sources' to all nodes in the
// graph.
template <class GraphType>
void ComputeManyToAllShortestPathsWithMultipleThreads(
const GraphType& graph, const std::vector<PathDistance>& arc_lengths,
@@ -258,51 +249,24 @@ void ComputeManyToAllShortestPathsWithMultipleThreads(
// Computes shortest paths from the nodes in 'sources' to the nodes in
// 'destinations'.
template <class GraphType>
void ComputeManyToManyShortestPathsWithMultipleThreads(
const GraphType& graph, const ZVector<PathDistance>& arc_lengths,
const std::vector<NodeIndex>& sources,
const std::vector<NodeIndex>& destinations, int num_threads,
PathContainer* const path_container) {
LOG(DFATAL) << "Graph type not supported";
}
template <class GraphType>
void ComputeManyToManyShortestPathsWithMultipleThreads(
const GraphType& graph, const std::vector<PathDistance>& arc_lengths,
const std::vector<typename GraphType::NodeIndex>& sources,
const std::vector<typename GraphType::NodeIndex>& destinations,
int num_threads, PathContainer* const path_container) {
(void)graph;
(void)arc_lengths;
(void)sources;
(void)destinations;
(void)num_threads;
(void)path_container;
LOG(DFATAL) << "Graph type not supported";
}
// Specialization for supported graph classes.
template <>
void ComputeManyToManyShortestPathsWithMultipleThreads(
const StarGraph& graph, const ZVector<PathDistance>& arc_lengths,
const std::vector<NodeIndex>& sources,
const std::vector<NodeIndex>& destinations, int num_threads,
PathContainer* path_container);
template <>
void ComputeManyToManyShortestPathsWithMultipleThreads(
const ForwardStarGraph& graph, const ZVector<PathDistance>& arc_lengths,
const std::vector<NodeIndex>& sources,
const std::vector<NodeIndex>& destinations, int num_threads,
PathContainer* path_container);
// Computes shortest paths between all nodes of the graph.
template <class GraphType>
void ComputeAllToAllShortestPathsWithMultipleThreads(
const GraphType& graph, const ZVector<PathDistance>& arc_lengths,
int num_threads, PathContainer* const path_container) {
std::vector<NodeIndex> all_nodes;
GetGraphNodes<GraphType>(graph, &all_nodes);
ComputeManyToManyShortestPathsWithMultipleThreads(
graph, arc_lengths, all_nodes, all_nodes, num_threads, path_container);
}
using ::util::ListGraph;
template <>
void ComputeManyToManyShortestPathsWithMultipleThreads(

View File

@@ -18,8 +18,10 @@
#include <vector>
#include "absl/base/macros.h"
#include "absl/log/check.h"
#include "absl/random/random.h"
#include "gtest/gtest.h"
#include "ortools/graph/ebert_graph.h"
#include "ortools/graph/strongly_connected_components.h"
#include "ortools/util/zvector.h"
@@ -117,100 +119,6 @@ void CheckPathDataFromGraph(const GraphType& graph,
PathContainer distance_container; \
PathContainer::BuildPathDistanceContainer(&distance_container)
template <class GraphType>
void TestShortestPathsFromGraph(const GraphType& graph,
const ZVector<PathDistance>& lengths,
const NodeIndex expected_paths[],
const PathDistance expected_distances[]) {
const int kThreads = 10;
const NodeIndex source = typename GraphType::NodeIterator(graph).Index();
std::vector<NodeIndex> some_nodes;
int index = 0;
std::mt19937 randomizer(12345);
for (typename GraphType::NodeIterator iterator(graph); iterator.Ok();
iterator.Next()) {
if (absl::Bernoulli(randomizer, 1.0 / 2)) {
some_nodes.push_back(iterator.Index());
}
++index;
}
// All-pair shortest paths.
{
BUILD_CONTAINERS();
ComputeAllToAllShortestPathsWithMultipleThreads(graph, lengths, kThreads,
&container);
ComputeAllToAllShortestPathsWithMultipleThreads(graph, lengths, kThreads,
&distance_container);
CheckPathData(graph, container, distance_container, expected_paths,
expected_distances);
}
// One-to-all shortest paths.
{
BUILD_CONTAINERS();
ComputeOneToAllShortestPaths(graph, lengths, source, &container);
ComputeOneToAllShortestPaths(graph, lengths, source, &distance_container);
CheckPathDataRow(graph, container, distance_container, expected_paths,
expected_distances, source);
}
// Many-to-all shortest paths.
{
BUILD_CONTAINERS();
ComputeManyToAllShortestPathsWithMultipleThreads(graph, lengths, some_nodes,
kThreads, &container);
ComputeManyToAllShortestPathsWithMultipleThreads(
graph, lengths, some_nodes, kThreads, &distance_container);
for (int i = 0; i < some_nodes.size(); ++i) {
CheckPathDataRow(graph, container, distance_container, expected_paths,
expected_distances, some_nodes[i]);
}
}
// Many-to-all shortest paths with duplicates.
{
BUILD_CONTAINERS();
std::vector<NodeIndex> sources(3, source);
ComputeManyToAllShortestPathsWithMultipleThreads(graph, lengths, sources,
kThreads, &container);
ComputeManyToAllShortestPathsWithMultipleThreads(
graph, lengths, sources, kThreads, &distance_container);
for (int i = 0; i < sources.size(); ++i) {
CheckPathDataRow(graph, container, distance_container, expected_paths,
expected_distances, sources[i]);
}
}
// One-to-many shortest paths.
{
BUILD_CONTAINERS();
ComputeOneToManyShortestPaths(graph, lengths, source, some_nodes,
&container);
ComputeOneToManyShortestPaths(graph, lengths, source, some_nodes,
&distance_container);
index = source * graph.num_nodes();
for (int i = 0; i < some_nodes.size(); ++i) {
CheckPathDataPair(container, distance_container,
expected_distances[index + some_nodes[i]],
expected_paths[index + some_nodes[i]], source,
some_nodes[i]);
}
}
// Many-to-many shortest paths.
{
BUILD_CONTAINERS();
ComputeManyToManyShortestPathsWithMultipleThreads(
graph, lengths, some_nodes, some_nodes, kThreads, &container);
ComputeManyToManyShortestPathsWithMultipleThreads(
graph, lengths, some_nodes, some_nodes, kThreads, &distance_container);
for (int i = 0; i < some_nodes.size(); ++i) {
index = some_nodes[i] * graph.num_nodes();
for (int j = 0; j < some_nodes.size(); ++j) {
CheckPathDataPair(container, distance_container,
expected_distances[index + some_nodes[j]],
expected_paths[index + some_nodes[j]], some_nodes[i],
some_nodes[j]);
}
}
}
}
template <class GraphType>
void TestShortestPathsFromGraph(const GraphType& graph,
const std::vector<PathDistance>& lengths,
@@ -306,20 +214,6 @@ void TestShortestPathsFromGraph(const GraphType& graph,
#undef BUILD_CONTAINERS
template <class GraphType>
void TestShortestPaths(int num_nodes, int num_arcs, const NodeIndex arcs[][2],
const PathDistance arc_lengths[],
const NodeIndex expected_paths[],
const PathDistance expected_distances[]) {
GraphType graph(num_nodes, num_arcs);
ZVector<PathDistance> lengths(0, num_arcs - 1);
for (int i = 0; i < num_arcs; ++i) {
lengths[graph.AddArc(arcs[i][0], arcs[i][1])] = arc_lengths[i];
}
TestShortestPathsFromGraph(graph, lengths, expected_paths,
expected_distances);
}
template <class GraphType>
void TestShortestPathsFromGraph(
int num_nodes, int num_arcs, const typename GraphType::NodeIndex arcs[][2],
@@ -369,15 +263,6 @@ TYPED_TEST_SUITE(GraphShortestPathsDeathTest,
TYPED_TEST_SUITE(GraphShortestPathsTest, GraphTypesForShortestPathsTesting);
// Test on an empty graph.
TYPED_TEST(ShortestPathsDeathTest, ShortestPathsEmptyGraph) {
const int kExpectedPaths[] = {};
const PathDistance kExpectedDistances[] = {};
TypeParam graph;
ZVector<PathDistance> lengths;
TestShortestPathsFromGraph(graph, lengths, kExpectedPaths,
kExpectedDistances);
}
TYPED_TEST(GraphShortestPathsDeathTest, ShortestPathsEmptyGraph) {
const int kExpectedPaths[] = {};
const PathDistance kExpectedDistances[] = {};
@@ -388,27 +273,6 @@ TYPED_TEST(GraphShortestPathsDeathTest, ShortestPathsEmptyGraph) {
}
// Test on a disconnected graph (set of nodes pointing to themselves).
TYPED_TEST(ShortestPathsDeathTest, ShortestPathsAllDisconnected) {
const NodeIndex kUnconnected = TypeParam::kNilNode;
const int kNodes = 3;
const NodeIndex kArcs[][2] = {{0, 0}, {1, 1}, {2, 2}};
const PathDistance kArcLengths[] = {0, 0, 0};
const int kExpectedPaths[] = {0, kUnconnected, kUnconnected, kUnconnected,
1, kUnconnected, kUnconnected, kUnconnected,
2};
const PathDistance kExpectedDistances[] = {0,
kDisconnectedPathDistance,
kDisconnectedPathDistance,
kDisconnectedPathDistance,
0,
kDisconnectedPathDistance,
kDisconnectedPathDistance,
kDisconnectedPathDistance,
0};
TestShortestPaths<TypeParam>(kNodes, ABSL_ARRAYSIZE(kArcLengths), kArcs,
kArcLengths, kExpectedPaths, kExpectedDistances);
}
TYPED_TEST(GraphShortestPathsDeathTest, ShortestPathsAllDisconnected) {
const typename TypeParam::NodeIndex kUnconnected = -1;
const int kNodes = 3;
@@ -440,21 +304,6 @@ TYPED_TEST(GraphShortestPathsDeathTest, ShortestPathsAllDisconnected) {
// | || |
// --------------- ----------
//
TYPED_TEST(ShortestPathsDeathTest, ShortestPaths1) {
const int kNodes = 6;
const NodeIndex kArcs[][2] = {{0, 2}, {0, 3}, {1, 4}, {2, 4},
{3, 5}, {4, 5}, {5, 0}, {5, 1}};
const PathDistance kArcLengths[] = {1, 4, 1, 1, 1, 1, 1, 3};
const int kExpectedPaths[] = {5, 5, 0, 0, 2, 4, 5, 5, 0, 0, 1, 4,
5, 5, 0, 0, 2, 4, 5, 5, 0, 0, 2, 3,
5, 5, 0, 0, 2, 4, 5, 5, 0, 0, 2, 4};
const PathDistance kExpectedDistances[] = {
4, 6, 1, 4, 2, 3, 3, 5, 4, 7, 1, 2, 3, 5, 4, 7, 1, 2,
2, 4, 3, 6, 4, 1, 2, 4, 3, 6, 4, 1, 1, 3, 2, 5, 3, 4};
TestShortestPaths<TypeParam>(kNodes, ABSL_ARRAYSIZE(kArcLengths), kArcs,
kArcLengths, kExpectedPaths, kExpectedDistances);
}
TYPED_TEST(GraphShortestPathsDeathTest, ShortestPaths1) {
const int kNodes = 6;
const typename TypeParam::NodeIndex kArcs[][2] = {
@@ -482,18 +331,6 @@ TYPED_TEST(GraphShortestPathsDeathTest, ShortestPaths1) {
// | |
// | 1 |
// ----------------------
TYPED_TEST(ShortestPathsDeathTest, ShortestPaths2) {
const int kNodes = 4;
const NodeIndex kArcs[][2] = {{0, 1}, {0, 0}, {0, 2}, {1, 2},
{1, 3}, {2, 3}, {3, 0}};
const PathDistance kArcLengths[] = {1, 0, 3, 1, 4, 1, 1};
const int kExpectedPaths[] = {0, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2};
const PathDistance kExpectedDistances[] = {0, 1, 2, 3, 3, 4, 1, 2,
2, 3, 4, 1, 1, 2, 3, 4};
TestShortestPaths<TypeParam>(kNodes, ABSL_ARRAYSIZE(kArcLengths), kArcs,
kArcLengths, kExpectedPaths, kExpectedDistances);
}
TYPED_TEST(GraphShortestPathsDeathTest, ShortestPaths2) {
const int kNodes = 4;
const typename TypeParam::NodeIndex kArcs[][2] = {
@@ -507,19 +344,6 @@ TYPED_TEST(GraphShortestPathsDeathTest, ShortestPaths2) {
kExpectedDistances);
}
TYPED_TEST(ShortestPathsDeathTest, MismatchedData) {
TypeParam graph(2, 2);
graph.AddArc(0, 1);
graph.AddArc(1, 0);
ZVector<PathDistance> lengths(0, 0);
lengths[0] = 0;
PathContainer container;
PathContainer::BuildInMemoryCompactPathContainer(&container);
EXPECT_DEATH(ComputeAllToAllShortestPathsWithMultipleThreads(graph, lengths,
1, &container),
"Number of arcs in graph must match arc length vector size");
}
TYPED_TEST(GraphShortestPathsDeathTest, MismatchedData) {
TypeParam graph(2, 2);
graph.AddArc(0, 1);
@@ -533,24 +357,6 @@ TYPED_TEST(GraphShortestPathsDeathTest, MismatchedData) {
}
// Test the case where some sources are not strongly connected to themselves.
TYPED_TEST(ShortestPathsDeathTest, SourceNotConnectedToItself) {
const int kNodes = 3;
const NodeIndex kArcs[][2] = {{1, 2}, {2, 2}};
const PathDistance kArcLengths[] = {1, 0};
const int kExpectedPaths[] = {-1, -1, -1, -1, -1, 1, -1, -1, 2};
const PathDistance kExpectedDistances[] = {kDisconnectedPathDistance,
kDisconnectedPathDistance,
kDisconnectedPathDistance,
kDisconnectedPathDistance,
kDisconnectedPathDistance,
1,
kDisconnectedPathDistance,
kDisconnectedPathDistance,
0};
TestShortestPaths<TypeParam>(kNodes, ABSL_ARRAYSIZE(kArcLengths), kArcs,
kArcLengths, kExpectedPaths, kExpectedDistances);
}
TYPED_TEST(GraphShortestPathsDeathTest, SourceNotConnectedToItself) {
const int kNodes = 3;
const typename TypeParam::NodeIndex kArcs[][2] = {{1, 2}, {2, 2}};
@@ -572,18 +378,6 @@ TYPED_TEST(GraphShortestPathsDeathTest, SourceNotConnectedToItself) {
// Test the case where the graph is a multigraph, a graph with parallel arcs
// (arcs which have the same end nodes).
TYPED_TEST(ShortestPathsDeathTest, Multigraph) {
const int kNodes = 4;
const NodeIndex kArcs[][2] = {{0, 1}, {0, 1}, {0, 2}, {0, 2}, {1, 3},
{2, 3}, {1, 3}, {2, 3}, {3, 0}};
const PathDistance kArcLengths[] = {2, 1, 1, 2, 2, 2, 1, 1, 1};
const int kExpectedPaths[] = {3, 0, 0, 2, 3, 0, 0, 1, 3, 0, 0, 2, 3, 0, 0, 2};
const PathDistance kExpectedDistances[] = {3, 1, 1, 2, 2, 3, 3, 1,
2, 3, 3, 1, 1, 2, 2, 3};
TestShortestPaths<TypeParam>(kNodes, ABSL_ARRAYSIZE(kArcLengths), kArcs,
kArcLengths, kExpectedPaths, kExpectedDistances);
}
TYPED_TEST(GraphShortestPathsDeathTest, Multigraph) {
const int kNodes = 4;
const typename TypeParam::NodeIndex kArcs[][2] = {
@@ -600,68 +394,6 @@ TYPED_TEST(GraphShortestPathsDeathTest, Multigraph) {
// Large test on a random strongly connected graph with 10,000,000 nodes and
// 50,000,000 arcs.
// Shortest paths are computed between 10 randomly chosen nodes.
TYPED_TEST(ShortestPathsTest, DISABLED_LargeRandomShortestPaths) {
const int kSize = 10000000;
const int kDegree = 4;
const int max_distance = 50;
const PathDistance kConnectionArcLength = 300;
std::mt19937 randomizer(12345);
TypeParam graph(kSize, kSize + kSize * kDegree);
ZVector<PathDistance> lengths(0, kSize + (kSize * kDegree) - 1);
for (int i = 0; i < kSize; ++i) {
const NodeIndex tail(absl::Uniform(randomizer, 0, kSize));
for (int j = 0; j < kDegree; ++j) {
const NodeIndex head(absl::Uniform(randomizer, 0, kSize));
const PathDistance length =
1 + absl::Uniform(randomizer, 0, max_distance);
lengths.Set(graph.AddArc(tail, head), length);
}
}
for (typename TypeParam::NodeIterator iterator(graph); iterator.Ok();) {
const NodeIndex node_index = iterator.Index();
iterator.Next();
if (iterator.Ok()) {
const NodeIndex next_node_index = iterator.Index();
lengths.Set(graph.AddArc(node_index, next_node_index),
kConnectionArcLength);
} else {
const NodeIndex first_node_index =
typename TypeParam::NodeIterator(graph).Index();
lengths.Set(graph.AddArc(node_index, first_node_index),
kConnectionArcLength);
}
}
std::vector<ConnectedComponent> components;
FindStronglyConnectedComponents(graph, &components);
CHECK_EQ(1, components.size());
CHECK_EQ(kSize, components[0].size());
const int kSourceSize = 10;
const int source_size = std::min(graph.num_nodes(), kSourceSize);
std::vector<NodeIndex> sources(source_size, 0);
for (int i = 0; i < source_size; ++i) {
sources[i] = absl::Uniform(randomizer, 0, graph.num_nodes());
}
const int kThreads = 10;
PathContainer container;
PathContainer::BuildInMemoryCompactPathContainer(&container);
ComputeManyToManyShortestPathsWithMultipleThreads(
graph, lengths, sources, sources, kThreads, &container);
PathContainer distance_container;
PathContainer::BuildPathDistanceContainer(&distance_container);
ComputeManyToManyShortestPathsWithMultipleThreads(
graph, lengths, sources, sources, kThreads, &distance_container);
for (int tail = 0; tail < sources.size(); ++tail) {
for (int head = 0; head < sources.size(); ++head) {
EXPECT_NE(TypeParam::kNilNode, container.GetPenultimateNodeInPath(
sources[tail], sources[head]));
EXPECT_NE(kDisconnectedPathDistance,
container.GetDistance(sources[tail], sources[head]));
EXPECT_NE(kDisconnectedPathDistance,
distance_container.GetDistance(sources[tail], sources[head]));
}
}
}
TYPED_TEST(GraphShortestPathsTest, DISABLED_LargeRandomShortestPaths) {
const int kSize = 10000000;
const int kDegree = 4;

View File

@@ -46,8 +46,8 @@
#include <limits>
#include <vector>
#include "ortools/base/logging.h"
#include "ortools/base/macros.h"
#include "absl/log/check.h"
#include "absl/types/span.h"
// Finds the strongly connected components of a directed graph. It is templated
// so it can be used in many contexts. See the simple example above for the
@@ -70,8 +70,8 @@
// - Its memory usage is also bounded by O(nodes + edges) but in practice it
// uses less than the input graph.
template <typename NodeIndex, typename Graph, typename SccOutput>
void FindStronglyConnectedComponents(const NodeIndex num_nodes,
const Graph& graph, SccOutput* components);
void FindStronglyConnectedComponents(NodeIndex num_nodes, const Graph& graph,
SccOutput* components);
// A simple custom output class that just counts the number of SCC. Not
// allocating many vectors can save both space and speed if your graph is large.
@@ -88,6 +88,7 @@ struct SccCounterOutput {
// This is just here so this class can transparently replace a code that
// use vector<vector<int>> as an SccOutput, and get its size with size().
int size() const { return number_of_components; }
void clear() { number_of_components = 0; }
};
// This implementation is slightly different than a classical iterative version
@@ -112,6 +113,9 @@ class StronglyConnectedComponentsFinder {
node_index_.assign(num_nodes, 0);
node_to_process_.clear();
// Caching the pointer to this vector.data() avoid re-fetching it and help.
absl::Span<NodeIndex> node_index = absl::MakeSpan(node_index_);
// Optimization. This will always be equal to scc_start_index_.back() except
// when scc_stack_ is empty, in which case its value does not matter.
NodeIndex current_scc_start = 0;
@@ -119,23 +123,23 @@ class StronglyConnectedComponentsFinder {
// Loop over all the nodes not yet settled and start a DFS from each of
// them.
for (NodeIndex base_node = 0; base_node < num_nodes; ++base_node) {
if (node_index_[base_node] != 0) continue;
if (node_index[base_node] != 0) continue;
DCHECK_EQ(0, node_to_process_.size());
node_to_process_.push_back(base_node);
do {
const NodeIndex node = node_to_process_.back();
const NodeIndex index = node_index_[node];
const NodeIndex index = node_index[node];
if (index == 0) {
// We continue the dfs from this node and set its 1-based index.
scc_stack_.push_back(node);
current_scc_start = scc_stack_.size();
node_index_[node] = current_scc_start;
node_index[node] = current_scc_start;
scc_start_index_.push_back(current_scc_start);
// Enqueue all its adjacent nodes.
NodeIndex min_head_index = kSettledIndex;
for (const NodeIndex head : graph[node]) {
const NodeIndex head_index = node_index_[head];
const NodeIndex head_index = node_index[head];
if (head_index == 0) {
node_to_process_.push_back(head);
} else {
@@ -159,7 +163,7 @@ class StronglyConnectedComponentsFinder {
components->emplace_back(&scc_stack_[current_scc_start - 1],
&scc_stack_[0] + scc_stack_.size());
for (int i = current_scc_start - 1; i < scc_stack_.size(); ++i) {
node_index_[scc_stack_[i]] = kSettledIndex;
node_index[scc_stack_[i]] = kSettledIndex;
}
scc_stack_.resize(current_scc_start - 1);
scc_start_index_.pop_back();

View File

@@ -12,6 +12,7 @@
# limitations under the License.
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("@rules_python//python:proto.bzl", "py_proto_library")
package(
default_visibility = ["//visibility:public"],
@@ -27,6 +28,11 @@ cc_proto_library(
deps = [":gscip_proto"],
)
py_proto_library(
name = "gscip_proto_py_pb2",
deps = [":gscip_proto"],
)
# NOTE(user): this file should ideally not have a compile time dependency on
# SCIP, so it can be used in client code.
cc_library(

View File

@@ -1001,6 +1001,7 @@ absl::StatusOr<GScipResult> GScip::Solve(
stats->set_total_lp_iterations(SCIPgetNLPIterations(scip_));
stats->set_primal_simplex_iterations(SCIPgetNPrimalLPIterations(scip_));
stats->set_dual_simplex_iterations(SCIPgetNDualLPIterations(scip_));
stats->set_barrier_iterations(SCIPgetNBarrierLPIterations(scip_));
stats->set_deterministic_time(SCIPgetDeterministicTime(scip_));
}
result.gscip_output.set_status(ConvertStatus(SCIPgetStatus(scip_)));

View File

@@ -131,10 +131,16 @@ message GScipSolvingStats {
// Returns +inf for maximization and -inf for minimization if no bound was
// found. Equivalent to SCIPgetDualBound().
double best_bound = 2;
// nprimallpiterations in SCIP. The number of primal simplex LP iterations.
int64 primal_simplex_iterations = 3;
// nduallpiterations in SCIP. The number of dual simplex LP iterations.
int64 dual_simplex_iterations = 4;
// nlp_iterations in SCIP. The total number of LP steps taken, i.e. primal
// simplex iterations + dual simplex iterations + barrier iterations.
// nbarrierlpiterations. The number of barrier LP iterations.
int64 barrier_iterations = 10;
// nlp_iterations in SCIP. The total number of LP steps taken. Note that this
// number be at least (and often strictly greater than)
// primal_simplex_iterations + dual simplex iterations + barrier iterations,
// as it includes other types of iterations (e.g. see ndivinglpiterations).
int64 total_lp_iterations = 5;
// NTotalNodes in SCIP.
// This is the total number of nodes used in the solve, potentially across

View File

@@ -34,3 +34,14 @@ cc_library(
"@com_google_absl//absl/types:optional",
],
)
cc_library(
name = "gurobi_util",
srcs = ["gurobi_util.cc"],
hdrs = ["gurobi_util.h"],
deps = [
":environment",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
],
)

View File

@@ -198,6 +198,18 @@ std::function<int(GRBenv* env, const char* paramname, double* valueP)>
GRBgetdblparam = nullptr;
std::function<int(GRBenv* env, const char* paramname, char* valueP)>
GRBgetstrparam = nullptr;
std::function<int(GRBenv* env, const char* paramname, int* valueP, int* minP,
int* maxP, int* defP)>
GRBgetintparaminfo = nullptr;
std::function<int(GRBenv* env, const char* paramname, double* valueP,
double* minP, double* maxP, double* defP)>
GRBgetdblparaminfo = nullptr;
std::function<int(GRBenv* env, const char* paramname, char* valueP, char* defP)>
GRBgetstrparaminfo = nullptr;
std::function<int(GRBenv* env, const char* paramname)> GRBgetparamtype =
nullptr;
std::function<int(GRBenv* env, int i, char** paramnameP)> GRBgetparamname =
nullptr;
std::function<int(GRBenv* env, const char* paramname, const char* value)>
GRBsetparam = nullptr;
std::function<int(GRBenv* env, const char* paramname, int value)>
@@ -208,6 +220,8 @@ std::function<int(GRBenv* env, const char* paramname, const char* value)>
GRBsetstrparam = nullptr;
std::function<int(GRBenv* env)> GRBresetparams = nullptr;
std::function<int(GRBenv* dest, GRBenv* src)> GRBcopyparams = nullptr;
std::function<int(GRBenv* env)> GRBgetnumparams = nullptr;
std::function<int(GRBenv** envP)> GRBemptyenv = nullptr;
std::function<int(GRBenv** envP, const char* logfilename)> GRBloadenv = nullptr;
std::function<GRBenv*(GRBmodel* model)> GRBgetenv = nullptr;
std::function<void(GRBenv* env)> GRBfreeenv = nullptr;
@@ -309,6 +323,16 @@ void LoadGurobiFunctions(DynamicLibrary* gurobi_dynamic_library) {
gurobi_dynamic_library->GetFunction(&GRBresetparams, "GRBresetparams");
gurobi_dynamic_library->GetFunction(&GRBcopyparams, "GRBcopyparams");
gurobi_dynamic_library->GetFunction(&GRBloadenv, "GRBloadenv");
gurobi_dynamic_library->GetFunction(&GRBemptyenv, "GRBemptyenv");
gurobi_dynamic_library->GetFunction(&GRBgetnumparams, "GRBgetnumparams");
gurobi_dynamic_library->GetFunction(&GRBgetparamname, "GRBgetparamname");
gurobi_dynamic_library->GetFunction(&GRBgetparamtype, "GRBgetparamtype");
gurobi_dynamic_library->GetFunction(&GRBgetintparaminfo,
"GRBgetintparaminfo");
gurobi_dynamic_library->GetFunction(&GRBgetdblparaminfo,
"GRBgetdblparaminfo");
gurobi_dynamic_library->GetFunction(&GRBgetstrparaminfo,
"GRBgetstrparaminfo");
gurobi_dynamic_library->GetFunction(&GRBgetenv, "GRBgetenv");
gurobi_dynamic_library->GetFunction(&GRBfreeenv, "GRBfreeenv");
gurobi_dynamic_library->GetFunction(&GRBgeterrormsg, "GRBgeterrormsg");

View File

@@ -147,106 +147,106 @@ extern std::function<int(void *cbdata, int where, int what, void *resultP)> GRBc
extern std::function<int(void *cbdata, const double *solution, double *objvalP)> GRBcbsolution;
extern std::function<int(void *cbdata, int cutlen, const int *cutind, const double *cutval,char cutsense, double cutrhs)> GRBcbcut;
extern std::function<int(void *cbdata, int lazylen, const int *lazyind,const double *lazyval, char lazysense, double lazyrhs)> GRBcblazy;
#define GRB_INT_ATTR_NUMCONSTRS "NumConstrs"
#define GRB_INT_ATTR_NUMVARS "NumVars"
#define GRB_INT_ATTR_NUMSOS "NumSOS"
#define GRB_INT_ATTR_NUMQCONSTRS "NumQConstrs"
#define GRB_INT_ATTR_NUMGENCONSTRS "NumGenConstrs"
#define GRB_INT_ATTR_NUMNZS "NumNZs"
#define GRB_DBL_ATTR_DNUMNZS "DNumNZs"
#define GRB_INT_ATTR_NUMQNZS "NumQNZs"
#define GRB_INT_ATTR_NUMQCNZS "NumQCNZs"
#define GRB_INT_ATTR_NUMINTVARS "NumIntVars"
#define GRB_INT_ATTR_NUMBINVARS "NumBinVars"
#define GRB_INT_ATTR_NUMPWLOBJVARS "NumPWLObjVars"
#define GRB_STR_ATTR_MODELNAME "ModelName"
#define GRB_INT_ATTR_MODELSENSE "ModelSense"
#define GRB_DBL_ATTR_OBJCON "ObjCon"
#define GRB_INT_ATTR_IS_MIP "IsMIP"
#define GRB_INT_ATTR_IS_QP "IsQP"
#define GRB_INT_ATTR_IS_QCP "IsQCP"
#define GRB_INT_ATTR_IS_MULTIOBJ "IsMultiObj"
#define GRB_INT_ATTR_LICENSE_EXPIRATION "LicenseExpiration"
#define GRB_INT_ATTR_NUMTAGGED "NumTagged"
#define GRB_INT_ATTR_FINGERPRINT "Fingerprint"
#define GRB_INT_ATTR_NUMCONSTRS "NumConstrs"
#define GRB_INT_ATTR_NUMVARS "NumVars"
#define GRB_INT_ATTR_NUMSOS "NumSOS"
#define GRB_INT_ATTR_NUMQCONSTRS "NumQConstrs"
#define GRB_INT_ATTR_NUMGENCONSTRS "NumGenConstrs"
#define GRB_INT_ATTR_NUMNZS "NumNZs"
#define GRB_DBL_ATTR_DNUMNZS "DNumNZs"
#define GRB_INT_ATTR_NUMQNZS "NumQNZs"
#define GRB_INT_ATTR_NUMQCNZS "NumQCNZs"
#define GRB_INT_ATTR_NUMINTVARS "NumIntVars"
#define GRB_INT_ATTR_NUMBINVARS "NumBinVars"
#define GRB_INT_ATTR_NUMPWLOBJVARS "NumPWLObjVars"
#define GRB_STR_ATTR_MODELNAME "ModelName"
#define GRB_INT_ATTR_MODELSENSE "ModelSense"
#define GRB_DBL_ATTR_OBJCON "ObjCon"
#define GRB_INT_ATTR_IS_MIP "IsMIP"
#define GRB_INT_ATTR_IS_QP "IsQP"
#define GRB_INT_ATTR_IS_QCP "IsQCP"
#define GRB_INT_ATTR_IS_MULTIOBJ "IsMultiObj"
#define GRB_INT_ATTR_LICENSE_EXPIRATION "LicenseExpiration"
#define GRB_INT_ATTR_NUMTAGGED "NumTagged"
#define GRB_INT_ATTR_FINGERPRINT "Fingerprint"
#define GRB_INT_ATTR_BATCHERRORCODE "BatchErrorCode"
#define GRB_STR_ATTR_BATCHERRORMESSAGE "BatchErrorMessage"
#define GRB_STR_ATTR_BATCHID "BatchID"
#define GRB_INT_ATTR_BATCHSTATUS "BatchStatus"
#define GRB_DBL_ATTR_LB "LB"
#define GRB_DBL_ATTR_UB "UB"
#define GRB_DBL_ATTR_OBJ "Obj"
#define GRB_CHAR_ATTR_VTYPE "VType"
#define GRB_DBL_ATTR_START "Start"
#define GRB_DBL_ATTR_PSTART "PStart"
#define GRB_INT_ATTR_BRANCHPRIORITY "BranchPriority"
#define GRB_STR_ATTR_VARNAME "VarName"
#define GRB_INT_ATTR_PWLOBJCVX "PWLObjCvx"
#define GRB_DBL_ATTR_VARHINTVAL "VarHintVal"
#define GRB_INT_ATTR_VARHINTPRI "VarHintPri"
#define GRB_INT_ATTR_PARTITION "Partition"
#define GRB_INT_ATTR_POOLIGNORE "PoolIgnore"
#define GRB_STR_ATTR_VTAG "VTag"
#define GRB_STR_ATTR_CTAG "CTag"
#define GRB_DBL_ATTR_RHS "RHS"
#define GRB_DBL_ATTR_DSTART "DStart"
#define GRB_CHAR_ATTR_SENSE "Sense"
#define GRB_STR_ATTR_CONSTRNAME "ConstrName"
#define GRB_INT_ATTR_LAZY "Lazy"
#define GRB_STR_ATTR_QCTAG "QCTag"
#define GRB_DBL_ATTR_QCRHS "QCRHS"
#define GRB_CHAR_ATTR_QCSENSE "QCSense"
#define GRB_STR_ATTR_QCNAME "QCName"
#define GRB_INT_ATTR_GENCONSTRTYPE "GenConstrType"
#define GRB_STR_ATTR_GENCONSTRNAME "GenConstrName"
#define GRB_INT_ATTR_FUNCPIECES "FuncPieces"
#define GRB_DBL_ATTR_FUNCPIECEERROR "FuncPieceError"
#define GRB_DBL_ATTR_FUNCPIECELENGTH "FuncPieceLength"
#define GRB_DBL_ATTR_FUNCPIECERATIO "FuncPieceRatio"
#define GRB_DBL_ATTR_MAX_COEFF "MaxCoeff"
#define GRB_DBL_ATTR_MIN_COEFF "MinCoeff"
#define GRB_DBL_ATTR_MAX_BOUND "MaxBound"
#define GRB_DBL_ATTR_MIN_BOUND "MinBound"
#define GRB_DBL_ATTR_MAX_OBJ_COEFF "MaxObjCoeff"
#define GRB_DBL_ATTR_MIN_OBJ_COEFF "MinObjCoeff"
#define GRB_DBL_ATTR_MAX_RHS "MaxRHS"
#define GRB_DBL_ATTR_MIN_RHS "MinRHS"
#define GRB_DBL_ATTR_MAX_QCCOEFF "MaxQCCoeff"
#define GRB_DBL_ATTR_MIN_QCCOEFF "MinQCCoeff"
#define GRB_DBL_ATTR_MAX_QOBJ_COEFF "MaxQObjCoeff"
#define GRB_DBL_ATTR_MIN_QOBJ_COEFF "MinQObjCoeff"
#define GRB_DBL_ATTR_MAX_QCLCOEFF "MaxQCLCoeff"
#define GRB_DBL_ATTR_MIN_QCLCOEFF "MinQCLCoeff"
#define GRB_DBL_ATTR_MAX_QCRHS "MaxQCRHS"
#define GRB_DBL_ATTR_MIN_QCRHS "MinQCRHS"
#define GRB_DBL_ATTR_RUNTIME "Runtime"
#define GRB_DBL_ATTR_WORK "Work"
#define GRB_INT_ATTR_STATUS "Status"
#define GRB_DBL_ATTR_OBJVAL "ObjVal"
#define GRB_DBL_ATTR_OBJBOUND "ObjBound"
#define GRB_DBL_ATTR_OBJBOUNDC "ObjBoundC"
#define GRB_DBL_ATTR_POOLOBJBOUND "PoolObjBound"
#define GRB_DBL_ATTR_POOLOBJVAL "PoolObjVal"
#define GRB_DBL_ATTR_MIPGAP "MIPGap"
#define GRB_INT_ATTR_SOLCOUNT "SolCount"
#define GRB_DBL_ATTR_ITERCOUNT "IterCount"
#define GRB_INT_ATTR_BARITERCOUNT "BarIterCount"
#define GRB_DBL_ATTR_NODECOUNT "NodeCount"
#define GRB_DBL_ATTR_OPENNODECOUNT "OpenNodeCount"
#define GRB_INT_ATTR_HASDUALNORM "HasDualNorm"
#define GRB_INT_ATTR_CONCURRENTWINMETHOD "ConcurrentWinMethod"
#define GRB_DBL_ATTR_X "X"
#define GRB_DBL_ATTR_XN "Xn"
#define GRB_DBL_ATTR_BARX "BarX"
#define GRB_DBL_ATTR_RC "RC"
#define GRB_DBL_ATTR_VDUALNORM "VDualNorm"
#define GRB_INT_ATTR_VBASIS "VBasis"
#define GRB_DBL_ATTR_PI "Pi"
#define GRB_DBL_ATTR_QCPI "QCPi"
#define GRB_DBL_ATTR_SLACK "Slack"
#define GRB_DBL_ATTR_QCSLACK "QCSlack"
#define GRB_DBL_ATTR_CDUALNORM "CDualNorm"
#define GRB_INT_ATTR_CBASIS "CBasis"
#define GRB_DBL_ATTR_LB "LB"
#define GRB_DBL_ATTR_UB "UB"
#define GRB_DBL_ATTR_OBJ "Obj"
#define GRB_CHAR_ATTR_VTYPE "VType"
#define GRB_DBL_ATTR_START "Start"
#define GRB_DBL_ATTR_PSTART "PStart"
#define GRB_INT_ATTR_BRANCHPRIORITY "BranchPriority"
#define GRB_STR_ATTR_VARNAME "VarName"
#define GRB_INT_ATTR_PWLOBJCVX "PWLObjCvx"
#define GRB_DBL_ATTR_VARHINTVAL "VarHintVal"
#define GRB_INT_ATTR_VARHINTPRI "VarHintPri"
#define GRB_INT_ATTR_PARTITION "Partition"
#define GRB_INT_ATTR_POOLIGNORE "PoolIgnore"
#define GRB_STR_ATTR_VTAG "VTag"
#define GRB_STR_ATTR_CTAG "CTag"
#define GRB_DBL_ATTR_RHS "RHS"
#define GRB_DBL_ATTR_DSTART "DStart"
#define GRB_CHAR_ATTR_SENSE "Sense"
#define GRB_STR_ATTR_CONSTRNAME "ConstrName"
#define GRB_INT_ATTR_LAZY "Lazy"
#define GRB_STR_ATTR_QCTAG "QCTag"
#define GRB_DBL_ATTR_QCRHS "QCRHS"
#define GRB_CHAR_ATTR_QCSENSE "QCSense"
#define GRB_STR_ATTR_QCNAME "QCName"
#define GRB_INT_ATTR_GENCONSTRTYPE "GenConstrType"
#define GRB_STR_ATTR_GENCONSTRNAME "GenConstrName"
#define GRB_INT_ATTR_FUNCPIECES "FuncPieces"
#define GRB_DBL_ATTR_FUNCPIECEERROR "FuncPieceError"
#define GRB_DBL_ATTR_FUNCPIECELENGTH "FuncPieceLength"
#define GRB_DBL_ATTR_FUNCPIECERATIO "FuncPieceRatio"
#define GRB_DBL_ATTR_MAX_COEFF "MaxCoeff"
#define GRB_DBL_ATTR_MIN_COEFF "MinCoeff"
#define GRB_DBL_ATTR_MAX_BOUND "MaxBound"
#define GRB_DBL_ATTR_MIN_BOUND "MinBound"
#define GRB_DBL_ATTR_MAX_OBJ_COEFF "MaxObjCoeff"
#define GRB_DBL_ATTR_MIN_OBJ_COEFF "MinObjCoeff"
#define GRB_DBL_ATTR_MAX_RHS "MaxRHS"
#define GRB_DBL_ATTR_MIN_RHS "MinRHS"
#define GRB_DBL_ATTR_MAX_QCCOEFF "MaxQCCoeff"
#define GRB_DBL_ATTR_MIN_QCCOEFF "MinQCCoeff"
#define GRB_DBL_ATTR_MAX_QOBJ_COEFF "MaxQObjCoeff"
#define GRB_DBL_ATTR_MIN_QOBJ_COEFF "MinQObjCoeff"
#define GRB_DBL_ATTR_MAX_QCLCOEFF "MaxQCLCoeff"
#define GRB_DBL_ATTR_MIN_QCLCOEFF "MinQCLCoeff"
#define GRB_DBL_ATTR_MAX_QCRHS "MaxQCRHS"
#define GRB_DBL_ATTR_MIN_QCRHS "MinQCRHS"
#define GRB_DBL_ATTR_RUNTIME "Runtime"
#define GRB_DBL_ATTR_WORK "Work"
#define GRB_INT_ATTR_STATUS "Status"
#define GRB_DBL_ATTR_OBJVAL "ObjVal"
#define GRB_DBL_ATTR_OBJBOUND "ObjBound"
#define GRB_DBL_ATTR_OBJBOUNDC "ObjBoundC"
#define GRB_DBL_ATTR_POOLOBJBOUND "PoolObjBound"
#define GRB_DBL_ATTR_POOLOBJVAL "PoolObjVal"
#define GRB_DBL_ATTR_MIPGAP "MIPGap"
#define GRB_INT_ATTR_SOLCOUNT "SolCount"
#define GRB_DBL_ATTR_ITERCOUNT "IterCount"
#define GRB_INT_ATTR_BARITERCOUNT "BarIterCount"
#define GRB_DBL_ATTR_NODECOUNT "NodeCount"
#define GRB_DBL_ATTR_OPENNODECOUNT "OpenNodeCount"
#define GRB_INT_ATTR_HASDUALNORM "HasDualNorm"
#define GRB_INT_ATTR_CONCURRENTWINMETHOD "ConcurrentWinMethod"
#define GRB_DBL_ATTR_X "X"
#define GRB_DBL_ATTR_XN "Xn"
#define GRB_DBL_ATTR_BARX "BarX"
#define GRB_DBL_ATTR_RC "RC"
#define GRB_DBL_ATTR_VDUALNORM "VDualNorm"
#define GRB_INT_ATTR_VBASIS "VBasis"
#define GRB_DBL_ATTR_PI "Pi"
#define GRB_DBL_ATTR_QCPI "QCPi"
#define GRB_DBL_ATTR_SLACK "Slack"
#define GRB_DBL_ATTR_QCSLACK "QCSlack"
#define GRB_DBL_ATTR_CDUALNORM "CDualNorm"
#define GRB_INT_ATTR_CBASIS "CBasis"
#define GRB_DBL_ATTR_MAX_VIO "MaxVio"
#define GRB_DBL_ATTR_BOUND_VIO "BoundVio"
#define GRB_DBL_ATTR_BOUND_SVIO "BoundSVio"
@@ -295,19 +295,19 @@ extern std::function<int(void *cbdata, int lazylen, const int *lazyind,const dou
#define GRB_DBL_ATTR_SA_UBUP "SAUBUp"
#define GRB_DBL_ATTR_SA_RHSLOW "SARHSLow"
#define GRB_DBL_ATTR_SA_RHSUP "SARHSUp"
#define GRB_INT_ATTR_IIS_MINIMAL "IISMinimal"
#define GRB_INT_ATTR_IIS_LB "IISLB"
#define GRB_INT_ATTR_IIS_UB "IISUB"
#define GRB_INT_ATTR_IIS_CONSTR "IISConstr"
#define GRB_INT_ATTR_IIS_SOS "IISSOS"
#define GRB_INT_ATTR_IIS_QCONSTR "IISQConstr"
#define GRB_INT_ATTR_IIS_GENCONSTR "IISGenConstr"
#define GRB_INT_ATTR_IIS_LBFORCE "IISLBForce"
#define GRB_INT_ATTR_IIS_UBFORCE "IISUBForce"
#define GRB_INT_ATTR_IIS_CONSTRFORCE "IISConstrForce"
#define GRB_INT_ATTR_IIS_SOSFORCE "IISSOSForce"
#define GRB_INT_ATTR_IIS_QCONSTRFORCE "IISQConstrForce"
#define GRB_INT_ATTR_IIS_GENCONSTRFORCE "IISGenConstrForce"
#define GRB_INT_ATTR_IIS_MINIMAL "IISMinimal"
#define GRB_INT_ATTR_IIS_LB "IISLB"
#define GRB_INT_ATTR_IIS_UB "IISUB"
#define GRB_INT_ATTR_IIS_CONSTR "IISConstr"
#define GRB_INT_ATTR_IIS_SOS "IISSOS"
#define GRB_INT_ATTR_IIS_QCONSTR "IISQConstr"
#define GRB_INT_ATTR_IIS_GENCONSTR "IISGenConstr"
#define GRB_INT_ATTR_IIS_LBFORCE "IISLBForce"
#define GRB_INT_ATTR_IIS_UBFORCE "IISUBForce"
#define GRB_INT_ATTR_IIS_CONSTRFORCE "IISConstrForce"
#define GRB_INT_ATTR_IIS_SOSFORCE "IISSOSForce"
#define GRB_INT_ATTR_IIS_QCONSTRFORCE "IISQConstrForce"
#define GRB_INT_ATTR_IIS_GENCONSTRFORCE "IISGenConstrForce"
#define GRB_INT_ATTR_TUNE_RESULTCOUNT "TuneResultCount"
#define GRB_DBL_ATTR_FARKASDUAL "FarkasDual"
#define GRB_DBL_ATTR_FARKASPROOF "FarkasProof"
@@ -316,25 +316,25 @@ extern std::function<int(void *cbdata, int lazylen, const int *lazyind,const dou
#define GRB_INT_ATTR_UNBDVAR "UnbdVar"
#define GRB_INT_ATTR_VARPRESTAT "VarPreStat"
#define GRB_DBL_ATTR_PREFIXVAL "PreFixVal"
#define GRB_DBL_ATTR_OBJN "ObjN"
#define GRB_DBL_ATTR_OBJNVAL "ObjNVal"
#define GRB_DBL_ATTR_OBJNCON "ObjNCon"
#define GRB_DBL_ATTR_OBJNWEIGHT "ObjNWeight"
#define GRB_INT_ATTR_OBJNPRIORITY "ObjNPriority"
#define GRB_DBL_ATTR_OBJNRELTOL "ObjNRelTol"
#define GRB_DBL_ATTR_OBJNABSTOL "ObjNAbsTol"
#define GRB_STR_ATTR_OBJNNAME "ObjNName"
#define GRB_DBL_ATTR_SCENNLB "ScenNLB"
#define GRB_DBL_ATTR_SCENNUB "ScenNUB"
#define GRB_DBL_ATTR_SCENNOBJ "ScenNObj"
#define GRB_DBL_ATTR_SCENNRHS "ScenNRHS"
#define GRB_STR_ATTR_SCENNNAME "ScenNName"
#define GRB_DBL_ATTR_SCENNX "ScenNX"
#define GRB_DBL_ATTR_SCENNOBJBOUND "ScenNObjBound"
#define GRB_DBL_ATTR_SCENNOBJVAL "ScenNObjVal"
#define GRB_INT_ATTR_NUMOBJ "NumObj"
#define GRB_INT_ATTR_NUMSCENARIOS "NumScenarios"
#define GRB_INT_ATTR_NUMSTART "NumStart"
#define GRB_DBL_ATTR_OBJN "ObjN"
#define GRB_DBL_ATTR_OBJNVAL "ObjNVal"
#define GRB_DBL_ATTR_OBJNCON "ObjNCon"
#define GRB_DBL_ATTR_OBJNWEIGHT "ObjNWeight"
#define GRB_INT_ATTR_OBJNPRIORITY "ObjNPriority"
#define GRB_DBL_ATTR_OBJNRELTOL "ObjNRelTol"
#define GRB_DBL_ATTR_OBJNABSTOL "ObjNAbsTol"
#define GRB_STR_ATTR_OBJNNAME "ObjNName"
#define GRB_DBL_ATTR_SCENNLB "ScenNLB"
#define GRB_DBL_ATTR_SCENNUB "ScenNUB"
#define GRB_DBL_ATTR_SCENNOBJ "ScenNObj"
#define GRB_DBL_ATTR_SCENNRHS "ScenNRHS"
#define GRB_STR_ATTR_SCENNNAME "ScenNName"
#define GRB_DBL_ATTR_SCENNX "ScenNX"
#define GRB_DBL_ATTR_SCENNOBJBOUND "ScenNObjBound"
#define GRB_DBL_ATTR_SCENNOBJVAL "ScenNObjVal"
#define GRB_INT_ATTR_NUMOBJ "NumObj"
#define GRB_INT_ATTR_NUMSCENARIOS "NumScenarios"
#define GRB_INT_ATTR_NUMSTART "NumStart"
#define GRB_GENCONSTR_MAX 0
#define GRB_GENCONSTR_MIN 1
#define GRB_GENCONSTR_ABS 2
@@ -714,6 +714,13 @@ extern std::function<int(GRBenv *env, const char *paramname, const char *value)>
extern std::function<int(GRBenv *env)> GRBresetparams;
extern std::function<int(GRBenv *dest, GRBenv *src)> GRBcopyparams;
extern std::function<int(GRBenv **envP, const char *logfilename)> GRBloadenv;
extern std::function<int(GRBenv **envP)> GRBemptyenv;
extern std::function<int(GRBenv *envP)> GRBgetnumparams;
extern std::function<int(GRBenv *envP, int i, char **paramnameP)> GRBgetparamname;
extern std::function<int(GRBenv *envP, const char *paramname)> GRBgetparamtype;
extern std::function<int(GRBenv *envP, const char *paramname, int *valueP, int *minP, int *maxP, int *defP)> GRBgetintparaminfo;
extern std::function<int(GRBenv *envP, const char *paramname, double *valueP, double *minP, double *maxP, double *defP)> GRBgetdblparaminfo;
extern std::function<int(GRBenv *envP, const char *paramname, char *valueP, char *defP)> GRBgetstrparaminfo;
extern std::function<GRBenv *(GRBmodel *model)> GRBgetenv;
extern std::function<void(GRBenv *env)> GRBfreeenv;
extern std::function<const char *(GRBenv *env)> GRBgeterrormsg;

View File

@@ -0,0 +1,98 @@
// Copyright 2010-2022 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/gurobi/gurobi_util.h"
#include <cstring>
#include <string>
#include <vector>
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "ortools/gurobi/environment.h"
namespace operations_research {
std::string GurobiParamInfoForLogging(GRBenv* grb, bool one_liner_output) {
const absl::ParsedFormat<'s', 's', 's'> kExtendedFormat(
" Parameter: '%s' value: %s default: %s");
const absl::ParsedFormat<'s', 's', 's'> kOneLinerFormat("'%s':%s (%s)");
const absl::ParsedFormat<'s', 's', 's'>& format =
one_liner_output ? kOneLinerFormat : kExtendedFormat;
std::vector<std::string> changed_parameters;
const int num_parameters = GRBgetnumparams(grb);
for (int i = 0; i < num_parameters; ++i) {
char* param_name = nullptr;
GRBgetparamname(grb, i, &param_name);
const int param_type = GRBgetparamtype(grb, param_name);
switch (param_type) {
case 1: // integer parameters.
{
int default_value;
int min_value;
int max_value;
int current_value;
GRBgetintparaminfo(grb, param_name, &current_value, &min_value,
&max_value, &default_value);
if (current_value != default_value) {
changed_parameters.push_back(
absl::StrFormat(format, param_name, absl::StrCat(current_value),
absl::StrCat(default_value)));
}
break;
}
case 2: // double parameters.
{
double default_value;
double min_value;
double max_value;
double current_value;
GRBgetdblparaminfo(grb, param_name, &current_value, &min_value,
&max_value, &default_value);
if (current_value != default_value) {
changed_parameters.push_back(
absl::StrFormat(format, param_name, absl::StrCat(current_value),
absl::StrCat(default_value)));
}
break;
}
case 3: // string parameters.
{
char current_value[GRB_MAX_STRLEN + 1];
char default_value[GRB_MAX_STRLEN + 1];
GRBgetstrparaminfo(grb, param_name, current_value, default_value);
// This ensure that strcmp does not go beyond the end of the char
// array.
current_value[GRB_MAX_STRLEN] = '\0';
default_value[GRB_MAX_STRLEN] = '\0';
if (std::strcmp(current_value, default_value) != 0) {
changed_parameters.push_back(absl::StrFormat(
format, param_name, current_value, default_value));
}
break;
}
default: // unknown parameter types
changed_parameters.push_back(absl::StrFormat(
"Parameter '%s' of unknown type %d", param_name, param_type));
}
}
if (changed_parameters.empty()) return "";
if (one_liner_output) {
return absl::StrCat("GurobiParams{",
absl::StrJoin(changed_parameters, ", "), "}");
}
return absl::StrJoin(changed_parameters, "\n");
}
} // namespace operations_research

View File

@@ -0,0 +1,31 @@
// Copyright 2010-2022 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 OR_TOOLS_GUROBI_GUROBI_UTIL_H_
#define OR_TOOLS_GUROBI_GUROBI_UTIL_H_
#include <string>
#include "ortools/gurobi/environment.h"
namespace operations_research {
// Returns a human-readable listing of all gurobi parameters that are set to
// non-default values, and their current value in the given environment. If all
// parameters are at their default value, returns the empty string.
// To produce a one-liner string, use `one_liner_output=true`.
std::string GurobiParamInfoForLogging(GRBenv* grb,
bool one_liner_output = false);
} // namespace operations_research
#endif // OR_TOOLS_GUROBI_GUROBI_UTIL_H_

View File

@@ -11,11 +11,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_python//python:proto.bzl", "py_proto_library")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
package(default_visibility = ["//visibility:public"])
@@ -84,7 +84,7 @@ config_setting(
bool_flag(
name = "with_highs",
build_setting_default = True,
build_setting_default = False,
)
config_setting(
@@ -251,7 +251,12 @@ cc_library(
"//ortools/base:timer",
"//ortools/gurobi:environment",
"//ortools/xpress:environment",
"//ortools/linear_solver/proto_solver",
"//ortools/gurobi:gurobi_util",
"//ortools/linear_solver/proto_solver:glop_proto_solver",
"//ortools/linear_solver/proto_solver:gurobi_proto_solver",
"//ortools/linear_solver/proto_solver:pdlp_proto_solver",
"//ortools/linear_solver/proto_solver:sat_proto_solver",
"//ortools/linear_solver/proto_solver:scip_proto_solver",
"//ortools/port:file",
"//ortools/port:proto_utils",
"//ortools/sat:cp_model_cc_proto",

View File

@@ -20,6 +20,7 @@
#include <vector>
#include "absl/base/attributes.h"
#include "absl/log/check.h"
#include "ortools/base/logging.h"
#include "ortools/glop/lp_solver.h"
#include "ortools/glop/parameters.pb.h"
@@ -145,7 +146,7 @@ MPSolver::ResultStatus GLOPInterface::Solve(const MPSolverParameters& param) {
result_status_ = GlopToMPSolverResultStatus(status);
objective_value_ = lp_solver_.GetObjectiveValue();
const size_t num_vars = solver_->variables_.size();
const int num_vars = solver_->variables_.size();
column_status_.resize(num_vars, MPSolver::FREE);
for (int var_id = 0; var_id < num_vars; ++var_id) {
MPVariable* const var = solver_->variables_[var_id];
@@ -164,7 +165,7 @@ MPSolver::ResultStatus GLOPInterface::Solve(const MPSolverParameters& param) {
column_status_.at(var_id) = GlopToMPSolverVariableStatus(variable_status);
}
const size_t num_constraints = solver_->constraints_.size();
const int num_constraints = solver_->constraints_.size();
row_status_.resize(num_constraints, MPSolver::FREE);
for (int ct_id = 0; ct_id < num_constraints; ++ct_id) {
MPConstraint* const ct = solver_->constraints_[ct_id];

View File

@@ -47,6 +47,7 @@
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <limits>
#include <memory>
#include <optional>
@@ -65,6 +66,7 @@
#include "ortools/base/map_util.h"
#include "ortools/base/timer.h"
#include "ortools/gurobi/environment.h"
#include "ortools/gurobi/gurobi_util.h"
#include "ortools/linear_solver/linear_solver.h"
#include "ortools/linear_solver/linear_solver_callback.h"
#include "ortools/linear_solver/proto_solver/gurobi_proto_solver.h"
@@ -1222,6 +1224,12 @@ MPSolver::ResultStatus GurobiInterface::Solve(const MPSolverParameters& param) {
CheckedGurobiCall(GRBsetintparam(
GRBgetenv(model_), GRB_INT_PAR_LAZYCONSTRAINTS, gurobi_lazy_constraint));
// Logs all parameters not at default values in the model environment.
if (!quiet()) {
std::cout << GurobiParamInfoForLogging(GRBgetenv(model_),
/*one_liner_output=*/true);
}
// Solve
timer.Restart();
const int status = GRBoptimize(model_);

View File

@@ -13,64 +13,159 @@
package(default_visibility = ["//visibility:public"])
# This works on a fixed set of solvers.
# By default SCIP, GUROBI, PDLP, and CP-SAT interface are included.
cc_library(
name = "proto_solver",
srcs = [
"gurobi_proto_solver.cc",
"highs_proto_solver.cc",
"pdlp_proto_solver.cc",
"sat_proto_solver.cc",
"sat_solver_utils.cc",
"scip_proto_solver.cc",
],
hdrs = [
"gurobi_proto_solver.h",
"highs_proto_solver.h",
"pdlp_proto_solver.h",
"sat_proto_solver.h",
"sat_solver_utils.h",
"scip_proto_solver.h",
],
copts = [
"-DUSE_PDLP",
"-DUSE_SCIP",
"-DUSE_HIGHS",
],
name = "proto_utils",
hdrs = ["proto_utils.h"],
visibility = ["//visibility:public"],
deps = [
"//ortools/base",
"//ortools/base:accurate_sum",
"//ortools/base:dynamic_library",
"//ortools/base:hash",
"//ortools/base:map_util",
"//ortools/base:status_macros",
"//ortools/base:stl_util",
"//ortools/base:timer",
"//ortools/bop:bop_parameters_cc_proto",
"//ortools/bop:integral_solver",
"//ortools/port:proto_utils",
"@com_google_absl//absl/log:check",
"@com_google_protobuf//:protobuf",
],
)
cc_library(
name = "glop_proto_solver",
srcs = ["glop_proto_solver.cc"],
hdrs = ["glop_proto_solver.h"],
deps = [
":proto_utils",
"//ortools/glop:lp_solver",
"//ortools/glop:parameters_cc_proto",
"//ortools/gscip:legacy_scip_params",
"//ortools/gurobi:environment",
"//ortools/glop:parameters_validation",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/linear_solver:model_exporter",
"//ortools/linear_solver:model_validator",
"//ortools/linear_solver:scip_with_glop",
"//ortools/lp_data",
"//ortools/lp_data:base",
"//ortools/lp_data:proto_utils",
"//ortools/port:proto_utils",
"//ortools/util:logging",
"//ortools/util:time_limit",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "pdlp_proto_solver",
srcs = ["pdlp_proto_solver.cc"],
hdrs = ["pdlp_proto_solver.h"],
deps = [
"//ortools/base:logging",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/linear_solver:model_validator",
"//ortools/pdlp:iteration_stats",
"//ortools/pdlp:primal_dual_hybrid_gradient",
"//ortools/pdlp:quadratic_program",
"//ortools/pdlp:solve_log_cc_proto",
"//ortools/pdlp:solvers_cc_proto",
"//ortools/port:file",
"//ortools/port:proto_utils",
"//ortools/util:lazy_mutable_copy",
"@com_google_absl//absl/status:statusor",
],
)
cc_library(
name = "sat_solver_utils",
srcs = ["sat_solver_utils.cc"],
hdrs = ["sat_solver_utils.h"],
deps = [
"//ortools/glop:parameters_cc_proto",
"//ortools/glop:preprocessor",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/lp_data:proto_utils",
"//ortools/util:logging",
"@com_google_absl//absl/memory",
],
)
cc_library(
name = "sat_proto_solver",
srcs = ["sat_proto_solver.cc"],
hdrs = ["sat_proto_solver.h"],
deps = [
":proto_utils",
":sat_solver_utils",
"//ortools/glop:preprocessor",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/linear_solver:model_validator",
"//ortools/lp_data",
"//ortools/lp_data:base",
"//ortools/port:proto_utils",
"//ortools/sat:cp_model_cc_proto",
"//ortools/sat:cp_model_solver",
"//ortools/sat:lp_utils",
"//ortools/util:fp_utils",
"//ortools/sat:model",
"//ortools/sat:parameters_validation",
"//ortools/sat:sat_parameters_cc_proto",
"//ortools/util:logging",
"//ortools/util:time_limit",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/types:span",
],
)
cc_library(
name = "scip_proto_solver",
srcs = ["scip_proto_solver.cc"],
hdrs = ["scip_proto_solver.h"],
defines = ["USE_SCIP"],
deps = [
"//ortools/base",
"//ortools/base:cleanup",
"//ortools/base:timer",
"//ortools/gscip:legacy_scip_params",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/linear_solver:model_validator",
"//ortools/linear_solver:scip_helper_macros",
"//ortools/util:lazy_mutable_copy",
"@com_google_absl//absl/cleanup",
"@com_google_absl//absl/container:btree",
"@com_google_absl//absl/log",
"@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/strings:str_format",
"@com_google_absl//absl/time",
"@scip//:libscip",
],
)
cc_library(
name = "gurobi_proto_solver",
srcs = ["gurobi_proto_solver.cc"],
hdrs = ["gurobi_proto_solver.h"],
deps = [
"//ortools/base:cleanup",
"//ortools/base:timer",
"//ortools/gurobi:environment",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/linear_solver:model_validator",
"//ortools/util:lazy_mutable_copy",
"@com_google_absl//absl/base:core_headers",
"@com_google_absl//absl/cleanup",
"@com_google_absl//absl/log",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/time",
"@com_google_absl//absl/types:optional",
],
)
cc_library(
name = "highs_proto_solver",
srcs = ["highs_proto_solver.cc"],
hdrs = ["highs_proto_solver.h"],
deps = [
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/linear_solver:model_validator",
"//ortools/port:proto_utils",
"@com_google_absl//absl/status:statusor",
],
)

View File

@@ -0,0 +1,221 @@
// Copyright 2010-2022 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/linear_solver/proto_solver/glop_proto_solver.h"
#include <atomic>
#include <cstdlib>
#include <functional>
#include <memory>
#include <string>
#include <type_traits>
#include "absl/log/check.h"
#include "absl/strings/str_cat.h"
#include "ortools/base/logging.h"
#include "ortools/glop/lp_solver.h"
#include "ortools/glop/parameters_validation.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/linear_solver/model_validator.h"
#include "ortools/linear_solver/proto_solver/proto_utils.h"
#include "ortools/lp_data/lp_data.h"
#include "ortools/lp_data/lp_types.h"
#include "ortools/lp_data/proto_utils.h"
#include "ortools/port/proto_utils.h"
#include "ortools/util/logging.h"
#include "ortools/util/time_limit.h"
namespace operations_research {
namespace {
MPSolutionResponse ModelInvalidResponse(SolverLogger& logger,
std::string message) {
SOLVER_LOG(&logger, "Invalid model/parameters in glop_solve_proto.\n",
message);
MPSolutionResponse response;
response.set_status(MPSolverResponseStatus::MPSOLVER_MODEL_INVALID);
response.set_status_str(message);
return response;
}
MPSolverResponseStatus ToMPSolverResultStatus(glop::ProblemStatus s) {
switch (s) {
case glop::ProblemStatus::OPTIMAL:
return MPSOLVER_OPTIMAL;
case glop::ProblemStatus::PRIMAL_FEASIBLE:
return MPSOLVER_FEASIBLE;
// Note(user): MPSolver does not have the equivalent of
// INFEASIBLE_OR_UNBOUNDED however UNBOUNDED is almost never relevant in
// applications, so we decided to report this status as INFEASIBLE since
// it should almost always be the case. Historically, we where reporting
// ABNORMAL, but that was more confusing than helpful.
//
// TODO(user): We could argue that it is infeasible to find the optimal of
// an unbounded problem. So it might just be simpler to completely get rid
// of the MpSolver::UNBOUNDED status that seems to never be used
// programmatically.
case glop::ProblemStatus::INFEASIBLE_OR_UNBOUNDED: // PASS_THROUGH_INTENDED
case glop::ProblemStatus::PRIMAL_INFEASIBLE: // PASS_THROUGH_INTENDED
case glop::ProblemStatus::DUAL_UNBOUNDED:
return MPSOLVER_INFEASIBLE;
case glop::ProblemStatus::DUAL_INFEASIBLE: // PASS_THROUGH_INTENDED
case glop::ProblemStatus::PRIMAL_UNBOUNDED:
return MPSOLVER_UNBOUNDED;
case glop::ProblemStatus::DUAL_FEASIBLE: // PASS_THROUGH_INTENDED
case glop::ProblemStatus::INIT:
return MPSOLVER_NOT_SOLVED;
case glop::ProblemStatus::ABNORMAL: // PASS_THROUGH_INTENDED
case glop::ProblemStatus::IMPRECISE: // PASS_THROUGH_INTENDED
case glop::ProblemStatus::INVALID_PROBLEM:
return MPSOLVER_ABNORMAL;
}
LOG(DFATAL) << "Invalid glop::ProblemStatus " << s;
return MPSOLVER_ABNORMAL;
}
} // namespace
MPSolutionResponse GlopSolveProto(
MPModelRequest request, std::atomic<bool>* interrupt_solve,
std::function<void(const std::string&)> logging_callback) {
glop::GlopParameters params;
params.set_log_search_progress(request.enable_internal_solver_output());
// TODO(user): We do not support all the parameters here. In particular the
// logs before the solver is called will not be appended to the response. Fix
// that, and remove code duplication for the logger config. One way should be
// to not touch/configure anything if the logger is already created while
// calling SolveCpModel() and call a common config function from here or from
// inside Solve()?
SolverLogger logger;
if (logging_callback != nullptr) {
logger.AddInfoLoggingCallback(logging_callback);
}
logger.EnableLogging(params.log_search_progress());
logger.SetLogToStdOut(params.log_to_stdout());
// Set it now so that it can be overwritten by the solver specific parameters.
if (request.has_solver_specific_parameters()) {
// See EncodeParametersAsString() documentation.
if (!std::is_base_of<Message, glop::GlopParameters>::value) {
if (!params.MergeFromString(request.solver_specific_parameters())) {
return ModelInvalidResponse(
logger,
"solver_specific_parameters is not a valid binary stream of the "
"GLOPParameters proto");
}
} else {
if (!ProtobufTextFormatMergeFromString(
request.solver_specific_parameters(), &params)) {
return ModelInvalidResponse(
logger,
"solver_specific_parameters is not a valid textual representation "
"of the GlopParameters proto");
}
}
}
if (request.has_solver_time_limit_seconds()) {
params.set_max_time_in_seconds(request.solver_time_limit_seconds());
}
if (!request.model().general_constraint().empty()) {
return ModelInvalidResponse(logger,
"GLOP does not support general constraints");
}
// Model validation and delta handling.
MPSolutionResponse response;
if (!ExtractValidMPModelInPlaceOrPopulateResponseStatus(&request,
&response)) {
// Note that the ExtractValidMPModelInPlaceOrPopulateResponseStatus() can
// also close trivial model (empty or trivially infeasible). So this is not
// always the MODEL_INVALID status.
return response;
}
{
const std::string error = glop::ValidateParameters(params);
if (!error.empty()) {
return ModelInvalidResponse(
logger, absl::StrCat("Invalid Glop parameters: ", error));
}
}
glop::LinearProgram linear_program;
MPModelProtoToLinearProgram(request.model(), &linear_program);
glop::LPSolver lp_solver;
lp_solver.SetParameters(params);
// TimeLimit and interrupt solve.
std::unique_ptr<TimeLimit> time_limit =
TimeLimit::FromParameters(lp_solver.GetParameters());
if (interrupt_solve != nullptr) {
if (interrupt_solve->load()) {
response.set_status(MPSOLVER_CANCELLED_BY_USER);
response.set_status_str(
"Solve not started, because the user set the atomic<bool> in "
"MPSolver::SolveWithProto() to true before solving could "
"start.");
return response;
} else {
time_limit->RegisterExternalBooleanAsLimit(interrupt_solve);
}
}
// Solve and set response status.
const glop::ProblemStatus status =
lp_solver.SolveWithTimeLimit(linear_program, time_limit.get());
const MPSolverResponseStatus result_status = ToMPSolverResultStatus(status);
response.set_status(result_status);
// Fill in solution.
if (result_status == MPSOLVER_OPTIMAL || result_status == MPSOLVER_FEASIBLE) {
response.set_objective_value(lp_solver.GetObjectiveValue());
const int num_vars = request.model().variable_size();
for (int var_id = 0; var_id < num_vars; ++var_id) {
const glop::Fractional solution_value =
lp_solver.variable_values()[glop::ColIndex(var_id)];
response.add_variable_value(solution_value);
const glop::Fractional reduced_cost =
lp_solver.reduced_costs()[glop::ColIndex(var_id)];
response.add_reduced_cost(reduced_cost);
}
}
if (result_status == MPSOLVER_UNKNOWN_STATUS && interrupt_solve != nullptr &&
interrupt_solve->load()) {
response.set_status(MPSOLVER_CANCELLED_BY_USER);
}
const size_t num_constraints = request.model().constraint_size();
for (int ct_id = 0; ct_id < num_constraints; ++ct_id) {
const glop::Fractional dual_value =
lp_solver.dual_values()[glop::RowIndex(ct_id)];
response.add_dual_value(dual_value);
}
return response;
}
std::string GlopSolverVersion() { return glop::LPSolver::GlopVersion(); }
} // namespace operations_research

View File

@@ -0,0 +1,55 @@
// Copyright 2010-2022 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 OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_GLOP_PROTO_SOLVER_H_
#define OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_GLOP_PROTO_SOLVER_H_
#include <atomic>
#include <functional>
#include <string>
#include "ortools/glop/parameters.pb.h"
#include "ortools/linear_solver/linear_solver.pb.h"
namespace operations_research {
// Solve the input LP model with the GLOP solver.
//
// If possible, std::move the request into this function call to avoid a copy.
//
// If you need to change the solver parameters, please use the
// EncodeParametersAsString() function to set the solver_specific_parameters
// field.
//
// The optional interrupt_solve can be used to interrupt the solve early. It
// must only be set to true, never reset to false. It is also used internally by
// the solver that will set it to true for its own internal logic. As a
// consequence the caller should ignore the stored value and should not use the
// same atomic for different concurrent calls.
//
// The optional logging_callback will be called when the GLOP parameter
// log_search_progress is set to true. Passing a callback will disable the
// default logging to INFO. Note though that by default the GLOP parameter
// log_to_stdout is true so even with a callback, the logs will appear on stdout
// too unless log_to_stdout is set to false. The enable_internal_solver_output
// in the request will act as the GLOP parameter log_search_progress.
MPSolutionResponse GlopSolveProto(
MPModelRequest request, std::atomic<bool>* interrupt_solve = nullptr,
std::function<void(const std::string&)> logging_callback = nullptr);
// Returns a string that describes the version of the GLOP solver.
std::string GlopSolverVersion();
} // namespace operations_research
#endif // OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_GLOP_PROTO_SOLVER_H_

View File

@@ -0,0 +1,67 @@
// Copyright 2010-2022 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 OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_PROTO_UTILS_H_
#define OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_PROTO_UTILS_H_
#include <string>
#include <type_traits>
#include "absl/log/check.h"
#include "google/protobuf/message.h"
#include "ortools/port/proto_utils.h"
namespace operations_research {
#if defined(PROTOBUF_INTERNAL_IMPL)
using google::protobuf::Message;
#else
using google::protobuf::Message;
#endif
// Returns a string that should be used in MPModelRequest's
// solver_specific_parameters field to encode the glop parameters.
//
// The returned string's content depends on the version of the proto library
// that is linked in the binary.
//
// By default it will contain the textual representation of the input proto.
// But when the proto-lite is used, it will contain the binary stream of the
// proto instead since it is not possible to build the textual representation in
// that case.
//
// This function will test if the proto-lite is used and expect a binary stream
// when it is the case. So in order for your code to be portable, you should
// always use this function to set the specific parameters.
//
// Proto-lite disables some features of protobufs and messages inherit from
// MessageLite directly instead of inheriting from Message (which is itself a
// specialization of MessageLite).
// See https://protobuf.dev/reference/cpp/cpp-generated/#message for details.
template <typename P>
std::string EncodeParametersAsString(const P& parameters) {
if constexpr (!std::is_base_of<Message, P>::value) {
// Here we use SerializeToString() instead of SerializeAsString() since the
// later ignores errors and returns an empty string instead (which can be a
// valid value when no fields are set).
std::string bytes;
CHECK(parameters.SerializeToString(&bytes));
return bytes;
}
return ProtobufShortDebugString(parameters);
}
} // namespace operations_research
#endif // OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_PROTO_UTILS_H_

View File

@@ -27,14 +27,13 @@
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/types/span.h"
#include "ortools/base/logging.h"
#include "ortools/glop/preprocessor.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/linear_solver/model_validator.h"
#include "ortools/linear_solver/proto_solver/proto_utils.h"
#include "ortools/linear_solver/proto_solver/sat_solver_utils.h"
#include "ortools/lp_data/lp_data.h"
#include "ortools/lp_data/lp_types.h"
@@ -52,19 +51,6 @@ namespace operations_research {
namespace {
#if defined(PROTOBUF_INTERNAL_IMPL)
using google::protobuf::Message;
#else
using google::protobuf::Message;
#endif
// Proto-lite disables some features of protos and messages inherit from
// MessageLite directly instead of inheriting from Message (which is itself a
// specialization of MessageLite).
// See https://protobuf.dev/reference/cpp/cpp-generated/#message for details.
constexpr bool kProtoLiteSatParameters =
!std::is_base_of<Message, sat::SatParameters>::value;
MPSolverResponseStatus ToMPSolverResponseStatus(sat::CpSolverStatus status,
bool has_objective) {
switch (status) {
@@ -116,10 +102,9 @@ MPSolutionResponse InfeasibleResponse(SolverLogger& logger,
return response;
}
MPSolutionResponse ModelInvalidResponse(SolverLogger& logger,
MPSolutionResponse InvalidModelResponse(SolverLogger& logger,
std::string message) {
SOLVER_LOG(&logger, "Invalid model/parameters in sat_solve_proto.\n",
message);
SOLVER_LOG(&logger, "Invalid model in sat_solve_proto.\n", message);
// This is needed for our benchmark scripts.
if (logger.LoggingIsEnabled()) {
@@ -134,35 +119,32 @@ MPSolutionResponse ModelInvalidResponse(SolverLogger& logger,
return response;
}
MPSolutionResponse InvalidParametersResponse(SolverLogger& logger,
std::string message) {
SOLVER_LOG(&logger, "Invalid parameters in sat_solve_proto.\n", message);
// This is needed for our benchmark scripts.
if (logger.LoggingIsEnabled()) {
sat::CpSolverResponse cp_response;
cp_response.set_status(sat::CpSolverStatus::MODEL_INVALID);
SOLVER_LOG(&logger, CpSolverResponseStats(cp_response));
}
MPSolutionResponse response;
response.set_status(
MPSolverResponseStatus::MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS);
response.set_status_str(message);
return response;
}
} // namespace
absl::StatusOr<MPSolutionResponse> SatSolveProto(
MPSolutionResponse SatSolveProto(
MPModelRequest request, std::atomic<bool>* interrupt_solve,
std::function<void(const std::string&)> logging_callback,
std::function<void(const MPSolution&)> solution_callback) {
sat::SatParameters params;
params.set_log_search_progress(request.enable_internal_solver_output());
// Set it now so that it can be overwritten by the solver specific parameters.
if (request.has_solver_specific_parameters()) {
// See EncodeSatParametersAsString() documentation.
if (kProtoLiteSatParameters) {
if (!params.MergeFromString(request.solver_specific_parameters())) {
return absl::InvalidArgumentError(
"solver_specific_parameters is not a valid binary stream of the "
"SatParameters proto");
}
} else {
if (!ProtobufTextFormatMergeFromString(
request.solver_specific_parameters(), &params)) {
return absl::InvalidArgumentError(
"solver_specific_parameters is not a valid textual representation "
"of the SatParameters proto");
}
}
}
if (request.has_solver_time_limit_seconds()) {
params.set_max_time_in_seconds(request.solver_time_limit_seconds());
}
// TODO(user): We do not support all the parameters here. In particular the
// logs before the solver is called will not be appended to the response. Fix
@@ -177,6 +159,46 @@ absl::StatusOr<MPSolutionResponse> SatSolveProto(
logger.EnableLogging(params.log_search_progress());
logger.SetLogToStdOut(params.log_to_stdout());
// Set it now so that it can be overwritten by the solver specific parameters.
if (request.has_solver_specific_parameters()) {
// See EncodeSatParametersAsString() documentation.
if constexpr (!std::is_base_of<Message, sat::SatParameters>::value) {
if (!params.MergeFromString(request.solver_specific_parameters())) {
return InvalidParametersResponse(
logger,
"solver_specific_parameters is not a valid binary stream of the "
"SatParameters proto");
}
} else {
if (!ProtobufTextFormatMergeFromString(
request.solver_specific_parameters(), &params)) {
return InvalidParametersResponse(
logger,
"solver_specific_parameters is not a valid textual representation "
"of the SatParameters proto");
}
}
}
// Validate parameters.
{
const std::string error = sat::ValidateParameters(params);
if (!error.empty()) {
return InvalidParametersResponse(
logger, absl::StrCat("Invalid CP-SAT parameters: ", error));
}
}
// Reconfigure the logger in case the solver_specific_parameters overwrite its
// configuration. Note that the invalid parameter message will be logged
// before that though according to request.enable_internal_solver_output().
logger.EnableLogging(params.log_search_progress());
logger.SetLogToStdOut(params.log_to_stdout());
if (request.has_solver_time_limit_seconds()) {
params.set_max_time_in_seconds(request.solver_time_limit_seconds());
}
// Model validation and delta handling.
MPSolutionResponse response;
if (!ExtractValidMPModelInPlaceOrPopulateResponseStatus(&request,
@@ -200,15 +222,7 @@ absl::StatusOr<MPSolutionResponse> SatSolveProto(
MPModelProto* const mp_model = request.mutable_model();
if (!sat::MPModelProtoValidationBeforeConversion(params, *mp_model,
&logger)) {
return ModelInvalidResponse(logger, "Extra CP-SAT validation failed.");
}
{
const std::string error = sat::ValidateParameters(params);
if (!error.empty()) {
return ModelInvalidResponse(
logger, absl::StrCat("Invalid CP-SAT parameters: ", error));
}
return InvalidModelResponse(logger, "Extra CP-SAT validation failed.");
}
// This is good to do before any presolve.
@@ -236,7 +250,7 @@ absl::StatusOr<MPSolutionResponse> SatSolveProto(
return InfeasibleResponse(
logger, "Problem proven infeasible during MIP presolve");
case glop::ProblemStatus::INVALID_PROBLEM:
return ModelInvalidResponse(
return InvalidModelResponse(
logger, "Problem detected invalid during MIP presolve");
default:
// TODO(user): We put the INFEASIBLE_OR_UNBOUNBED case here since there
@@ -293,7 +307,7 @@ absl::StatusOr<MPSolutionResponse> SatSolveProto(
}
}
if (!all_integer) {
return ModelInvalidResponse(
return InvalidModelResponse(
logger,
"The model contains non-integer variables but the parameter "
"'only_solve_ip' was set. Change this parameter if you "
@@ -305,7 +319,7 @@ absl::StatusOr<MPSolutionResponse> SatSolveProto(
sat::CpModelProto cp_model;
if (!ConvertMPModelProtoToCpModelProto(params, *mp_model, &cp_model,
&logger)) {
return ModelInvalidResponse(logger,
return InvalidModelResponse(logger,
"Failed to convert model into CP-SAT model");
}
DCHECK_EQ(cp_model.variables().size(), var_scaling.size());
@@ -433,19 +447,6 @@ absl::StatusOr<MPSolutionResponse> SatSolveProto(
return response;
}
std::string EncodeSatParametersAsString(const sat::SatParameters& parameters) {
if (kProtoLiteSatParameters) {
// Here we use SerializeToString() instead of SerializeAsString() since the
// later ignores errors and returns an empty string instead (which can be a
// valid value when no fields are set).
std::string bytes;
CHECK(parameters.SerializeToString(&bytes));
return bytes;
}
return ProtobufShortDebugString(parameters);
}
std::string SatSolverVersion() { return sat::CpSatSolverVersion(); }
} // namespace operations_research

View File

@@ -18,7 +18,6 @@
#include <functional>
#include <string>
#include "absl/status/statusor.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/sat/sat_parameters.pb.h"
#include "ortools/util/logging.h"
@@ -50,27 +49,11 @@ namespace operations_research {
// found by the solver. The solver may call solution_callback from multiple
// threads, but it will ensure that at most one thread executes
// solution_callback at a time.
absl::StatusOr<MPSolutionResponse> SatSolveProto(
MPSolutionResponse SatSolveProto(
MPModelRequest request, std::atomic<bool>* interrupt_solve = nullptr,
std::function<void(const std::string&)> logging_callback = nullptr,
std::function<void(const MPSolution&)> solution_callback = nullptr);
// Returns a string that should be used in MPModelRequest's
// solver_specific_parameters field to encode the SAT parameters.
//
// The returned string's content depends on the version of the proto library
// that is linked in the binary.
//
// By default it will contain the textual representation of the input proto.
// But when the proto-lite is used, it will contain the binary stream of the
// proto instead since it is not possible to build the textual representation in
// that case.
//
// The SatSolveProto() function will test if the proto-lite is used and expect a
// binary stream when it is the case. So in order for your code to be portable,
// you should always use this function to set the specific parameters.
std::string EncodeSatParametersAsString(const sat::SatParameters& parameters);
// Returns a string that describes the version of the CP-SAT solver.
std::string SatSolverVersion();

View File

@@ -83,3 +83,13 @@ py_test(
"//ortools/linear_solver:linear_solver_py_pb2",
],
)
py_binary(
name = "solve_model",
srcs = ["solve_model.py"],
deps = [
":model_builder",
requirement("absl-py"),
"//ortools/linear_solver:linear_solver_py_pb2",
],
)

View File

@@ -1537,17 +1537,29 @@ class Model:
return mbh.to_mpmodel_proto(self.__helper)
def import_from_mps_string(self, mps_string: str) -> bool:
"""Loads the a model from an MPS string."""
return self.__helper.import_from_mps_string(mps_string)
def import_from_mps_file(self, mps_file: str) -> bool:
"""Loads the a model from an MPS file."""
return self.__helper.import_from_mps_file(mps_file)
def import_from_lp_string(self, lp_string: str) -> bool:
"""Loads the a model from an LP string."""
return self.__helper.import_from_lp_string(lp_string)
def import_from_lp_file(self, lp_file: str) -> bool:
"""Loads the a model from an LP file."""
return self.__helper.import_from_lp_file(lp_file)
def import_from_proto_file(self, proto_file: str) -> bool:
"""Loads the a model from an proto file."""
return self.__helper.load_model_from_file(proto_file)
def export_to_proto_file(self, proto_file: str) -> bool:
"""Write a model to a proto file."""
return self.__helper.write_model_to_file(proto_file)
# Model getters and Setters
@property

View File

@@ -177,6 +177,8 @@ PYBIND11_MODULE(model_builder_helper, m) {
arg("options") = MPModelExportOptions())
.def("write_model_to_file", &ModelBuilderHelper::WriteModelToFile,
arg("filename"))
.def("load_model_from_file", &ModelBuilderHelper::LoadModelFromFile,
arg("filename"))
.def("import_from_mps_string", &ModelBuilderHelper::ImportFromMpsString,
arg("mps_string"))
.def("import_from_mps_file", &ModelBuilderHelper::ImportFromMpsFile,

View File

@@ -0,0 +1,63 @@
# Copyright 2010-2022 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.
"""Minimal solver python binary."""
from collections.abc import Sequence
from absl import app
from absl import flags
from ortools.linear_solver.python import model_builder
_INPUT = flags.DEFINE_string("input", "", "Input file to load and solve.")
_PARAMS = flags.DEFINE_string("params", "", "Solver parameters in string format.")
_SOLVER = flags.DEFINE_string("solver", "sat", "Solver type to solve the model with.")
def main(argv: Sequence[str]) -> None:
"""Load a model and solves it."""
if len(argv) > 1:
raise app.UsageError("Too many command-line arguments.")
model = model_builder.ModelBuilder()
# Load MPS file.
if _INPUT.value.endswith(".mps"):
if not model.import_from_mps_file(_INPUT.value):
print(f"Cannot import MPS file: '{_INPUT.value}'")
return
elif not model.import_from_proto_file(_INPUT.value):
print(f"Cannot import Proto file: '{_INPUT.value}'")
return
# Create solver.
solver = model_builder.ModelSolver(_SOLVER.value)
if not solver.solver_is_supported():
print(f"Cannot create solver with name '{_SOLVER.value}'")
return
# Set parameters.
if _PARAMS.value:
solver.set_solver_specific_parameters(_PARAMS.value)
# Enable the output of the solver.
solver.enable_output(True)
# And solve.
solver.solve(model)
if __name__ == "__main__":
app.run(main)

View File

@@ -109,39 +109,39 @@ def main():
# Group1
constraint_g1 = solver.Constraint(1, 1)
for i in range(len(group1)):
for index, _ in enumerate(group1):
# a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1]
# p is True if a AND b, False otherwise
constraint = solver.Constraint(0, 1)
constraint.SetCoefficient(work[group1[i][0]], 1)
constraint.SetCoefficient(work[group1[i][1]], 1)
p = solver.BoolVar(f"g1_p{i}")
constraint.SetCoefficient(work[group1[index][0]], 1)
constraint.SetCoefficient(work[group1[index][1]], 1)
p = solver.BoolVar(f"g1_p{index}")
constraint.SetCoefficient(p, -2)
constraint_g1.SetCoefficient(p, 1)
# Group2
constraint_g2 = solver.Constraint(1, 1)
for i in range(len(group2)):
for index, _ in enumerate(group2):
# a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1]
# p is True if a AND b, False otherwise
constraint = solver.Constraint(0, 1)
constraint.SetCoefficient(work[group2[i][0]], 1)
constraint.SetCoefficient(work[group2[i][1]], 1)
p = solver.BoolVar(f"g2_p{i}")
constraint.SetCoefficient(work[group2[index][0]], 1)
constraint.SetCoefficient(work[group2[index][1]], 1)
p = solver.BoolVar(f"g2_p{index}")
constraint.SetCoefficient(p, -2)
constraint_g2.SetCoefficient(p, 1)
# Group3
constraint_g3 = solver.Constraint(1, 1)
for i in range(len(group3)):
for index, _ in enumerate(group3):
# a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1]
# p is True if a AND b, False otherwise
constraint = solver.Constraint(0, 1)
constraint.SetCoefficient(work[group3[i][0]], 1)
constraint.SetCoefficient(work[group3[i][1]], 1)
p = solver.BoolVar(f"g3_p{i}")
constraint.SetCoefficient(work[group3[index][0]], 1)
constraint.SetCoefficient(work[group3[index][1]], 1)
p = solver.BoolVar(f"g3_p{index}")
constraint.SetCoefficient(p, -2)
constraint_g3.SetCoefficient(p, 1)

View File

@@ -25,7 +25,7 @@ from ortools.linear_solver.python import model_builder
# [START program_part1]
# [START data_model]
def create_data_model():
def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame]:
"""Create the data for the example."""
items_str = """

View File

@@ -30,7 +30,6 @@ def create_data_model():
data["bins"] = data["items"]
data["bin_capacity"] = 100
return data
# [END data_model]

View File

@@ -13,6 +13,7 @@
# limitations under the License.
"""MIP example that uses a variable array."""
# [START program]
# [START import]
from ortools.linear_solver import pywraplp
@@ -35,7 +36,6 @@ def create_data_model():
data["num_vars"] = 5
data["num_constraints"] = 4
return data
# [END data_model]

View File

@@ -90,7 +90,8 @@ def main():
for i in data["all_items"]:
if x[i, b].solution_value() > 0:
print(
f"Item {i} weight: {data['weights'][i]} value: {data['values'][i]}"
f"Item {i} weight: {data['weights'][i]} value:"
f" {data['values'][i]}"
)
bin_weight += data["weights"][i]
bin_value += data["values"][i]

View File

@@ -24,6 +24,7 @@
#include "ortools/base/logging.h"
#include "ortools/linear_solver/linear_solver.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/linear_solver/proto_solver/proto_utils.h"
#include "ortools/linear_solver/proto_solver/sat_proto_solver.h"
#include "ortools/port/proto_utils.h"
#include "ortools/sat/cp_model.pb.h"
@@ -130,14 +131,11 @@ MPSolver::ResultStatus SatInterface::Solve(const MPSolverParameters& param) {
MPModelRequest request;
solver_->ExportModelToProto(request.mutable_model());
request.set_solver_specific_parameters(
EncodeSatParametersAsString(parameters_));
request.set_solver_specific_parameters(EncodeParametersAsString(parameters_));
request.set_enable_internal_solver_output(!quiet_);
const absl::StatusOr<MPSolutionResponse> status_or =
SatSolveProto(std::move(request), &interrupt_solve_);
if (!status_or.ok()) return MPSolver::ABNORMAL;
const MPSolutionResponse& response = status_or.value();
const MPSolutionResponse response =
SatSolveProto(std::move(request), &interrupt_solve_);
// The solution must be marked as synchronized even when no solution exists.
sync_status_ = SOLUTION_SYNCHRONIZED;
@@ -156,22 +154,7 @@ MPSolver::ResultStatus SatInterface::Solve(const MPSolverParameters& param) {
std::optional<MPSolutionResponse> SatInterface::DirectlySolveProto(
const MPModelRequest& request, std::atomic<bool>* interrupt) {
absl::StatusOr<MPSolutionResponse> status_or =
SatSolveProto(request, interrupt);
if (status_or.ok()) return std::move(status_or).value();
if (request.enable_internal_solver_output()) {
LOG(INFO) << "Failed SAT solve: " << status_or.status();
}
MPSolutionResponse response;
// As of 2021-08, the sole non-OK status returned by SatSolveProto is an
// INVALID_ARGUMENT error caused by invalid solver parameters.
// TODO(user): Move that conversion to SatSolveProto, which should always
// return a MPSolutionResponse, even for errors.
response.set_status(absl::IsInvalidArgument(status_or.status())
? MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS
: MPSOLVER_ABNORMAL);
response.set_status_str(status_or.status().ToString());
return response;
return SatSolveProto(request, interrupt);
}
bool SatInterface::InterruptSolve() {

View File

@@ -40,6 +40,11 @@ cc_library(
"//ortools/linear_solver",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/linear_solver:model_exporter",
"//ortools/linear_solver/proto_solver:glop_proto_solver",
"//ortools/linear_solver/proto_solver:gurobi_proto_solver",
"//ortools/linear_solver/proto_solver:pdlp_proto_solver",
"//ortools/linear_solver/proto_solver:sat_proto_solver",
"//ortools/linear_solver/proto_solver:scip_proto_solver",
"//ortools/lp_data:lp_parser",
"//ortools/lp_data:mps_reader",
"//ortools/util:logging",

View File

@@ -27,6 +27,8 @@
#include "ortools/base/options.h"
#include "ortools/linear_solver/linear_solver.h"
#include "ortools/linear_solver/linear_solver.pb.h"
#include "ortools/linear_solver/model_exporter.h"
#include "ortools/linear_solver/proto_solver/glop_proto_solver.h"
#include "ortools/linear_solver/proto_solver/sat_proto_solver.h"
#if defined(USE_SCIP)
#include "ortools/linear_solver/proto_solver/scip_proto_solver.h"
@@ -59,13 +61,23 @@ std::string ModelBuilderHelper::ExportToLpString(
}
bool ModelBuilderHelper::WriteModelToFile(const std::string& filename) {
if (absl::EndsWith(filename, "txt")) {
if (absl::EndsWith(filename, "txt") ||
absl::EndsWith(filename, ".textproto")) {
return file::SetTextProto(filename, model_, file::Defaults()).ok();
} else {
return file::SetBinaryProto(filename, model_, file::Defaults()).ok();
}
}
bool ModelBuilderHelper::LoadModelFromFile(const std::string& filename) {
if (absl::EndsWith(filename, "txt") ||
absl::EndsWith(filename, ".textproto")) {
return file::GetTextProto(filename, &model_, file::Defaults()).ok();
} else {
return file::GetBinaryProto(filename, &model_, file::Defaults()).ok();
}
}
// See comment in the header file why we need to wrap absl::Status code with
// code having simpler APIs.
bool ModelBuilderHelper::ImportFromMpsString(const std::string& mps_string) {
@@ -537,18 +549,12 @@ void ModelSolverHelper::Solve(const ModelBuilderHelper& model) {
}
switch (solver_type_.value()) {
case MPModelRequest::GLOP_LINEAR_PROGRAMMING: {
// TODO(user): Enable log_callback support.
MPSolutionResponse temp;
MPSolver::SolveWithProto(request, &temp, &interrupt_solve_);
response_ = std::move(temp);
response_ = GlopSolveProto(request, &interrupt_solve_, log_callback_);
break;
}
case MPModelRequest::SAT_INTEGER_PROGRAMMING: {
const auto temp =
response_ =
SatSolveProto(request, &interrupt_solve_, log_callback_, nullptr);
if (temp.ok()) {
response_ = std::move(temp.value());
}
break;
}
#if defined(USE_SCIP)

View File

@@ -51,6 +51,7 @@ class ModelBuilderHelper {
std::string ExportToLpString(const operations_research::MPModelExportOptions&
options = MPModelExportOptions());
bool WriteModelToFile(const std::string& filename);
bool LoadModelFromFile(const std::string& filename);
bool ImportFromMpsString(const std::string& mps_string);
bool ImportFromMpsFile(const std::string& mps_file);

View File

@@ -1549,6 +1549,29 @@ bool LinearProgram::BoundsOfIntegerConstraintsAreInteger(
return true;
}
void LinearProgram::RemoveNearZeroEntries(Fractional threshold) {
int64_t num_removed_objective_entries = 0;
int64_t num_removed_matrix_entries = 0;
for (ColIndex col(0); col < matrix_.num_cols(); ++col) {
const int64_t old_size = matrix_.column(col).num_entries().value();
matrix_.mutable_column(col)->RemoveNearZeroEntries(threshold);
num_removed_matrix_entries +=
old_size - matrix_.column(col).num_entries().value();
if (std::abs(objective_coefficients_[col]) <= threshold) {
objective_coefficients_[col] = 0.0;
++num_removed_objective_entries;
}
}
if (num_removed_matrix_entries > 0) {
transpose_matrix_is_consistent_ = false;
VLOG(1) << "Removed " << num_removed_matrix_entries << " matrix entries.";
}
if (num_removed_objective_entries > 0) {
VLOG(1) << "Removed " << num_removed_objective_entries
<< " objective coefficients.";
}
}
// --------------------------------------------------------
// ProblemSolution
// --------------------------------------------------------

View File

@@ -562,6 +562,9 @@ class LinearProgram {
return &constraint_upper_bounds_;
}
// Removes objective and coefficient with magnitude <= threshold.
void RemoveNearZeroEntries(Fractional threshold);
private:
// A helper function that updates the vectors integer_variables_list_,
// binary_variables_list_, and non_binary_variables_list_.

View File

@@ -13,6 +13,10 @@
#include "ortools/lp_data/proto_utils.h"
#include "ortools/lp_data/lp_data.h"
#include "ortools/lp_data/lp_types.h"
#include "ortools/lp_data/sparse.h"
namespace operations_research {
namespace glop {

View File

@@ -901,10 +901,13 @@ void SparseVector<IndexType, IteratorType>::AddMultipleToSparseVectorInternal(
++ia;
++ib;
} else if (index_a < index_b) {
c.MutableIndex(ic) = index_a;
c.MutableCoefficient(ic) = multiplier * a.GetCoefficient(ia);
const Fractional new_value = multiplier * a.GetCoefficient(ia);
if (std::abs(new_value) > drop_tolerance) {
c.MutableIndex(ic) = index_a;
c.MutableCoefficient(ic) = new_value;
++ic;
}
++ia;
++ic;
} else { // index_b < index_a
c.MutableIndex(ic) = b.GetIndex(ib);
c.MutableCoefficient(ic) = b.GetCoefficient(ib);
@@ -913,10 +916,13 @@ void SparseVector<IndexType, IteratorType>::AddMultipleToSparseVectorInternal(
}
}
while (ia < size_a) {
c.MutableIndex(ic) = a.GetIndex(ia);
c.MutableCoefficient(ic) = multiplier * a.GetCoefficient(ia);
const Fractional new_value = multiplier * a.GetCoefficient(ia);
if (std::abs(new_value) > drop_tolerance) {
c.MutableIndex(ic) = a.GetIndex(ia);
c.MutableCoefficient(ic) = new_value;
++ic;
}
++ia;
++ic;
}
while (ib < size_b) {
c.MutableIndex(ic) = b.GetIndex(ib);
@@ -927,6 +933,7 @@ void SparseVector<IndexType, IteratorType>::AddMultipleToSparseVectorInternal(
c.ResizeDown(ic);
c.may_contain_duplicates_ = false;
c.Swap(accumulator_vector);
DCHECK(accumulator_vector->IsCleanedUp());
}
template <typename IndexType, typename IteratorType>

View File

@@ -11,9 +11,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.
load("@rules_python//python:proto.bzl", "py_proto_library")
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
package(default_visibility = ["//ortools/math_opt:__subpackages__"])
package(default_visibility = ["//visibility:public"])
# Features that are new or under construction have restricted access, contact
# math-opt-dev@ if you want try using these with submitted code.
package_group(
name = "math_opt_allow_list",
packages = [
"//ortools/...",
],
)
proto_library(
name = "callback_proto",
@@ -26,12 +36,16 @@ proto_library(
cc_proto_library(
name = "callback_cc_proto",
visibility = ["//visibility:public"],
deps = [
":callback_proto",
],
)
py_proto_library(
name = "callback_py_pb2",
deps = [":callback_proto"],
)
proto_library(
name = "model_proto",
srcs = ["model.proto"],
@@ -42,12 +56,16 @@ proto_library(
cc_proto_library(
name = "model_cc_proto",
visibility = ["//visibility:public"],
deps = [
":model_proto",
],
)
py_proto_library(
name = "model_py_pb2",
deps = [":model_proto"],
)
proto_library(
name = "model_parameters_proto",
srcs = ["model_parameters.proto"],
@@ -59,12 +77,16 @@ proto_library(
cc_proto_library(
name = "model_parameters_cc_proto",
visibility = ["//visibility:public"],
deps = [
":model_parameters_proto",
],
)
py_proto_library(
name = "model_parameters_py_pb2",
deps = [":model_parameters_proto"],
)
proto_library(
name = "model_update_proto",
srcs = ["model_update.proto"],
@@ -76,12 +98,16 @@ proto_library(
cc_proto_library(
name = "model_update_cc_proto",
visibility = ["//visibility:public"],
deps = [
":model_update_proto",
],
)
py_proto_library(
name = "model_update_py_pb2",
deps = [":model_update_proto"],
)
proto_library(
name = "parameters_proto",
srcs = ["parameters.proto"],
@@ -91,6 +117,7 @@ proto_library(
"//ortools/math_opt/solvers:glpk_proto",
"//ortools/math_opt/solvers:gurobi_proto",
"//ortools/math_opt/solvers:highs_proto",
"//ortools/math_opt/solvers:osqp_proto",
"//ortools/sat:sat_parameters_proto",
"@com_google_protobuf//:duration_proto",
],
@@ -98,18 +125,23 @@ proto_library(
cc_proto_library(
name = "parameters_cc_proto",
visibility = ["//visibility:public"],
deps = [
":parameters_proto",
],
)
py_proto_library(
name = "parameters_py_pb2",
deps = [":parameters_proto"],
)
proto_library(
name = "result_proto",
srcs = ["result.proto"],
deps = [
":solution_proto",
"//ortools/gscip:gscip_proto",
"//ortools/math_opt/solvers:osqp_proto",
"//ortools/pdlp:solve_log_proto",
"@com_google_protobuf//:duration_proto",
],
@@ -117,12 +149,16 @@ proto_library(
cc_proto_library(
name = "result_cc_proto",
visibility = ["//visibility:public"],
deps = [
":result_proto",
],
)
py_proto_library(
name = "result_py_pb2",
deps = [":result_proto"],
)
proto_library(
name = "solution_proto",
srcs = ["solution.proto"],
@@ -133,12 +169,16 @@ proto_library(
cc_proto_library(
name = "solution_cc_proto",
visibility = ["//visibility:public"],
deps = [
":solution_proto",
],
)
py_proto_library(
name = "solution_py_pb2",
deps = [":solution_proto"],
)
proto_library(
name = "sparse_containers_proto",
srcs = ["sparse_containers.proto"],
@@ -146,19 +186,32 @@ proto_library(
cc_proto_library(
name = "sparse_containers_cc_proto",
visibility = ["//visibility:public"],
deps = [
":sparse_containers_proto",
],
)
cc_proto_library(
name = "infeasible_subsystem_cc_proto",
deps = [":infeasible_subsystem_proto"],
py_proto_library(
name = "sparse_containers_py_pb2",
deps = [":sparse_containers_proto"],
)
proto_library(
name = "infeasible_subsystem_proto",
srcs = ["infeasible_subsystem.proto"],
deps = [":result_proto"],
deps = [
":result_proto",
],
)
cc_proto_library(
name = "infeasible_subsystem_cc_proto",
deps = [
":infeasible_subsystem_proto",
],
)
py_proto_library(
name = "infeasible_subsystem_py_pb2",
deps = [":infeasible_subsystem_proto"],
)

View File

@@ -35,8 +35,8 @@ cc_library(
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:model_update_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/core:sorted",
"//ortools/math_opt/storage:atomic_constraint_storage",
"//ortools/math_opt/storage:sorted",
"//ortools/math_opt/storage:sparse_coefficient_map",
"@com_google_absl//absl/container:flat_hash_set",
],

View File

@@ -13,16 +13,15 @@
#include "ortools/math_opt/constraints/indicator/storage.h"
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "ortools/base/strong_int.h"
#include "ortools/math_opt/core/sorted.h"
#include "ortools/math_opt/model.pb.h"
#include "ortools/math_opt/sparse_containers.pb.h"
#include "ortools/math_opt/storage/sorted.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
namespace operations_research::math_opt {

View File

@@ -35,13 +35,11 @@ cc_library(
srcs = ["storage.cc"],
hdrs = ["storage.h"],
deps = [
"//ortools/base:intops",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:model_update_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/storage:atomic_constraint_storage",
"//ortools/math_opt/storage:model_storage_types",
"//ortools/math_opt/storage:sorted",
"//ortools/math_opt/storage:sparse_coefficient_map",
"//ortools/math_opt/storage:sparse_matrix",
"@com_google_absl//absl/container:flat_hash_set",

View File

@@ -13,15 +13,12 @@
#include "ortools/math_opt/constraints/quadratic/storage.h"
#include <cstdint>
#include <string>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "ortools/base/strong_int.h"
#include "ortools/math_opt/sparse_containers.pb.h"
#include "ortools/math_opt/storage/model_storage_types.h"
#include "ortools/math_opt/storage/sorted.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
#include "ortools/math_opt/storage/sparse_matrix.h"

View File

@@ -14,17 +14,17 @@
package(default_visibility = ["//ortools/math_opt:__subpackages__"])
cc_library(
name = "validator",
srcs = ["validator.cc"],
hdrs = ["validator.h"],
name = "second_order_cone_constraint",
srcs = ["second_order_cone_constraint.cc"],
hdrs = ["second_order_cone_constraint.h"],
deps = [
"//ortools/base:status_macros",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/core:model_summary",
"//ortools/math_opt/core:sparse_vector_view",
"//ortools/math_opt/validators:linear_expression_validator",
"@com_google_absl//absl/status",
":storage",
"//ortools/base:intops",
"//ortools/math_opt/constraints/util:model_util",
"//ortools/math_opt/cpp:variable_and_expressions",
"//ortools/math_opt/storage:linear_expression_data",
"//ortools/math_opt/storage:model_storage",
"@com_google_absl//absl/strings",
],
)
@@ -40,23 +40,22 @@ cc_library(
"//ortools/math_opt/storage:atomic_constraint_storage",
"//ortools/math_opt/storage:linear_expression_data",
"//ortools/math_opt/storage:model_storage_types",
"//ortools/math_opt/storage:sorted",
"//ortools/math_opt/storage:sparse_coefficient_map",
"@com_google_absl//absl/container:flat_hash_set",
],
)
cc_library(
name = "second_order_cone_constraint",
srcs = ["second_order_cone_constraint.cc"],
hdrs = ["second_order_cone_constraint.h"],
name = "validator",
srcs = ["validator.cc"],
hdrs = ["validator.h"],
deps = [
":storage",
"//ortools/base:intops",
"//ortools/math_opt/constraints/util:model_util",
"//ortools/math_opt/cpp:variable_and_expressions",
"//ortools/math_opt/storage:linear_expression_data",
"//ortools/math_opt/storage:model_storage",
"@com_google_absl//absl/strings",
"//ortools/base:status_macros",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/core:model_summary",
"//ortools/math_opt/core:sparse_vector_view",
"//ortools/math_opt/validators:linear_expression_validator",
"@com_google_absl//absl/status",
],
)

View File

@@ -22,7 +22,6 @@
#include "ortools/math_opt/sparse_containers.pb.h"
#include "ortools/math_opt/storage/linear_expression_data.h"
#include "ortools/math_opt/storage/model_storage_types.h"
#include "ortools/math_opt/storage/sorted.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
namespace operations_research::math_opt {

View File

@@ -13,47 +13,6 @@
package(default_visibility = ["//ortools/math_opt:__subpackages__"])
cc_library(
name = "validator",
srcs = ["validator.cc"],
hdrs = ["validator.h"],
deps = [
"//ortools/base:status_macros",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/core:model_summary",
"//ortools/math_opt/validators:linear_expression_validator",
"//ortools/math_opt/validators:scalar_validator",
"@com_google_absl//absl/container:flat_hash_set",
"@com_google_absl//absl/status",
],
)
cc_library(
name = "storage",
hdrs = ["storage.h"],
deps = [
"//ortools/base:intops",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:model_update_cc_proto",
"//ortools/math_opt/storage:atomic_constraint_storage",
"//ortools/math_opt/storage:linear_expression_data",
"//ortools/math_opt/storage:sparse_coefficient_map",
"@com_google_absl//absl/container:flat_hash_set",
"@com_google_absl//absl/log:check",
],
)
cc_library(
name = "util",
hdrs = ["util.h"],
deps = [
"//ortools/math_opt/cpp:variable_and_expressions",
"//ortools/util:fp_roundtrip_conv",
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "sos1_constraint",
srcs = ["sos1_constraint.cc"],
@@ -85,3 +44,44 @@ cc_library(
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "storage",
hdrs = ["storage.h"],
deps = [
"//ortools/base:intops",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:model_update_cc_proto",
"//ortools/math_opt/storage:atomic_constraint_storage",
"//ortools/math_opt/storage:linear_expression_data",
"//ortools/math_opt/storage:sparse_coefficient_map",
"@com_google_absl//absl/container:flat_hash_set",
"@com_google_absl//absl/log:check",
],
)
cc_library(
name = "util",
hdrs = ["util.h"],
deps = [
"//ortools/math_opt/cpp:variable_and_expressions",
"//ortools/util:fp_roundtrip_conv",
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "validator",
srcs = ["validator.cc"],
hdrs = ["validator.h"],
deps = [
"//ortools/base:status_macros",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/core:model_summary",
"//ortools/math_opt/validators:linear_expression_validator",
"//ortools/math_opt/validators:scalar_validator",
"@com_google_absl//absl/container:flat_hash_set",
"@com_google_absl//absl/status",
],
)

View File

@@ -17,6 +17,7 @@ cc_library(
name = "math_opt_proto_utils",
srcs = ["math_opt_proto_utils.cc"],
hdrs = ["math_opt_proto_utils.h"],
visibility = ["//visibility:public"],
deps = [
":sparse_vector_view",
"//ortools/base",
@@ -150,6 +151,7 @@ cc_library(
cc_library(
name = "non_streamable_solver_init_arguments",
srcs = ["non_streamable_solver_init_arguments.cc"],
hdrs = ["non_streamable_solver_init_arguments.h"],
deps = ["//ortools/math_opt:parameters_cc_proto"],
)
@@ -231,3 +233,14 @@ cc_library(
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "sorted",
hdrs = ["sorted.h"],
deps = [
"@com_google_absl//absl/algorithm:container",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/container:flat_hash_set",
"@com_google_protobuf//:protobuf",
],
)

View File

@@ -195,8 +195,12 @@ TerminationProto OptimalTerminationProto(double finite_primal_objective,
// Returns a TERMINATION_REASON_INFEASIBLE termination with
// FEASIBILITY_STATUS_INFEASIBLE primal status and the provided dual status.
//
// It sets a trivial primal bound and a trivial dual bound based on the provided
// dual status.
// It sets a trivial primal bound and a dual bound based on the provided dual
// status, which should be FEASIBILITY_STATUS_FEASIBLE or
// FEASIBILITY_STATUS_UNDETERMINED. If the dual status is
// FEASIBILITY_STATUS_UNDETERMINED, then the dual bound will be trivial and if
// the dual status is FEASIBILITY_STATUS_FEASIBLE, then the dual bound will be
// equal to the primal bound.
//
// The convention for infeasible MIPs is that dual_feasibility_status is
// feasible (There always exist a dual feasible convex relaxation of an
@@ -205,7 +209,9 @@ TerminationProto OptimalTerminationProto(double finite_primal_objective,
// dual_feasibility_status must not be FEASIBILITY_STATUS_UNSPECIFIED for a
// valid TerminationProto to be returned.
TerminationProto InfeasibleTerminationProto(
bool is_maximize, FeasibilityStatusProto dual_feasibility_status,
bool is_maximize,
FeasibilityStatusProto dual_feasibility_status =
FEASIBILITY_STATUS_UNDETERMINED,
absl::string_view detail = {});
// Returns a TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED termination with
@@ -218,7 +224,9 @@ TerminationProto InfeasibleTerminationProto(
// dual_feasibility_status must be infeasible or undetermined for a valid
// TerminationProto to be returned.
TerminationProto InfeasibleOrUnboundedTerminationProto(
bool is_maximize, FeasibilityStatusProto dual_feasibility_status,
bool is_maximize,
FeasibilityStatusProto dual_feasibility_status =
FEASIBILITY_STATUS_UNDETERMINED,
absl::string_view detail = {});
// Returns a TERMINATION_REASON_UNBOUNDED termination with a

View File

@@ -0,0 +1,33 @@
# Copyright 2010-2022 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.
load("@pybind11_bazel//:build_defs.bzl", "pybind_extension")
package(default_visibility = ["//ortools/math_opt:__subpackages__"])
pybind_extension(
name = "solver",
srcs = ["solver.cc"],
deps = [
"//ortools/math_opt:result_cc_proto",
"//ortools/math_opt/core:solve_interrupter",
"//ortools/math_opt/core:solver",
"//ortools/math_opt/core:solver_debug",
"//ortools/math_opt/solvers:cp_sat_solver",
"//ortools/math_opt/solvers:glop_solver",
"//ortools/math_opt/solvers:glpk_solver",
"//ortools/math_opt/solvers:gscip_solver",
"@pybind11_abseil//pybind11_abseil:status_casters",
"@pybind11_protobuf//pybind11_protobuf:native_proto_caster",
],
)

View File

@@ -0,0 +1,152 @@
// Copyright 2010-2022 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/math_opt/core/solver.h"
#include <pybind11/functional.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "ortools/math_opt/core/solve_interrupter.h"
#include "ortools/math_opt/core/solver_debug.h"
#include "ortools/math_opt/result.pb.h"
#include "pybind11/cast.h"
#include "pybind11_abseil/status_casters.h" // IWYU pragma: keep
#include "pybind11_protobuf/native_proto_caster.h"
namespace operations_research::math_opt {
namespace py = ::pybind11;
using PybindSolverCallback =
std::function<CallbackResultProto(CallbackDataProto)>;
using PybindSolverMessageCallback =
std::function<void(std::vector<std::string>)>;
// Wrapper for Solver::NonIncrementalSolve with flat arguments.
absl::StatusOr<SolveResultProto> PybindSolve(
const ModelProto& model, const SolverTypeProto solver_type,
SolverInitializerProto solver_initializer, SolveParametersProto parameters,
ModelSolveParametersProto model_parameters,
PybindSolverMessageCallback message_callback,
CallbackRegistrationProto callback_registration,
PybindSolverCallback user_cb, SolveInterrupter* const interrupter) {
return Solver::NonIncrementalSolve(
model, solver_type, {.streamable = std::move(solver_initializer)},
{.parameters = std::move(parameters),
.model_parameters = std::move(model_parameters),
.message_callback = std::move(message_callback),
.callback_registration = std::move(callback_registration),
.user_cb = std::move(user_cb),
.interrupter = interrupter});
}
// Wrapper for Solver::NonIncrementalComputeInfeasibleSubsystem
absl::StatusOr<ComputeInfeasibleSubsystemResultProto>
PybindComputeInfeasibleSubsystem(const ModelProto& model,
const SolverTypeProto solver_type,
SolverInitializerProto solver_initializer,
SolveParametersProto parameters,
PybindSolverMessageCallback message_callback,
SolveInterrupter* const interrupter) {
return Solver::NonIncrementalComputeInfeasibleSubsystem(
model, solver_type, {.streamable = std::move(solver_initializer)},
{.parameters = std::move(parameters),
.message_callback = std::move(message_callback),
.interrupter = interrupter});
}
// Wrapper for the Solver class with flat arguments.
class PybindSolver {
public:
static absl::StatusOr<std::unique_ptr<PybindSolver>> New(
const SolverTypeProto solver_type, const ModelProto& model,
SolverInitializerProto solver_initializer) {
ASSIGN_OR_RETURN(
std::unique_ptr<Solver> solver,
Solver::New(solver_type, model,
{.streamable = std::move(solver_initializer)}));
return absl::WrapUnique<PybindSolver>(new PybindSolver(std::move(solver)));
}
static int64_t DebugNumSolver() { return internal::debug_num_solver.load(); }
PybindSolver(const PybindSolver&) = delete;
PybindSolver& operator=(const PybindSolver&) = delete;
absl::StatusOr<SolveResultProto> Solve(
SolveParametersProto parameters,
ModelSolveParametersProto model_parameters,
PybindSolverMessageCallback message_callback,
CallbackRegistrationProto callback_registration,
PybindSolverCallback user_cb, SolveInterrupter* const interrupter) {
return solver_->Solve(
{.parameters = std::move(parameters),
.model_parameters = std::move(model_parameters),
.message_callback = std::move(message_callback),
.callback_registration = std::move(callback_registration),
.user_cb = std::move(user_cb),
.interrupter = interrupter});
}
absl::StatusOr<bool> Update(const ModelUpdateProto& model_update) {
return solver_->Update(model_update);
}
private:
explicit PybindSolver(std::unique_ptr<Solver> solver)
: solver_(std::move(solver)) {}
const std::unique_ptr<Solver> solver_;
};
PYBIND11_MODULE(solver, m) {
pybind11_protobuf::ImportNativeProtoCasters();
pybind11::google::ImportStatusModule();
// The Global Interpreter Lock (GIL) is released with gil_scoped_release
// during the solve to allow Python threads to run callbacks in parallel.
m.def("solve", &PybindSolve, py::arg("model"), py::arg("solver_type"),
py::arg("solver_initializer"), py::arg("parameters"),
py::arg("model_parameters"), py::arg("message_cb"),
py::arg("callback_registration"), py::arg("user_cb"),
py::arg("interrupt"), py::call_guard<py::gil_scoped_release>());
m.def("compute_infeasible_subsystem", &PybindComputeInfeasibleSubsystem,
py::arg("model"), py::arg("solver_type"), py::arg("solver_initializer"),
py::arg("parameters"), py::arg("message_cb"), py::arg("interrupt"),
py::call_guard<py::gil_scoped_release>());
m.def("new", &PybindSolver::New, py::arg("solver_type"), py::arg("model"),
py::arg("solver_initializer"),
py::call_guard<py::gil_scoped_release>());
m.def("debug_num_solver", &PybindSolver::DebugNumSolver);
py::class_<PybindSolver>(m, "Solver")
.def("solve", &PybindSolver::Solve,
py::call_guard<py::gil_scoped_release>())
.def("update", &PybindSolver::Update,
py::call_guard<py::gil_scoped_release>());
py::class_<SolveInterrupter>(m, "SolveInterrupter")
.def(py::init())
.def("interrupt", &SolveInterrupter::Interrupt)
.def("is_interrupted", &SolveInterrupter::IsInterrupted);
}
} // namespace operations_research::math_opt

View File

@@ -0,0 +1,180 @@
#!/usr/bin/env python3
# Copyright 2010-2022 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.
import datetime
import math
import unittest
from google3.third_party.pybind11_abseil.status import StatusNotOk
from ortools.math_opt import infeasible_subsystem_pb2
from ortools.math_opt import model_pb2
from ortools.math_opt import parameters_pb2
from ortools.math_opt import result_pb2
from ortools.math_opt.core.python import solver
from ortools.math_opt.python.testing import compare_proto
# The model is:
# x + z <= 4 (c)
# 3 <= x + z (d)
# x, y, z in [0, 1]
# The IIS is x upper bound, z upper bound, (d) lower bound
def _simple_infeasible_model() -> model_pb2.ModelProto:
model = model_pb2.ModelProto()
model.variables.ids[:] = [0, 1, 2]
model.variables.lower_bounds[:] = [0.0, 0.0, 0.0]
model.variables.upper_bounds[:] = [1.0, 1.0, 1.0]
model.variables.integers[:] = [False, False, False]
model.linear_constraints.ids[:] = [0, 1]
model.linear_constraints.lower_bounds[:] = [-math.inf, 3.0]
model.linear_constraints.upper_bounds[:] = [4.0, math.inf]
model.linear_constraint_matrix.row_ids[:] = [0, 0, 1, 1]
model.linear_constraint_matrix.column_ids[:] = [0, 2, 0, 2]
model.linear_constraint_matrix.coefficients[:] = [1.0, 1.0, 1.0, 1.0]
return model
# The model is
# 2*x + 2*y + 2*z >= 3.0
# x + y <= 1
# y + z <= 1
# x + z <= 1
# x, y, z in {0, 1}
def _nontrivial_infeasible_model() -> model_pb2.ModelProto:
model = model_pb2.ModelProto()
model.variables.ids[:] = [0, 1, 2]
model.variables.lower_bounds[:] = [0.0, 0.0, 0.0]
model.variables.upper_bounds[:] = [1.0, 1.0, 1.0]
model.variables.integers[:] = [True, True, True]
model.linear_constraints.ids[:] = [0, 1, 2, 3]
model.linear_constraints.lower_bounds[:] = [
3.0,
-math.inf,
-math.inf,
-math.inf,
]
model.linear_constraints.upper_bounds[:] = [math.inf, 1.0, 1.0, 1.0]
model.linear_constraint_matrix.row_ids[:] = [0, 0, 0, 1, 1, 2, 2, 3, 3]
model.linear_constraint_matrix.column_ids[:] = [0, 1, 2, 0, 1, 1, 2, 0, 2]
model.linear_constraint_matrix.coefficients[:] = [
2.0,
2.0,
2.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
]
return model
def _expected_iis_success() -> (
infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto
):
expected = infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto(
is_minimal=True, feasibility=result_pb2.FEASIBILITY_STATUS_INFEASIBLE
)
expected.infeasible_subsystem.variable_bounds[0].upper = True
expected.infeasible_subsystem.variable_bounds[2].upper = True
expected.infeasible_subsystem.linear_constraints[1].lower = True
return expected
class PybindComputeInfeasibleSubsystemTest(
compare_proto.MathOptProtoAssertions, unittest.TestCase
):
def test_compute_infeasible_subsystem_infeasible(self) -> None:
iis_result = solver.compute_infeasible_subsystem(
_simple_infeasible_model(),
parameters_pb2.SOLVER_TYPE_GUROBI,
parameters_pb2.SolverInitializerProto(),
parameters_pb2.SolveParametersProto(),
None,
None,
)
self.assert_protos_equiv(iis_result, _expected_iis_success())
def test_compute_infeasible_subsystem_infeasible_uninterrupted(self) -> None:
interrupter = solver.SolveInterrupter()
iis_result = solver.compute_infeasible_subsystem(
_simple_infeasible_model(),
parameters_pb2.SOLVER_TYPE_GUROBI,
parameters_pb2.SolverInitializerProto(),
parameters_pb2.SolveParametersProto(),
None,
interrupter,
)
self.assert_protos_equiv(iis_result, _expected_iis_success())
def test_compute_infeasible_subsystem_interrupted(self) -> None:
interrupter = solver.SolveInterrupter()
interrupter.interrupt()
iis_result = solver.compute_infeasible_subsystem(
_nontrivial_infeasible_model(),
parameters_pb2.SOLVER_TYPE_GUROBI,
parameters_pb2.SolverInitializerProto(),
parameters_pb2.SolveParametersProto(),
None,
interrupter,
)
expected = infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto(
feasibility=result_pb2.FEASIBILITY_STATUS_UNDETERMINED
)
self.assert_protos_equiv(iis_result, expected)
def test_compute_infeasible_subsystem_time_limit(self) -> None:
params = parameters_pb2.SolveParametersProto()
params.time_limit.FromTimedelta(datetime.timedelta(seconds=0.0))
iis_result = solver.compute_infeasible_subsystem(
_nontrivial_infeasible_model(),
parameters_pb2.SOLVER_TYPE_GUROBI,
parameters_pb2.SolverInitializerProto(),
params,
None,
None,
)
expected = infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto(
feasibility=result_pb2.FEASIBILITY_STATUS_UNDETERMINED
)
self.assert_protos_equiv(iis_result, expected)
def test_compute_infeasible_subsystem_infeasible_message_cb(self) -> None:
messages = []
iis_result = solver.compute_infeasible_subsystem(
_simple_infeasible_model(),
parameters_pb2.SOLVER_TYPE_GUROBI,
parameters_pb2.SolverInitializerProto(),
parameters_pb2.SolveParametersProto(),
messages.extend,
None,
)
self.assert_protos_equiv(iis_result, _expected_iis_success())
self.assertIn("IIS computed", "\n".join(messages))
def test_compute_infeasible_subsystem_error_wrong_solver(self) -> None:
with self.assertRaisesRegex(StatusNotOk, "SOLVER_TYPE_GLPK is not registered"):
solver.compute_infeasible_subsystem(
_simple_infeasible_model(),
parameters_pb2.SOLVER_TYPE_GLPK,
parameters_pb2.SolverInitializerProto(),
parameters_pb2.SolveParametersProto(),
None,
None,
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,273 @@
#!/usr/bin/env python3
# Copyright 2010-2022 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.
import threading
from typing import Callable, Optional, Sequence
import unittest
from google3.testing.pybase import parameterized
from google3.third_party.pybind11_abseil.status import StatusNotOk
from ortools.math_opt import callback_pb2
from ortools.math_opt import model_parameters_pb2
from ortools.math_opt import model_pb2
from ortools.math_opt import model_update_pb2
from ortools.math_opt import parameters_pb2
from ortools.math_opt import result_pb2
from ortools.math_opt.core.python import solver
def _build_simple_model() -> model_pb2.ModelProto:
model = model_pb2.ModelProto()
model.variables.ids.append(0)
model.variables.lower_bounds.append(1.0)
model.variables.upper_bounds.append(2.0)
model.variables.integers.append(False)
model.variables.names.append("x")
model.objective.maximize = True
model.objective.linear_coefficients.ids.append(0)
model.objective.linear_coefficients.values.append(1.0)
return model
def _solve_model(
model: model_pb2.ModelProto,
*,
use_solver_class: bool,
solver_type: parameters_pb2.SolverTypeProto = parameters_pb2.SOLVER_TYPE_GLOP,
solver_initializer: parameters_pb2.SolverInitializerProto = parameters_pb2.SolverInitializerProto(),
parameters: parameters_pb2.SolveParametersProto = parameters_pb2.SolveParametersProto(),
model_parameters: model_parameters_pb2.ModelSolveParametersProto = model_parameters_pb2.ModelSolveParametersProto(),
message_callback: Optional[Callable[[Sequence[str]], None]] = None,
callback_registration: callback_pb2.CallbackRegistrationProto = callback_pb2.CallbackRegistrationProto(),
user_cb: Optional[
Callable[[callback_pb2.CallbackDataProto], callback_pb2.CallbackResultProto]
] = None,
interrupter: Optional[solver.SolveInterrupter] = None,
) -> result_pb2.SolveResultProto:
"""Convenience function for both types of solve with parameter defaults."""
if use_solver_class:
pybind_solver = solver.new(
solver_type,
model,
solver_initializer,
)
return pybind_solver.solve(
parameters,
model_parameters,
message_callback,
callback_registration,
user_cb,
interrupter,
)
else:
return solver.solve(
model,
solver_type,
solver_initializer,
parameters,
model_parameters,
message_callback,
callback_registration,
user_cb,
interrupter,
)
class PybindSolverTest(parameterized.TestCase):
def tearDown(self):
super().tearDown()
self.assertEqual(solver.debug_num_solver(), 0)
@parameterized.named_parameters(
dict(testcase_name="without_solver", use_solver_class=False),
dict(testcase_name="with_solver", use_solver_class=True),
)
def test_valid_solve(self, use_solver_class: bool) -> None:
model = _build_simple_model()
result = _solve_model(model, use_solver_class=use_solver_class)
self.assertEqual(
result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL
)
self.assertTrue(result.solutions)
self.assertAlmostEqual(result.solutions[0].primal_solution.objective_value, 2.0)
@parameterized.named_parameters(
dict(testcase_name="without_solver", use_solver_class=False),
dict(testcase_name="with_solver", use_solver_class=True),
)
def test_invalid_input_throws_error(self, use_solver_class: bool) -> None:
model = _build_simple_model()
# Add invalid variable id to cause MathOpt model validation error.
model.objective.linear_coefficients.ids.append(7)
model.objective.linear_coefficients.values.append(2.0)
with self.assertRaisesRegex(StatusNotOk, "id 7 not found"):
_solve_model(model, use_solver_class=use_solver_class)
@parameterized.named_parameters(
dict(testcase_name="without_solver", use_solver_class=False),
dict(testcase_name="with_solver", use_solver_class=True),
)
def test_solve_interrupter_interrupts_solve(self, use_solver_class: bool) -> None:
model = _build_simple_model()
interrupter = solver.SolveInterrupter()
interrupter.interrupt()
result = _solve_model(
model, use_solver_class=use_solver_class, interrupter=interrupter
)
self.assertEqual(
result.termination.reason,
result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND,
)
self.assertEqual(result.termination.limit, result_pb2.LIMIT_INTERRUPTED)
@parameterized.named_parameters(
dict(testcase_name="without_solver", use_solver_class=False),
dict(testcase_name="with_solver", use_solver_class=True),
)
def test_message_callback_is_invoked(self, use_solver_class: bool) -> None:
model = _build_simple_model()
messages = []
# Message callback extends `messages` with solver output.
_solve_model(
model,
use_solver_class=use_solver_class,
parameters=parameters_pb2.SolveParametersProto(
enable_output=True, threads=1
),
message_callback=messages.extend,
)
self.assertIn("status:", "\n".join(messages))
@parameterized.named_parameters(
dict(testcase_name="without_solver", use_solver_class=False),
dict(testcase_name="with_solver", use_solver_class=True),
)
def test_user_callback_is_invoked(self, use_solver_class: bool) -> None:
model = _build_simple_model()
solution_values = []
mutex = threading.Lock()
# Callback stores solution values found during solve in `solution_values`.
def collect_solution_values_user_callback(
cb_data: callback_pb2.CallbackDataProto,
) -> callback_pb2.CallbackResultProto:
with mutex:
assert cb_data.event == callback_pb2.CALLBACK_EVENT_MIP_SOLUTION
solution_values.extend(cb_data.primal_solution_vector.values)
return callback_pb2.CallbackResultProto()
# This implicitly tests that the GIL is released, since the solve below can
# deadlock otherwise.
result = _solve_model(
model,
use_solver_class=use_solver_class,
solver_type=parameters_pb2.SOLVER_TYPE_CP_SAT,
callback_registration=callback_pb2.CallbackRegistrationProto(
request_registration=[callback_pb2.CALLBACK_EVENT_MIP_SOLUTION]
),
user_cb=collect_solution_values_user_callback,
)
self.assertEqual(
result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL
)
# `solution_values` should at least contain the optimal value 2.0.
self.assertContainsSubset(solution_values, [2.0])
@parameterized.named_parameters(
dict(testcase_name="without_solver", use_solver_class=False),
dict(testcase_name="with_solver", use_solver_class=True),
)
def test_solution_hint_is_used(self, use_solver_class: bool) -> None:
model = _build_simple_model()
solution_hint = model_parameters_pb2.SolutionHintProto()
solution_hint.variable_values.ids.append(0)
solution_hint.variable_values.values.append(1.0)
# Limit the solver so that it does not find a solution other than provided.
result = _solve_model(
model,
use_solver_class=use_solver_class,
solver_type=parameters_pb2.SOLVER_TYPE_GSCIP,
parameters=parameters_pb2.SolveParametersProto(
node_limit=0,
heuristics=parameters_pb2.EMPHASIS_OFF,
presolve=parameters_pb2.EMPHASIS_OFF,
),
model_parameters=model_parameters_pb2.ModelSolveParametersProto(
solution_hints=[solution_hint]
),
)
self.assertEqual(
result.termination.reason, result_pb2.TERMINATION_REASON_FEASIBLE
)
self.assertTrue(result.solutions)
self.assertAlmostEqual(result.solutions[0].primal_solution.objective_value, 1.0)
def test_debug_num_solver(self) -> None:
self.assertEqual(solver.debug_num_solver(), 0)
pybind_solver = solver.new(
parameters_pb2.SOLVER_TYPE_GLOP,
model_pb2.ModelProto(),
parameters_pb2.SolverInitializerProto(),
)
self.assertEqual(solver.debug_num_solver(), 1)
del pybind_solver
self.assertEqual(solver.debug_num_solver(), 0)
def test_incremental_solver_update(self) -> None:
model = _build_simple_model()
incremental_solver = solver.new(
parameters_pb2.SOLVER_TYPE_GLOP,
model,
parameters_pb2.SolverInitializerProto(),
)
result = incremental_solver.solve(
parameters_pb2.SolveParametersProto(),
model_parameters_pb2.ModelSolveParametersProto(),
None,
callback_pb2.CallbackRegistrationProto(),
None,
None,
)
self.assertEqual(
result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL
)
self.assertAlmostEqual(result.solve_stats.best_primal_bound, 2.0)
update = model_update_pb2.ModelUpdateProto()
update.variable_updates.upper_bounds.ids.append(0)
update.variable_updates.upper_bounds.values.append(2.5)
self.assertTrue(incremental_solver.update(update))
result = incremental_solver.solve(
parameters_pb2.SolveParametersProto(),
model_parameters_pb2.ModelSolveParametersProto(),
None,
callback_pb2.CallbackRegistrationProto(),
None,
None,
)
self.assertEqual(
result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL
)
self.assertAlmostEqual(result.solve_stats.best_primal_bound, 2.5)
class PybindSolveInterrupterTest(parameterized.TestCase):
def test_solve_interrupter_is_interrupted(self) -> None:
interrupter = solver.SolveInterrupter()
self.assertFalse(interrupter.is_interrupted())
interrupter.interrupt()
self.assertTrue(interrupter.is_interrupted())
del interrupter
if __name__ == "__main__":
unittest.main()

View File

@@ -11,8 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef OR_TOOLS_MATH_OPT_STORAGE_SORTED_H_
#define OR_TOOLS_MATH_OPT_STORAGE_SORTED_H_
#ifndef OR_TOOLS_MATH_OPT_CORE_SORTED_H_
#define OR_TOOLS_MATH_OPT_CORE_SORTED_H_
#include <algorithm>
#include <vector>
@@ -67,4 +67,4 @@ std::vector<K> SortedMapKeys(const google::protobuf::Map<K, V>& in_map) {
} // namespace operations_research::math_opt
#endif // OR_TOOLS_MATH_OPT_STORAGE_SORTED_H_
#endif // OR_TOOLS_MATH_OPT_CORE_SORTED_H_

View File

@@ -13,7 +13,9 @@
# External users should depend only on ":math_opt" and include
# "math_opt.h". Hence other libraries are private.
package(default_visibility = ["//ortools/math_opt:__subpackages__"])
package(default_visibility = [
"//ortools/math_opt/constraints:__subpackages__",
])
cc_library(
name = "math_opt",
@@ -52,12 +54,15 @@ cc_library(
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/core:sparse_vector_view",
"//ortools/math_opt/storage:model_storage",
"//ortools/math_opt/storage:model_storage_types",
"//ortools/math_opt/validators:ids_validator",
"//ortools/math_opt/validators:sparse_vector_validator",
"//ortools/util:status_macros",
"@com_google_absl//absl/algorithm:container",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings:string_view",
"@com_google_absl//absl/types:span",
"@com_google_protobuf//:protobuf",
],
@@ -117,6 +122,21 @@ cc_library(
],
)
cc_library(
name = "objective",
srcs = ["objective.cc"],
hdrs = ["objective.h"],
deps = [
":key_types",
":variable_and_expressions",
"//ortools/base:intops",
"//ortools/math_opt/storage:model_storage",
"//ortools/math_opt/storage:model_storage_types",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "linear_constraint",
hdrs = ["linear_constraint.h"],
@@ -175,10 +195,13 @@ cc_library(
"//ortools/gscip:gscip_cc_proto",
"//ortools/math_opt:result_cc_proto",
"//ortools/math_opt:solution_cc_proto",
"//ortools/math_opt/core:math_opt_proto_utils",
"//ortools/math_opt/storage:model_storage",
"//ortools/port:proto_utils",
"//ortools/util:fp_roundtrip_conv",
"//ortools/util:status_macros",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/time",
@@ -400,6 +423,7 @@ cc_library(
name = "statistics",
srcs = ["statistics.cc"],
hdrs = ["statistics.h"],
visibility = ["//visibility:public"],
deps = [
":model",
"//ortools/math_opt/storage:model_storage",
@@ -418,21 +442,6 @@ cc_library(
deps = ["//ortools/math_opt:model_update_cc_proto"],
)
cc_library(
name = "objective",
srcs = ["objective.cc"],
hdrs = ["objective.h"],
deps = [
":key_types",
":variable_and_expressions",
"//ortools/base:intops",
"//ortools/math_opt/storage:model_storage",
"//ortools/math_opt/storage:model_storage_types",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "compute_infeasible_subsystem_result",
srcs = ["compute_infeasible_subsystem_result.cc"],

View File

@@ -19,7 +19,9 @@
#include <optional>
#include <utility>
#include "google/protobuf/message.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "google/protobuf/repeated_field.h"
#include "ortools/base/status_macros.h"
#include "ortools/math_opt/cpp/linear_constraint.h"
#include "ortools/math_opt/cpp/solution.h"
@@ -108,6 +110,20 @@ ModelSolveParameters::SolutionHint::FromProto(
};
}
ObjectiveParametersProto ModelSolveParameters::ObjectiveParameters::Proto()
const {
ObjectiveParametersProto params;
if (objective_degradation_absolute_tolerance) {
params.set_objective_degradation_absolute_tolerance(
*objective_degradation_absolute_tolerance);
}
if (objective_degradation_relative_tolerance) {
params.set_objective_degradation_relative_tolerance(
*objective_degradation_relative_tolerance);
}
return params;
}
ModelSolveParametersProto ModelSolveParameters::Proto() const {
ModelSolveParametersProto ret;
*ret.mutable_variable_values_filter() = variable_values_filter.Proto();
@@ -160,6 +176,14 @@ ModelSolveParametersProto ModelSolveParameters::Proto() const {
variable_values->Add(branching_priorities.at(key));
}
}
for (const auto& [objective, params] : objective_parameters) {
if (objective.id()) {
(*ret.mutable_auxiliary_objective_parameters())[*objective.id()] =
params.Proto();
} else {
*ret.mutable_primary_objective_parameters() = params.Proto();
}
}
return ret;
}

View File

@@ -19,6 +19,7 @@
#include <sys/types.h>
#include <cstdint>
#include <initializer_list>
#include <optional>
#include <vector>
@@ -146,6 +147,44 @@ struct ModelSolveParameters {
// solver's default priority (usually zero).
VariableMap<int32_t> branching_priorities;
// Parameters for an individual objective in a multi-objective model.
struct ObjectiveParameters {
// Optional objective degradation absolute tolerance. For a hierarchical
// multi-objective solver, each objective fⁱ is processed in priority order:
// the solver determines the optimal objective value Γⁱ, if it exists,
// subject to all constraints in the model and the additional constraints
// that fᵏ(x) = Γᵏ (within tolerances) for each k < i. If set, a solution is
// considered to be "within tolerances" for this objective fᵏ if
// |fᵏ(x) - Γᵏ| ≤ `objective_degradation_absolute_tolerance`.
//
// See also `objective_degradation_relative_tolerance`; if both parameters
// are set for a given objective, the solver need only satisfy one to be
// considered "within tolerances".
//
// If set, must be nonnegative.
std::optional<double> objective_degradation_absolute_tolerance;
// Optional objective degradation relative tolerance. For a hierarchical
// multi-objective solver, each objective fⁱ is processed in priority order:
// the solver determines the optimal objective value Γⁱ, if it exists,
// subject to all constraints in the model and the additional constraints
// that fᵏ(x) = Γᵏ (within tolerances) for each k < i. If set, a solution is
// considered to be "within tolerances" for this objective fᵏ if
// |fᵏ(x) - Γᵏ| ≤ `objective_degradation_relative_tolerance` * |Γᵏ|.
//
// See also `objective_degradation_absolute_tolerance`; if both parameters
// are set for a given objective, the solver need only satisfy one to be
// considered "within tolerances".
//
// If set, must be nonnegative.
std::optional<double> objective_degradation_relative_tolerance;
// Returns the proto equivalent of this object.
ObjectiveParametersProto Proto() const;
};
// Parameters for individual objectives in a multi-objective model.
ObjectiveMap<ObjectiveParameters> objective_parameters;
// Returns a failure if the referenced variables don't belong to the input
// expected_storage (which must not be nullptr).
absl::Status CheckModelStorage(const ModelStorage* expected_storage) const;

View File

@@ -81,6 +81,8 @@ std::optional<absl::string_view> Enum<SolverType>::ToOptString(
return "scs";
case SolverType::kHighs:
return "highs";
case SolverType::kSantorini:
return "santorini";
}
return std::nullopt;
}
@@ -89,7 +91,7 @@ absl::Span<const SolverType> Enum<SolverType>::AllValues() {
static constexpr SolverType kSolverTypeValues[] = {
SolverType::kGscip, SolverType::kGurobi, SolverType::kGlop,
SolverType::kCpSat, SolverType::kGlpk, SolverType::kEcos,
SolverType::kScs, SolverType::kHighs,
SolverType::kScs, SolverType::kHighs, SolverType::kSantorini,
};
return absl::MakeConstSpan(kSolverTypeValues);
}

View File

@@ -96,6 +96,12 @@ enum class SolverType {
//
// Supports LP and MIP problems (convex QPs are unimplemented).
kHighs = SOLVER_TYPE_HIGHS,
// MathOpt's reference implementation of a MIP solver.
//
// Slow/not recommended for production. Not an LP solver (no dual information
// returned).
kSantorini = SOLVER_TYPE_SANTORINI,
};
MATH_OPT_DEFINE_ENUM(SolverType, SOLVER_TYPE_UNSPECIFIED);

View File

@@ -13,6 +13,8 @@
#include "ortools/math_opt/cpp/solve_result.h"
#include <initializer_list>
#include <limits>
#include <optional>
#include <ostream>
#include <sstream>
@@ -20,7 +22,10 @@
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
@@ -28,16 +33,84 @@
#include "absl/types/span.h"
#include "ortools/base/protoutil.h"
#include "ortools/base/status_macros.h"
#include "ortools/math_opt/core/math_opt_proto_utils.h"
#include "ortools/math_opt/cpp/linear_constraint.h"
#include "ortools/math_opt/cpp/variable_and_expressions.h"
#include "ortools/math_opt/result.pb.h"
#include "ortools/math_opt/solution.pb.h"
#include "ortools/math_opt/storage/model_storage.h"
#include "ortools/port/proto_utils.h"
#include "ortools/util/fp_roundtrip_conv.h"
#include "ortools/util/status_macros.h"
namespace operations_research {
namespace math_opt {
namespace operations_research::math_opt {
namespace {
constexpr double kInf = std::numeric_limits<double>::infinity();
} // namespace
ObjectiveBoundsProto ObjectiveBounds::Proto() const {
ObjectiveBoundsProto proto;
proto.set_primal_bound(primal_bound);
proto.set_dual_bound(dual_bound);
return proto;
}
ObjectiveBounds ObjectiveBounds::FromProto(
const ObjectiveBoundsProto& objective_bounds_proto) {
ObjectiveBounds result;
result.primal_bound = objective_bounds_proto.primal_bound();
result.dual_bound = objective_bounds_proto.dual_bound();
return result;
}
std::ostream& operator<<(std::ostream& ostr,
const ObjectiveBounds& objective_bounds) {
ostr << "{primal_bound: "
<< RoundTripDoubleFormat(objective_bounds.primal_bound);
ostr << ", dual_bound: "
<< RoundTripDoubleFormat(objective_bounds.dual_bound);
ostr << "}";
return ostr;
}
std::string ObjectiveBounds::ToString() const {
std::ostringstream stream;
stream << *this;
return stream.str();
}
ObjectiveBounds ObjectiveBounds::MakeTrivial(const bool is_maximize) {
const double primal_bound = is_maximize ? -kInf : kInf;
const double dual_bound = -primal_bound;
return ObjectiveBounds{.primal_bound = primal_bound,
.dual_bound = dual_bound};
}
ObjectiveBounds ObjectiveBounds::MaximizeMakeTrivial() {
return ObjectiveBounds::MakeTrivial(true);
}
ObjectiveBounds ObjectiveBounds::MinimizeMakeTrivial() {
return ObjectiveBounds::MakeTrivial(false);
}
ObjectiveBounds ObjectiveBounds::MakeUnbounded(const bool is_maximize) {
const double primal_bound = is_maximize ? kInf : -kInf;
const double dual_bound = primal_bound;
return ObjectiveBounds{.primal_bound = primal_bound,
.dual_bound = dual_bound};
}
ObjectiveBounds ObjectiveBounds::MinimizeMakeUnbounded() {
return ObjectiveBounds::MakeUnbounded(/*is_maximize=*/false);
}
ObjectiveBounds ObjectiveBounds::MaximizeMakeUnbounded() {
return ObjectiveBounds::MakeUnbounded(/*is_maximize=*/true);
}
ObjectiveBounds ObjectiveBounds::MakeOptimal(double objective_value) {
return ObjectiveBounds{.primal_bound = objective_value,
.dual_bound = objective_value};
}
std::optional<absl::string_view> Enum<FeasibilityStatus>::ToOptString(
FeasibilityStatus value) {
@@ -140,27 +213,110 @@ absl::Span<const Limit> Enum<Limit>::AllValues() {
return absl::MakeConstSpan(kLimitValues);
}
Termination::Termination(const TerminationReason reason, std::string detail)
: reason(reason), detail(std::move(detail)) {}
Termination::Termination(const bool is_maximize, const TerminationReason reason,
std::string detail)
: reason(reason),
detail(std::move(detail)),
objective_bounds(ObjectiveBounds::MakeTrivial(is_maximize)) {}
Termination Termination::Feasible(const Limit limit, const std::string detail) {
Termination termination(TerminationReason::kFeasible, detail);
Termination Termination::Optimal(const double primal_objective_value,
const double dual_objective_value,
const std::string detail) {
Termination termination(/*is_maximize=*/false, TerminationReason::kOptimal,
detail);
termination.objective_bounds.primal_bound = primal_objective_value;
termination.objective_bounds.dual_bound = dual_objective_value;
termination.problem_status.primal_status = FeasibilityStatus::kFeasible;
termination.problem_status.dual_status = FeasibilityStatus::kFeasible;
return termination;
}
Termination Termination::Optimal(const double objective_value,
const std::string detail) {
return Optimal(objective_value, objective_value, detail);
}
Termination Termination::Infeasible(const bool is_maximize,
FeasibilityStatus dual_feasibility_status,
const std::string detail) {
Termination termination(is_maximize, TerminationReason::kInfeasible, detail);
if (dual_feasibility_status == FeasibilityStatus::kFeasible) {
termination.objective_bounds.dual_bound =
termination.objective_bounds.primal_bound;
}
termination.problem_status.primal_status = FeasibilityStatus::kInfeasible;
termination.problem_status.dual_status = dual_feasibility_status;
return termination;
}
Termination Termination::InfeasibleOrUnbounded(
const bool is_maximize, const FeasibilityStatus dual_feasibility_status,
const std::string detail) {
Termination termination(is_maximize,
TerminationReason::kInfeasibleOrUnbounded, detail);
termination.problem_status.primal_status = FeasibilityStatus::kUndetermined;
termination.problem_status.dual_status = dual_feasibility_status;
if (dual_feasibility_status == FeasibilityStatus::kUndetermined) {
termination.problem_status.primal_or_dual_infeasible = true;
}
return termination;
}
Termination Termination::Unbounded(const bool is_maximize,
const std::string detail) {
Termination termination(is_maximize, TerminationReason::kUnbounded, detail);
termination.objective_bounds = ObjectiveBounds::MakeUnbounded(is_maximize);
termination.problem_status.primal_status = FeasibilityStatus::kFeasible;
termination.problem_status.dual_status = FeasibilityStatus::kInfeasible;
return termination;
}
Termination Termination::NoSolutionFound(
const bool is_maximize, Limit limit,
const std::optional<double> optional_dual_objective,
const std::string detail) {
Termination termination(is_maximize, TerminationReason::kNoSolutionFound,
detail);
termination.problem_status.primal_status = FeasibilityStatus::kUndetermined;
termination.problem_status.dual_status = FeasibilityStatus::kUndetermined;
if (optional_dual_objective.has_value()) {
termination.objective_bounds.dual_bound = *optional_dual_objective;
termination.problem_status.dual_status = FeasibilityStatus::kFeasible;
}
termination.limit = limit;
return termination;
}
Termination Termination::NoSolutionFound(const Limit limit,
const std::string detail) {
Termination termination(TerminationReason::kNoSolutionFound, detail);
Termination Termination::Feasible(
const bool is_maximize, const Limit limit,
const double finite_primal_objective,
const std::optional<double> optional_dual_objective,
const std::string detail) {
Termination termination(is_maximize, TerminationReason::kFeasible, detail);
termination.problem_status.primal_status = FeasibilityStatus::kFeasible;
termination.objective_bounds.primal_bound = finite_primal_objective;
termination.problem_status.dual_status = FeasibilityStatus::kUndetermined;
if (optional_dual_objective.has_value()) {
termination.objective_bounds.dual_bound = *optional_dual_objective;
termination.problem_status.dual_status = FeasibilityStatus::kFeasible;
}
termination.limit = limit;
return termination;
}
Termination Termination::Cutoff(const bool is_maximize,
const std::string detail) {
return NoSolutionFound(is_maximize, Limit::kCutoff,
/*optional_dual_objective=*/std::nullopt, detail);
}
TerminationProto Termination::Proto() const {
TerminationProto proto;
proto.set_reason(EnumToProto(reason));
proto.set_limit(EnumToProto(limit));
proto.set_detail(detail);
*proto.mutable_problem_status() = problem_status.Proto();
*proto.mutable_objective_bounds() = objective_bounds.Proto();
return proto;
}
@@ -207,8 +363,15 @@ absl::StatusOr<Termination> Termination::FromProto(
if (!reason.has_value()) {
return absl::InvalidArgumentError("reason must be specified");
}
Termination result(*reason, termination_proto.detail());
Termination result(/*is_maximize=*/false, *reason,
termination_proto.detail());
result.limit = EnumFromProto(termination_proto.limit());
OR_ASSIGN_OR_RETURN3(
result.problem_status,
ProblemStatus::FromProto(termination_proto.problem_status()),
_ << "invalid problem_status");
result.objective_bounds =
ObjectiveBounds::FromProto(termination_proto.objective_bounds());
return result;
}
@@ -218,9 +381,10 @@ std::ostream& operator<<(std::ostream& ostr, const Termination& termination) {
ostr << ", limit: " << *termination.limit;
}
if (!termination.detail.empty()) {
// TODO(b/200835670): quote detail and escape it properly.
ostr << ", detail: " << termination.detail;
ostr << ", detail: " << '"' << absl::CEscape(termination.detail) << '"';
}
ostr << ", problem_status: " << termination.problem_status;
ostr << ", objective_bounds: " << termination.objective_bounds;
ostr << "}";
return ostr;
}
@@ -278,9 +442,6 @@ absl::StatusOr<SolveStatsProto> SolveStats::Proto() const {
RETURN_IF_ERROR(
util_time::EncodeGoogleApiProto(solve_time, proto.mutable_solve_time()))
<< "invalid solve_time (value must be finite)";
proto.set_best_primal_bound(best_primal_bound);
proto.set_best_dual_bound(best_dual_bound);
*proto.mutable_problem_status() = problem_status.Proto();
proto.set_simplex_iterations(simplex_iterations);
proto.set_barrier_iterations(barrier_iterations);
proto.set_first_order_iterations(first_order_iterations);
@@ -295,12 +456,6 @@ absl::StatusOr<SolveStats> SolveStats::FromProto(
result.solve_time,
util_time::DecodeGoogleApiProto(solve_stats_proto.solve_time()),
_ << "invalid solve_time");
result.best_primal_bound = solve_stats_proto.best_primal_bound();
result.best_dual_bound = solve_stats_proto.best_dual_bound();
OR_ASSIGN_OR_RETURN3(
result.problem_status,
ProblemStatus::FromProto(solve_stats_proto.problem_status()),
_ << "invalid problem_status");
result.simplex_iterations = solve_stats_proto.simplex_iterations();
result.barrier_iterations = solve_stats_proto.barrier_iterations();
result.first_order_iterations = solve_stats_proto.first_order_iterations();
@@ -310,11 +465,6 @@ absl::StatusOr<SolveStats> SolveStats::FromProto(
std::ostream& operator<<(std::ostream& ostr, const SolveStats& solve_stats) {
ostr << "{solve_time: " << solve_stats.solve_time;
ostr << ", best_primal_bound: "
<< RoundTripDoubleFormat(solve_stats.best_primal_bound);
ostr << ", best_dual_bound: "
<< RoundTripDoubleFormat(solve_stats.best_dual_bound);
ostr << ", problem_status: " << solve_stats.problem_status;
ostr << ", simplex_iterations: " << solve_stats.simplex_iterations;
ostr << ", barrier_iterations: " << solve_stats.barrier_iterations;
ostr << ", first_order_iterations: " << solve_stats.first_order_iterations;
@@ -362,12 +512,31 @@ absl::StatusOr<SolveResultProto> SolveResult::Proto() const {
}
return result;
}
namespace {
TerminationProto UpgradedTerminationProtoForStatsMigration(
const SolveResultProto& solve_result_proto) {
TerminationProto termination;
termination.set_reason(solve_result_proto.termination().reason());
termination.set_limit(solve_result_proto.termination().limit());
termination.set_detail(solve_result_proto.termination().detail());
*termination.mutable_problem_status() = GetProblemStatus(solve_result_proto);
*termination.mutable_objective_bounds() =
GetObjectiveBounds(solve_result_proto);
return termination;
}
} // namespace
absl::StatusOr<SolveResult> SolveResult::FromProto(
const ModelStorage* model, const SolveResultProto& solve_result_proto) {
OR_ASSIGN_OR_RETURN3(auto termination,
Termination::FromProto(solve_result_proto.termination()),
_ << "invalid termination");
OR_ASSIGN_OR_RETURN3(
auto termination,
Termination::FromProto(
// TODO(b/290091715): Remove once solve_stats proto no longer has
// best_primal/dual_bound/problem_status and
// problem_status/objective_bounds are guaranteed to be present in
// termination proto.
UpgradedTerminationProtoForStatsMigration(solve_result_proto)),
_ << "invalid termination");
SolveResult result(std::move(termination));
OR_ASSIGN_OR_RETURN3(result.solve_stats,
SolveStats::FromProto(solve_result_proto.solve_stats()),
@@ -421,7 +590,15 @@ const PrimalSolution& SolveResult::best_primal_solution() const {
}
double SolveResult::best_objective_bound() const {
return solve_stats.best_dual_bound;
return termination.objective_bounds.dual_bound;
}
double SolveResult::primal_bound() const {
return termination.objective_bounds.primal_bound;
}
double SolveResult::dual_bound() const {
return termination.objective_bounds.dual_bound;
}
double SolveResult::objective_value() const {
@@ -435,9 +612,9 @@ double SolveResult::objective_value(const Objective objective) const {
}
bool SolveResult::bounded() const {
return solve_stats.problem_status.primal_status ==
return termination.problem_status.primal_status ==
FeasibilityStatus::kFeasible &&
solve_stats.problem_status.dual_status == FeasibilityStatus::kFeasible;
termination.problem_status.dual_status == FeasibilityStatus::kFeasible;
}
const VariableMap<double>& SolveResult::variable_values() const {
@@ -521,5 +698,4 @@ std::ostream& operator<<(std::ostream& out, const SolveResult& result) {
return out;
}
} // namespace math_opt
} // namespace operations_research
} // namespace operations_research::math_opt

View File

@@ -24,6 +24,7 @@
#include <utility>
#include <vector>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/time/time.h"
#include "ortools/gscip/gscip.pb.h"
@@ -55,14 +56,16 @@ MATH_OPT_DEFINE_ENUM(FeasibilityStatus, FEASIBILITY_STATUS_UNSPECIFIED);
// Feasibility status of the primal problem and its dual (or the dual of a
// continuous relaxation) as claimed by the solver. The solver is not required
// to return a certificate for the claim (e.g. the solver may claim primal
// feasibility without returning a primal feasible solutuion). This combined
// feasibility without returning a primal feasible solution). This combined
// status gives a comprehensive description of a solver's claims about
// feasibility and unboundedness of the solved problem. For instance,
//
// * a feasible status for primal and dual problems indicates the primal is
// feasible and bounded and likely has an optimal solution (guaranteed for
// problems without non-linear constraints).
// * a primal feasible and a dual infeasible status indicates the primal
// problem is unbounded (i.e. has arbitrarily good solutions).
//
// Note that a dual infeasible status by itself (i.e. accompanied by an
// undetermined primal status) does not imply the primal problem is unbounded as
// we could have both problems be infeasible. Also, while a primal and dual
@@ -98,66 +101,6 @@ struct SolveStats {
// Solver::Solve(). Note: this does not include work done building the model.
absl::Duration solve_time = absl::ZeroDuration();
// TODO(b/195295177): Update to add clearer contracts once PDLP's bounds
// contract is clarified.
// Solver claims the optimal value is equal or better (smaller for
// minimization and larger for maximization) than best_primal_bound up to the
// solvers primal feasibility tolerance (see warning below):
// * best_primal_bound is trivial (+inf for minimization and -inf
// maximization) when the solver does not claim to have such bound.
// * best_primal_bound can be closer to the optimal value than the objective
// of the best primal feasible solution. In particular, best_primal_bound
// may be non-trivial even when no primal feasible solutions are returned.
// Warning: The precise claim is that there exists a primal solution that:
// * is numerically feasible (i.e. feasible up to the solvers tolerance), and
// * has an objective value best_primal_bound.
// This numerically feasible solution could be slightly infeasible, in which
// case best_primal_bound could be strictly better than the optimal value.
// Translating a primal feasibility tolerance to a tolerance on
// best_primal_bound is non-trivial, specially when the feasibility tolerance
// is relatively large (e.g. when solving with PDLP).
double best_primal_bound = 0.0;
// Solver claims the optimal value is equal or worse (larger for
// minimization and smaller for maximization) than best_dual_bound up to the
// solvers dual feasibility tolerance (see warning below):
// * best_dual_bound is trivial (-inf for minimization and +inf
// maximization) when the solver does not claim to have such bound.
// Similarly to best_primal_bound, this may happen for some solvers even
// when returning optimal. MIP solvers will typically report a bound even
// if it is imprecise.
// * for continuous problems best_dual_bound can be closer to the optimal
// value than the objective of the best dual feasible solution. For MIP
// one of the first non-trivial values for best_dual_bound is often the
// optimal value of the LP relaxation of the MIP.
// * best_dual_bound should be better (smaller for minimization and larger
// for maximization) than best_primal_bound up to the solvers tolerances
// (see warning below).
// Warning:
// * For continuous problems, the precise claim is that there exists a
// dual solution that:
// * is numerically feasible (i.e. feasible up to the solvers tolerance),
// and
// * has an objective value best_dual_bound.
// This numerically feasible solution could be slightly infeasible, in
// which case best_dual_bound could be strictly worse than the optimal
// value and best_primal_bound. Similar to the primal case, translating a
// dual feasibility tolerance to a tolerance on best_dual_bound is
// non-trivial, specially when the feasibility tolerance is relatively
// large. However, some solvers provide a corrected version of
// best_dual_bound that can be numerically safer. This corrected version
// can be accessed through the solver's specific output (e.g. for PDLP,
// pdlp_output.convergence_information.corrected_dual_objective).
// * For MIP solvers, best_dual_bound may be associated to a dual solution
// for some continuous relaxation (e.g. LP relaxation), but it is often a
// complex consequence of the solvers execution and is typically more
// imprecise than the bounds reported by LP solvers.
double best_dual_bound = 0.0;
// Feasibility statuses for primal and dual problems.
ProblemStatus problem_status;
int simplex_iterations = 0;
int barrier_iterations = 0;
@@ -190,7 +133,7 @@ enum class TerminationReason {
kUnbounded = TERMINATION_REASON_UNBOUNDED,
// The primal problem is either infeasible or unbounded. More details on the
// problem status may be available in solve_stats.problem_status. Note that
// problem status may be available in termination.problem_status. Note that
// Gurobi's unbounded status may be mapped here.
kInfeasibleOrUnbounded = TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED,
@@ -282,11 +225,74 @@ enum class Limit {
MATH_OPT_DEFINE_ENUM(Limit, LIMIT_UNSPECIFIED);
// Bounds on the optimal objective value.
struct ObjectiveBounds {
// Solver claims there exists a primal solution that is numerically feasible
// (i.e. feasible up to the solvers tolerance), and whose objective value is
// primal_bound.
//
// The optimal value is equal or better (smaller for min objectives and larger
// for max objectives) than primal_bound, but only up to solver-tolerances.
double primal_bound = 0.0;
// Solver claims there exists a dual solution that is numerically feasible
// (i.e. feasible up to the solvers tolerance), and whose objective value is
// dual_bound.
//
// For MIP solvers, the associated dual problem may be some continuous
// relaxation (e.g. LP relaxation), but it is often an implicitly defined
// problem that is a complex consequence of the solvers execution. For both
// continuous and MIP solvers, the optimal value is equal or worse (larger for
// min objective and smaller for max objectives) than dual_bound, but only up
// to solver-tolerances. Some continuous solvers provide a numerically safer
// dual bound through solver's specific output (e.g. for PDLP,
// pdlp_output.convergence_information.corrected_dual_objective).
double dual_bound = 0.0;
// Returns trivial bounds.
//
// Trivial bounds are:
// * for a maximization:
// - primal_bound = -inf
// - dual_bound = +inf
// * for a minimization:
// - primal_bound = +inf
// - dual_bound = -inf
static ObjectiveBounds MakeTrivial(bool is_maximize);
static ObjectiveBounds MaximizeMakeTrivial();
static ObjectiveBounds MinimizeMakeTrivial();
// Returns unbounded bounds.
//
// Unbounded bounds are:
// * for a maximization:
// - primal_bound = dual_bound = +inf
// * for a minimization:
// - primal_bound = dual_bound = -inf
static ObjectiveBounds MakeUnbounded(bool is_maximize);
static ObjectiveBounds MinimizeMakeUnbounded();
static ObjectiveBounds MaximizeMakeUnbounded();
// Sets both bounds to objective_value.
static ObjectiveBounds MakeOptimal(double objective_value);
static ObjectiveBounds FromProto(
const ObjectiveBoundsProto& objective_bounds_proto);
ObjectiveBoundsProto Proto() const;
std::string ToString() const;
};
std::ostream& operator<<(std::ostream& ostr,
const ObjectiveBounds& objective_bounds);
// All information regarding why a call to Solve() terminated.
struct Termination {
// When the reason is kFeasible or kNoSolutionFound, please use the static
// functions Feasible and NoSolutionFound.
explicit Termination(TerminationReason reason, std::string detail = {});
// Returns a Termination with the provided reason and details along with
// trivial bounds and kUndetermined statuses.
// A variety of static factory functions are provided below for common
// Termination conditions, generally prefer these if applicable.
Termination(bool is_maximize, TerminationReason reason,
std::string detail = {});
// Additional information in `limit` when value is kFeasible or
// kNoSolutionFound, see `limit` for details.
@@ -304,6 +310,12 @@ struct Termination {
// Limit::kUndetermined is used when the cause cannot be determined.
std::string detail;
// Feasibility statuses for primal and dual problems.
ProblemStatus problem_status;
// Bounds on the optimal objective value.
ObjectiveBounds objective_bounds;
// Returns true if a limit was reached (i.e. if reason is kFeasible or
// kNoSolutionFound, and limit is not empty).
bool limit_reached() const;
@@ -329,11 +341,91 @@ struct Termination {
absl::Status ReasonIsAnyOf(
std::initializer_list<TerminationReason> reasons) const;
// Sets the reason to kFeasible
static Termination Feasible(Limit limit, std::string detail = {});
// Returns termination with reason kOptimal, the provided objective for both
// primal and dual bounds, and kFeasible primal and dual statuses.
static Termination Optimal(double objective_value, std::string detail = {});
// Sets the reason to kNoSolutionFound
static Termination NoSolutionFound(Limit limit, std::string detail = {});
// Returns termination with reason kOptimal, the provided objective bounds and
// kFeasible primal and dual statuses.
static Termination Optimal(double primal_objective_value,
double dual_objective_value,
std::string detail = {});
// Returns a termination with reason kInfeasible, primal status kInfeasible
// and the provided dual status.
//
// It sets a trivial primal bound and a dual bound based on the provided dual
// status, which should be kFeasible or kUndetermined. If the dual status is
// kUndetermined, then the dual bound will be trivial and if the dual status
// is kFeasible, then the dual bound will be equal to the primal bound.
//
// The convention for infeasible MIPs is that dual_feasibility_status is
// feasible (There always exist a dual feasible convex relaxation of an
// infeasible MIP).
static Termination Infeasible(bool is_maximize,
FeasibilityStatus dual_feasibility_status =
FeasibilityStatus::kUndetermined,
std::string detail = {});
// Returns a termination with reason kInfeasibleOrUnbounded, primal status
// kUndetermined, the provided dual status (which should be kUndetermined or
// kInfeasible) and trivial bounds.
//
// primal_or_dual_infeasible is set if dual_feasibility_status is
// kUndetermined.
static Termination InfeasibleOrUnbounded(
bool is_maximize,
FeasibilityStatus dual_feasibility_status =
FeasibilityStatus::kUndetermined,
std::string detail = {});
// Returns a termination with reason kUnbounded, primal status kFeasible,
// dual status kInfeasible and unbounded bounds.
static Termination Unbounded(bool is_maximize, std::string detail = {});
// Returns a termination with reason kNoSolutionFound and primal status
// kUndetermined.
//
// Assumes dual solution exists iff optional_dual_objective is set even if
// infinite (some solvers return feasible dual solutions without an objective
// value). optional_dual_objective should not be set when limit is
// kCutoff for a valid TerminationProto to be returned (use LimitCutoff()
// below instead).
//
// It sets a trivial primal bound. The dual bound is either set to the
// optional_dual_objective if set, else to a trivial value.
//
// TODO(b/290359402): Consider improving to require a finite dual bound when
// dual feasible solutions are returned.
static Termination NoSolutionFound(
bool is_maximize, Limit limit,
std::optional<double> optional_dual_objective = std::nullopt,
std::string detail = {});
// Returns a termination with reason kFeasible and primal status kFeasible.
// The dual status depends on optional_dual_objective.
//
// finite_primal_objective should be finite and limit should not be
// kCutoff for a valid TerminationProto to be returned (use LimitCutoff()
// below instead).
//
// Assumes dual solution exists iff optional_dual_objective is set even if
// infinite (some solvers return feasible dual solutions without an objective
// value). If set the dual status is set to kFeasible, else
// it is kUndetermined.
//
// It sets the primal bound based on the primal objective. The dual bound is
// either set to the optional_dual_objective if set, else to a trivial value.
//
// TODO(b/290359402): Consider improving to require a finite dual bound when
// dual feasible solutions are returned.
static Termination Feasible(
bool is_maximize, Limit limit, double finite_primal_objective,
std::optional<double> optional_dual_objective = std::nullopt,
std::string detail = {});
// Calls NoSolutionFound() with LIMIT_CUTOFF LIMIT.
static Termination Cutoff(bool is_maximize, std::string detail = {});
// Will return an error if termination_proto.reason is UNSPECIFIED.
static absl::StatusOr<Termination> FromProto(
@@ -420,6 +512,16 @@ struct SolveResult {
absl::Duration solve_time() const { return solve_stats.solve_time; }
// A primal bound on the optimal objective value as described in
// ObjectiveBounds. Will return a valid (possibly infinite) bound even if
// no primal feasible solutions are available.
double primal_bound() const;
// A dual bound on the optimal objective value as described in
// ObjectiveBounds. Will return a valid (possibly infinite) bound even if
// no dual feasible solutions are available.
double dual_bound() const;
// Indicates if at least one primal feasible solution is available.
//
// For SolveResults generated by calling Solver::Solve(), when
@@ -435,10 +537,20 @@ struct SolveResult {
// The objective value of the best primal feasible solution. Will CHECK fail
// if there are no primal feasible solutions.
//
// primal_bound() above is guaranteed to be at least as good (larger or equal
// for max problems and smaller or equal for min problems) as
// objective_value() and will never CHECK fail, so it may be preferable in
// some cases. Note that primal_bound() could be better than objective_value()
// even for optimal terminations, but on such optimal termination, both should
// satisfy the optimality tolerances.
double objective_value() const;
double objective_value(Objective objective) const;
// A bound on the best possible objective value.
//
// best_objective_bound() is always equal to dual_bound(), so they can be
// used interchangeably.
double best_objective_bound() const;
// The variable values from the best primal feasible solution. Will CHECK fail

View File

@@ -15,11 +15,23 @@
#include <cstdint>
#include <optional>
#include <utility>
#include <vector>
#include "absl/algorithm/container.h"
#include "absl/container/flat_hash_map.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "google/protobuf/map.h"
#include "ortools/base/status_builder.h"
#include "ortools/base/status_macros.h"
#include "ortools/math_opt/core/sparse_vector_view.h"
#include "ortools/math_opt/cpp/basis_status.h"
#include "ortools/math_opt/cpp/linear_constraint.h"
#include "ortools/math_opt/cpp/objective.h"
#include "ortools/math_opt/cpp/variable_and_expressions.h"
#include "ortools/math_opt/storage/model_storage.h"
#include "ortools/math_opt/storage/model_storage_types.h"
namespace operations_research::math_opt {
namespace {

View File

@@ -11,17 +11,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
package(default_visibility = ["//ortools/math_opt:__subpackages__"])
package(default_visibility = ["//visibility:public"])
cc_library(
name = "proto_converter",
srcs = ["proto_converter.cc"],
hdrs = ["proto_converter.h"],
visibility = ["//visibility:public"],
deps = [
"//ortools/base:status_macros",
"//ortools/linear_solver",
"//ortools/linear_solver:linear_solver_cc_proto",
"//ortools/linear_solver:model_validator",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:model_parameters_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",

View File

@@ -11,7 +11,29 @@
# See the License for the specific language governing permissions and
# limitations under the License.
package(default_visibility = ["//ortools/math_opt:__subpackages__"])
cc_library(
name = "general_constraint_to_mip",
srcs = ["general_constraint_to_mip.cc"],
hdrs = ["general_constraint_to_mip.h"],
visibility = ["//visibility:public"],
deps = [
":linear_expr_util",
"//ortools/math_opt/cpp:math_opt",
"@com_google_absl//absl/status",
"@com_google_absl//absl/strings:str_format",
],
)
cc_library(
name = "linear_expr_util",
srcs = ["linear_expr_util.cc"],
hdrs = ["linear_expr_util.h"],
visibility = ["//visibility:public"],
deps = [
"//ortools/math_opt/cpp:math_opt",
"@com_google_absl//absl/algorithm:container",
],
)
cc_library(
name = "solution_feasibility_checker",
@@ -32,11 +54,18 @@ cc_library(
)
cc_library(
name = "linear_expr_util",
srcs = ["linear_expr_util.cc"],
hdrs = ["linear_expr_util.h"],
name = "solution_improvement",
srcs = ["solution_improvement.cc"],
hdrs = ["solution_improvement.h"],
visibility = ["//visibility:public"],
deps = [
"//ortools/base:status_macros",
"//ortools/math_opt/core:math_opt_proto_utils",
"//ortools/math_opt/cpp:math_opt",
"@com_google_absl//absl/algorithm:container",
"//ortools/math_opt/validators:model_validator",
"//ortools/util:fp_roundtrip_conv",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
],
)

View File

@@ -58,6 +58,39 @@ message SolutionHintProto {
SparseDoubleVectorProto dual_values = 2;
}
// Parameters for an individual objective in a multi-objective model.
message ObjectiveParametersProto {
// Optional objective degradation absolute tolerance. For a hierarchical
// multi-objective solver, each objective fⁱ is processed in priority order:
// the solver determines the optimal objective value Γⁱ, if it exists, subject
// to all constraints in the model and the additional constraints that
// fᵏ(x) = Γᵏ (within tolerances) for each k < i. If set, a solution is
// considered to be "within tolerances" for this objective fᵏ if
// |fᵏ(x) - Γᵏ| ≤ `objective_degradation_absolute_tolerance`.
//
// See also `objective_degradation_relative_tolerance`; if both parameters are
// set for a given objective, the solver need only satisfy one to be
// considered "within tolerances".
//
// If set, must be nonnegative.
optional double objective_degradation_absolute_tolerance = 7;
// Optional objective degradation relative tolerance. For a hierarchical
// multi-objective solver, each objective fⁱ is processed in priority order:
// the solver determines the optimal objective value Γⁱ, if it exists, subject
// to all constraints in the model and the additional constraints that
// fᵏ(x) = Γᵏ (within tolerances) for each k < i. If set, a solution is
// considered to be "within tolerances" for this objective fᵏ if
// |fᵏ(x) - Γᵏ| ≤ `objective_degradation_relative_tolerance` * |Γᵏ|.
//
// See also `objective_degradation_absolute_tolerance`; if both parameters are
// set for a given objective, the solver need only satisfy one to be
// considered "within tolerances".
//
// If set, must be nonnegative.
optional double objective_degradation_relative_tolerance = 8;
}
// TODO(b/183628247): follow naming convention in fields below.
// Parameters to control a single solve that are specific to the input model
// (see SolveParametersProto for model independent parameters).
@@ -103,4 +136,14 @@ message ModelSolveParametersProto {
// * branching_priorities.values must be finite.
// * branching_priorities.ids must be elements of VariablesProto.ids.
SparseInt32VectorProto branching_priorities = 6;
// Optional parameters for the primary objective in a multi-objective model.
ObjectiveParametersProto primary_objective_parameters = 7;
// Optional parameters for the auxiliary objectives in a multi-objective
// model.
//
// Requirements:
// * Map keys must also be map keys of ModelProto.auxiliary_objectives.
map<int64, ObjectiveParametersProto> auxiliary_objective_parameters = 8;
}

View File

@@ -22,6 +22,7 @@ import "ortools/gscip/gscip.proto";
import "ortools/math_opt/solvers/glpk.proto";
import "ortools/math_opt/solvers/gurobi.proto";
import "ortools/math_opt/solvers/highs.proto";
import "ortools/math_opt/solvers/osqp.proto";
import "ortools/sat/sat_parameters.proto";
option java_package = "com.google.ortools.mathopt";
@@ -73,7 +74,11 @@ enum SolverTypeProto {
// for details.
SOLVER_TYPE_GLPK = 6;
reserved 7;
// The Operator Splitting Quadratic Program (OSQP) solver (third party).
//
// Supports continuous problems with linear constraints and linear or convex
// quadratic objectives. Uses a first-order method.
SOLVER_TYPE_OSQP = 7;
// The Embedded Conic Solver (ECOS) (third party).
//
@@ -89,6 +94,12 @@ enum SolverTypeProto {
//
// Supports LP and MIP problems (convex QPs are unimplemented).
SOLVER_TYPE_HIGHS = 10;
// MathOpt's reference implementation of a MIP solver.
//
// Slow/not recommended for production. Not an LP solver (no dual information
// returned).
SOLVER_TYPE_SANTORINI = 11;
}
// Selects an algorithm for solving linear programs.
@@ -337,7 +348,15 @@ message SolveParametersProto {
reserved 16;
reserved 19;
// Users should prefer the generic MathOpt parameters over OSQP-level
// parameters, when available:
// * Prefer SolveParametersProto.enable_output to OsqpSettingsProto.verbose.
// * Prefer SolveParametersProto.time_limit to OsqpSettingsProto.time_limit.
// * Prefer SolveParametersProto.iteration_limit to
// OsqpSettingsProto.iteration_limit.
// * If a less granular configuration is acceptable, prefer
// SolveParametersProto.scaling to OsqpSettingsProto.
OsqpSettingsProto osqp = 19;
GlpkParametersProto glpk = 26;

View File

@@ -0,0 +1,190 @@
# Copyright 2010-2022 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.
load("@pip_deps//:requirements.bzl", "requirement")
load("@rules_python//python:defs.bzl", "py_library")
# External users should depend only on ":mathopt" and import "mathopt".
# Hence other libraries are private.
package(default_visibility = ["//visibility:private"])
py_library(
name = "mathopt",
srcs = ["mathopt.py"],
visibility = ["//visibility:public"],
deps = [
":callback",
":compute_infeasible_subsystem_result",
":hash_model_storage",
":message_callback",
":model",
":model_parameters",
":model_storage",
":parameters",
":result",
":solution",
":solve",
":sparse_containers",
],
)
py_library(
name = "model_storage",
srcs = ["model_storage.py"],
visibility = ["//ortools/math_opt/python:__subpackages__"],
deps = [
"//ortools/math_opt:model_py_pb2",
"//ortools/math_opt:model_update_py_pb2",
],
)
py_library(
name = "hash_model_storage",
srcs = ["hash_model_storage.py"],
deps = [
":model_storage",
"//ortools/math_opt:model_py_pb2",
"//ortools/math_opt:model_update_py_pb2",
"//ortools/math_opt:sparse_containers_py_pb2",
],
)
py_library(
name = "model",
srcs = ["model.py"],
deps = [
":hash_model_storage",
":model_storage",
requirement("immutabledict"),
"//ortools/math_opt:model_py_pb2",
"//ortools/math_opt:model_update_py_pb2",
],
)
py_library(
name = "sparse_containers",
srcs = ["sparse_containers.py"],
deps = [
":model",
"//ortools/math_opt:sparse_containers_py_pb2",
],
)
py_library(
name = "solution",
srcs = ["solution.py"],
deps = [
":model",
":sparse_containers",
"//ortools/math_opt:solution_py_pb2",
],
)
py_library(
name = "result",
srcs = ["result.py"],
deps = [
":model",
":solution",
"//ortools/gscip:gscip_proto_py_pb2",
"//ortools/math_opt:result_py_pb2",
"//ortools/math_opt/solvers:osqp_py_pb2",
],
)
py_library(
name = "parameters",
srcs = ["parameters.py"],
deps = [
"//ortools/glop:parameters_py_pb2",
"//ortools/gscip:gscip_proto_py_pb2",
"//ortools/math_opt:parameters_py_pb2",
"//ortools/math_opt/solvers:glpk_py_pb2",
"//ortools/math_opt/solvers:gurobi_py_pb2",
"//ortools/math_opt/solvers:highs_py_pb2",
"//ortools/math_opt/solvers:osqp_py_pb2",
"//ortools/pdlp:solvers_py_pb2",
"//ortools/sat:sat_parameters_py_pb2",
],
)
py_library(
name = "model_parameters",
srcs = ["model_parameters.py"],
deps = [
":model",
":solution",
":sparse_containers",
"//ortools/math_opt:model_parameters_py_pb2",
],
)
py_library(
name = "callback",
srcs = ["callback.py"],
deps = [
":model",
":sparse_containers",
"//ortools/math_opt:callback_py_pb2",
],
)
py_library(
name = "compute_infeasible_subsystem_result",
srcs = ["compute_infeasible_subsystem_result.py"],
deps = [
":model",
":result",
requirement("immutabledict"),
"//ortools/math_opt:infeasible_subsystem_py_pb2",
],
)
py_library(
name = "solve",
srcs = ["solve.py"],
deps = [
":callback",
":compute_infeasible_subsystem_result",
":message_callback",
":model",
":model_parameters",
":parameters",
":result",
"//ortools/math_opt:parameters_py_pb2",
"//ortools/math_opt/core/python:solver",
],
)
py_library(
name = "message_callback",
srcs = ["message_callback.py"],
srcs_version = "PY3",
deps = [requirement("absl-py")],
)
py_library(
name = "statistics",
srcs = ["statistics.py"],
deps = [":model"],
)
py_library(
name = "normalize",
srcs = ["normalize.py"],
visibility = ["//ortools/math_opt/python:__subpackages__"],
deps = [
# "@com_google_protobuf//protobuf:duration_py_pb2",
"@com_google_protobuf//:protobuf_python",
],
)

View File

@@ -0,0 +1,346 @@
# Copyright 2010-2022 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.
"""Defines how to request a callback and the input and output of a callback."""
import dataclasses
import datetime
import enum
import math
from typing import Dict, List, Mapping, Optional, Set, Union
from ortools.math_opt import callback_pb2
from ortools.math_opt.python import model
from ortools.math_opt.python import sparse_containers
@enum.unique
class Event(enum.Enum):
"""The supported events during a solve for callbacks.
* UNSPECIFIED: The event is unknown (typically an internal error).
* PRESOLVE: The solver is currently running presolve. Gurobi only.
* SIMPLEX: The solver is currently running the simplex method. Gurobi only.
* MIP: The solver is in the MIP loop (called periodically before starting a
new node). Useful for early termination. Note that this event does not
provide information on LP relaxations nor about new incumbent solutions.
Gurobi only.
* MIP_SOLUTION: Called every time a new MIP incumbent is found. Fully
supported by Gurobi, partially supported by CP-SAT (you can observe new
solutions, but not add lazy constraints).
* MIP_NODE: Called inside a MIP node. Note that there is no guarantee that the
callback function will be called on every node. That behavior is
solver-dependent. Gurobi only.
Disabling cuts using SolveParameters may interfere with this event being
called and/or adding cuts at this event, the behavior is solver specific.
* BARRIER: Called in each iterate of an interior point/barrier method. Gurobi
only.
"""
UNSPECIFIED = callback_pb2.CALLBACK_EVENT_UNSPECIFIED
PRESOLVE = callback_pb2.CALLBACK_EVENT_PRESOLVE
SIMPLEX = callback_pb2.CALLBACK_EVENT_SIMPLEX
MIP = callback_pb2.CALLBACK_EVENT_MIP
MIP_SOLUTION = callback_pb2.CALLBACK_EVENT_MIP_SOLUTION
MIP_NODE = callback_pb2.CALLBACK_EVENT_MIP_NODE
BARRIER = callback_pb2.CALLBACK_EVENT_BARRIER
PresolveStats = callback_pb2.CallbackDataProto.PresolveStats
SimplexStats = callback_pb2.CallbackDataProto.SimplexStats
BarrierStats = callback_pb2.CallbackDataProto.BarrierStats
MipStats = callback_pb2.CallbackDataProto.MipStats
@dataclasses.dataclass
class CallbackData:
"""Input to the solve callback (produced by the solver).
Attributes:
event: The current state of the solver when the callback is run. The event
(partially) determines what data is available and what the user is allowed
to return.
solution: A solution to the primal optimization problem, if available. For
Event.MIP_SOLUTION, solution is always present, integral, and feasible.
For Event.MIP_NODE, the primal_solution contains the current LP-node
relaxation. In some cases, no solution will be available (e.g. because LP
was infeasible or the solve was imprecise). Empty for other events.
messages: Logs generated by the underlying solver, as a list of strings
without new lines (each string is a line). Only filled on Event.MESSAGE.
runtime: The time since Solve() was invoked.
presolve_stats: Filled for Event.PRESOLVE only.
simplex_stats: Filled for Event.SIMPLEX only.
barrier_stats: Filled for Event.BARRIER only.
mip_stats: Filled for the events MIP, MIP_SOLUTION and MIP_NODE only.
"""
event: Event = Event.UNSPECIFIED
solution: Optional[Dict[model.Variable, float]] = None
messages: List[str] = dataclasses.field(default_factory=list)
runtime: datetime.timedelta = datetime.timedelta()
presolve_stats: PresolveStats = dataclasses.field(default_factory=PresolveStats)
simplex_stats: SimplexStats = dataclasses.field(default_factory=SimplexStats)
barrier_stats: BarrierStats = dataclasses.field(default_factory=BarrierStats)
mip_stats: MipStats = dataclasses.field(default_factory=MipStats)
def parse_callback_data(
cb_data: callback_pb2.CallbackDataProto, mod: model.Model
) -> CallbackData:
"""Creates a CallbackData from an equivalent proto.
Args:
cb_data: A protocol buffer with the information the user needs for a
callback.
mod: The model being solved.
Returns:
An equivalent CallbackData.
Raises:
ValueError: if cb_data is invalid or inconsistent with mod, e.g. cb_data
refers to a variable id not in mod.
"""
result = CallbackData()
result.event = Event(cb_data.event)
if cb_data.HasField("primal_solution_vector"):
primal_solution = cb_data.primal_solution_vector
result.solution = {
mod.get_variable(id): val
for (id, val) in zip(primal_solution.ids, primal_solution.values)
}
result.runtime = cb_data.runtime.ToTimedelta()
result.presolve_stats = cb_data.presolve_stats
result.simplex_stats = cb_data.simplex_stats
result.barrier_stats = cb_data.barrier_stats
result.mip_stats = cb_data.mip_stats
return result
@dataclasses.dataclass
class CallbackRegistration:
"""Request the events and input data and reports output types for a callback.
Note that it is an error to add a constraint in a callback without setting
add_cuts and/or add_lazy_constraints to true.
Attributes:
events: When the callback should be invoked, by default, never. If an
unsupported event for a solver/model combination is selected, an
excecption is raised, see Event above for details.
mip_solution_filter: restricts the variable values returned in
CallbackData.solution (the callback argument) at each MIP_SOLUTION event.
By default, values are returned for all variables.
mip_node_filter: restricts the variable values returned in
CallbackData.solution (the callback argument) at each MIP_NODE event. By
default, values are returned for all variables.
add_cuts: The callback may add "user cuts" (linear constraints that
strengthen the LP without cutting of integer points) at MIP_NODE events.
add_lazy_constraints: The callback may add "lazy constraints" (linear
constraints that cut off integer solutions) at MIP_NODE or MIP_SOLUTION
events.
"""
events: Set[Event] = dataclasses.field(default_factory=set)
mip_solution_filter: sparse_containers.VariableFilter = (
sparse_containers.VariableFilter()
)
mip_node_filter: sparse_containers.VariableFilter = (
sparse_containers.VariableFilter()
)
add_cuts: bool = False
add_lazy_constraints: bool = False
def to_proto(self) -> callback_pb2.CallbackRegistrationProto:
"""Returns an equivalent proto to this CallbackRegistration."""
result = callback_pb2.CallbackRegistrationProto()
result.request_registration[:] = sorted([event.value for event in self.events])
result.mip_solution_filter.CopyFrom(self.mip_solution_filter.to_proto())
result.mip_node_filter.CopyFrom(self.mip_node_filter.to_proto())
result.add_cuts = self.add_cuts
result.add_lazy_constraints = self.add_lazy_constraints
return result
@dataclasses.dataclass
class GeneratedConstraint:
"""A linear constraint to add inside a callback.
Models a constraint of the form:
lb <= sum_{i in I} a_i * x_i <= ub
Two types of generated linear constraints are supported based on is_lazy:
* The "lazy constraint" can remove integer points from the feasible
region and can be added at event Event.MIP_NODE or
Event.MIP_SOLUTION
* The "user cut" (on is_lazy=false) strengthens the LP without removing
integer points. It can only be added at Event.MIP_NODE.
Attributes:
terms: The variables and linear coefficients in the constraint, a_i and x_i
in the model above.
lower_bound: lb in the model above.
upper_bound: ub in the model above.
is_lazy: Indicates if the constraint should be interpreted as a "lazy
constraint" (cuts off integer solutions) or a "user cut" (strengthens the
LP relaxation without cutting of integer solutions).
"""
terms: Mapping[model.Variable, float] = dataclasses.field(default_factory=dict)
lower_bound: float = -math.inf
upper_bound: float = math.inf
is_lazy: bool = False
def to_proto(
self,
) -> callback_pb2.CallbackResultProto.GeneratedLinearConstraint:
"""Returns an equivalent proto for the constraint."""
result = callback_pb2.CallbackResultProto.GeneratedLinearConstraint()
result.is_lazy = self.is_lazy
result.lower_bound = self.lower_bound
result.upper_bound = self.upper_bound
result.linear_expression.CopyFrom(
sparse_containers.to_sparse_double_vector_proto(self.terms)
)
return result
@dataclasses.dataclass
class CallbackResult:
"""The value returned by a solve callback (produced by the user).
Attributes:
terminate: Stop the solve process and return early. Can be called from any
event.
generated_constraints: Constraints to add to the model. For details, see
GeneratedConstraint documentation.
suggested_solutions: A list of solutions (or partially defined solutions) to
suggest to the solver. Some solvers (e.g. gurobi) will try and convert a
partial solution into a full solution by solving a MIP. Use only for
Event.MIP_NODE.
"""
terminate: bool = False
generated_constraints: List[GeneratedConstraint] = dataclasses.field(
default_factory=list
)
suggested_solutions: List[Mapping[model.Variable, float]] = dataclasses.field(
default_factory=list
)
def add_generated_constraint(
self,
bounded_expr: Optional[Union[bool, model.BoundedLinearTypes]] = None,
*,
lb: Optional[float] = None,
ub: Optional[float] = None,
expr: Optional[model.LinearTypes] = None,
is_lazy: bool,
) -> None:
"""Adds a linear constraint to the list of generated constraints.
The constraint can be of two exclusive types: a "lazy constraint" or a
"user cut. A "user cut" is a constraint that excludes the current LP
solution, but does not cut off any integer-feasible points that satisfy the
already added constraints (either in callbacks or through
Model.add_linear_constraint()). A "lazy constraint" is a constraint that
excludes such integer-feasible points and hence is needed for corrctness of
the forlumation.
The simplest way to specify the constraint is by passing a one-sided or
two-sided linear inequality as in:
* add_generated_constraint(x + y + 1.0 <= 2.0, is_lazy=True),
* add_generated_constraint(x + y >= 2.0, is_lazy=True), or
* add_generated_constraint((1.0 <= x + y) <= 2.0, is_lazy=True).
Note the extra parenthesis for two-sided linear inequalities, which is
required due to some language limitations (see
https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/).
If the parenthesis are omitted, a TypeError will be raised explaining the
issue (if this error was not raised the first inequality would have been
silently ignored because of the noted language limitations).
The second way to specify the constraint is by setting lb, ub, and/o expr as
in:
* add_generated_constraint(expr=x + y + 1.0, ub=2.0, is_lazy=True),
* add_generated_constraint(expr=x + y, lb=2.0, is_lazy=True),
* add_generated_constraint(expr=x + y, lb=1.0, ub=2.0, is_lazy=True), or
* add_generated_constraint(lb=1.0, is_lazy=True).
Omitting lb is equivalent to setting it to -math.inf and omiting ub is
equivalent to setting it to math.inf.
These two alternatives are exclusive and a combined call like:
* add_generated_constraint(x + y <= 2.0, lb=1.0, is_lazy=True), or
* add_generated_constraint(x + y <= 2.0, ub=math.inf, is_lazy=True)
will raise a ValueError. A ValueError is also raised if expr's offset is
infinite.
Args:
bounded_expr: a linear inequality describing the constraint. Cannot be
specified together with lb, ub, or expr.
lb: The constraint's lower bound if bounded_expr is omitted (if both
bounder_expr and lb are omitted, the lower bound is -math.inf).
ub: The constraint's upper bound if bounded_expr is omitted (if both
bounder_expr and ub are omitted, the upper bound is math.inf).
expr: The constraint's linear expression if bounded_expr is omitted.
is_lazy: Whether the constraint is lazy or not.
"""
normalized_inequality = model.as_normalized_linear_inequality(
bounded_expr, lb=lb, ub=ub, expr=expr
)
self.generated_constraints.append(
GeneratedConstraint(
lower_bound=normalized_inequality.lb,
terms=normalized_inequality.coefficients,
upper_bound=normalized_inequality.ub,
is_lazy=is_lazy,
)
)
def add_lazy_constraint(
self,
bounded_expr: Optional[Union[bool, model.BoundedLinearTypes]] = None,
*,
lb: Optional[float] = None,
ub: Optional[float] = None,
expr: Optional[model.LinearTypes] = None,
) -> None:
"""Shortcut for add_generated_constraint(..., is_lazy=True).."""
self.add_generated_constraint(
bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=True
)
def add_user_cut(
self,
bounded_expr: Optional[Union[bool, model.BoundedLinearTypes]] = None,
*,
lb: Optional[float] = None,
ub: Optional[float] = None,
expr: Optional[model.LinearTypes] = None,
) -> None:
"""Shortcut for add_generated_constraint(..., is_lazy=False)."""
self.add_generated_constraint(
bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=False
)
def to_proto(self) -> callback_pb2.CallbackResultProto:
"""Returns a proto equivalent to this CallbackResult."""
result = callback_pb2.CallbackResultProto(terminate=self.terminate)
for generated_constraint in self.generated_constraints:
result.cuts.add().CopyFrom(generated_constraint.to_proto())
for suggested_solution in self.suggested_solutions:
result.suggested_solutions.add().CopyFrom(
sparse_containers.to_sparse_double_vector_proto(suggested_solution)
)
return result

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
# Copyright 2010-2022 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.
import datetime
import math
import unittest
from ortools.math_opt import callback_pb2
from ortools.math_opt import sparse_containers_pb2
from ortools.math_opt.python import callback
from ortools.math_opt.python import model
from ortools.math_opt.python import sparse_containers
from ortools.math_opt.python.testing import compare_proto
class CallbackDataTest(compare_proto.MathOptProtoAssertions, unittest.TestCase):
def test_parse_callback_data_no_solution(self) -> None:
mod = model.Model(name="test_model")
cb_data_proto = callback_pb2.CallbackDataProto(
event=callback_pb2.CALLBACK_EVENT_PRESOLVE
)
cb_data_proto.runtime.FromTimedelta(datetime.timedelta(seconds=16.0))
cb_data_proto.presolve_stats.removed_variables = 10
cb_data_proto.simplex_stats.iteration_count = 3
cb_data_proto.barrier_stats.primal_objective = 2.0
cb_data_proto.mip_stats.open_nodes = 5
cb_data = callback.parse_callback_data(cb_data_proto, mod)
self.assertEqual(cb_data.event, callback.Event.PRESOLVE)
self.assertIsNone(cb_data.solution)
self.assertEqual(16.0, cb_data.runtime.seconds)
self.assert_protos_equiv(
cb_data.presolve_stats,
callback_pb2.CallbackDataProto.PresolveStats(removed_variables=10),
)
self.assert_protos_equiv(
cb_data.simplex_stats,
callback_pb2.CallbackDataProto.SimplexStats(iteration_count=3),
)
self.assert_protos_equiv(
cb_data.barrier_stats,
callback_pb2.CallbackDataProto.BarrierStats(primal_objective=2.0),
)
self.assert_protos_equiv(
cb_data.mip_stats, callback_pb2.CallbackDataProto.MipStats(open_nodes=5)
)
def test_parse_callback_data_with_solution(self) -> None:
mod = model.Model(name="test_model")
x = mod.add_binary_variable(name="x")
y = mod.add_binary_variable(name="y")
cb_data_proto = callback_pb2.CallbackDataProto(
event=callback_pb2.CALLBACK_EVENT_MIP_SOLUTION
)
solution = cb_data_proto.primal_solution_vector
solution.ids[:] = [0, 1]
solution.values[:] = [0.0, 1.0]
cb_data_proto.runtime.FromTimedelta(datetime.timedelta(seconds=12.0))
cb_data = callback.parse_callback_data(cb_data_proto, mod)
self.assertEqual(cb_data.event, callback.Event.MIP_SOLUTION)
self.assertDictEqual(cb_data.solution, {x: 0.0, y: 1.0})
self.assertListEqual(cb_data.messages, [])
self.assertEqual(12.0, cb_data.runtime.seconds)
self.assert_protos_equiv(
cb_data.presolve_stats, callback_pb2.CallbackDataProto.PresolveStats()
)
self.assert_protos_equiv(
cb_data.simplex_stats, callback_pb2.CallbackDataProto.SimplexStats()
)
self.assert_protos_equiv(
cb_data.barrier_stats, callback_pb2.CallbackDataProto.BarrierStats()
)
self.assert_protos_equiv(
cb_data.mip_stats, callback_pb2.CallbackDataProto.MipStats()
)
class CallbackRegistrationTest(compare_proto.MathOptProtoAssertions, unittest.TestCase):
def testToProto(self) -> None:
mod = model.Model(name="test_model")
x = mod.add_binary_variable(name="x")
mod.add_binary_variable(name="y")
z = mod.add_binary_variable(name="z")
reg = callback.CallbackRegistration()
reg.events = {callback.Event.MIP_SOLUTION, callback.Event.MIP_NODE}
reg.mip_node_filter = sparse_containers.VariableFilter(filtered_items=(z, x))
reg.mip_solution_filter = sparse_containers.VariableFilter(
skip_zero_values=True
)
reg.add_lazy_constraints = True
reg.add_cuts = False
self.assert_protos_equiv(
reg.to_proto(),
callback_pb2.CallbackRegistrationProto(
request_registration=[
callback_pb2.CALLBACK_EVENT_MIP_SOLUTION,
callback_pb2.CALLBACK_EVENT_MIP_NODE,
],
mip_node_filter=sparse_containers_pb2.SparseVectorFilterProto(
filter_by_ids=True, filtered_ids=[0, 2]
),
mip_solution_filter=sparse_containers_pb2.SparseVectorFilterProto(
skip_zero_values=True
),
add_lazy_constraints=True,
add_cuts=False,
),
)
class GeneratedLinearConstraintTest(
compare_proto.MathOptProtoAssertions, unittest.TestCase
):
def testToProto(self) -> None:
mod = model.Model(name="test_model")
x = mod.add_binary_variable(name="x")
mod.add_binary_variable(name="y")
z = mod.add_binary_variable(name="z")
gen_con = callback.GeneratedConstraint()
gen_con.terms = {x: 2.0, z: 4.0}
gen_con.upper_bound = 5.0
gen_con.is_lazy = True
self.assert_protos_equiv(
gen_con.to_proto(),
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
lower_bound=-math.inf,
upper_bound=5.0,
is_lazy=True,
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
ids=[0, 2], values=[2.0, 4.0]
),
),
)
class CallbackResultTest(compare_proto.MathOptProtoAssertions, unittest.TestCase):
def testToProto(self) -> None:
mod = model.Model(name="test_model")
x = mod.add_binary_variable(name="x")
y = mod.add_binary_variable(name="y")
z = mod.add_binary_variable(name="z")
result = callback.CallbackResult()
result.terminate = True
# Test le/ge combinations to avoid mutants.
result.add_lazy_constraint(2 * x <= 0)
result.add_lazy_constraint(2 * x >= 0)
result.add_user_cut(2 * z >= 2)
result.add_user_cut(2 * z <= 2)
result.add_generated_constraint(expr=2 * z, lb=2, is_lazy=False)
result.add_generated_constraint(expr=2 * z, ub=2, is_lazy=False)
result.suggested_solutions.append({x: 1.0, y: 0.0, z: 1.0})
result.suggested_solutions.append({x: 0.0, y: 0.0, z: 0.0})
expected = callback_pb2.CallbackResultProto(
terminate=True,
cuts=[
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
lower_bound=-math.inf,
upper_bound=0.0,
is_lazy=True,
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
ids=[0], values=[2.0]
),
),
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
lower_bound=0.0,
upper_bound=math.inf,
is_lazy=True,
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
ids=[0], values=[2.0]
),
),
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
lower_bound=2.0,
upper_bound=math.inf,
is_lazy=False,
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
ids=[2], values=[2.0]
),
),
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
lower_bound=-math.inf,
upper_bound=2.0,
is_lazy=False,
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
ids=[2], values=[2.0]
),
),
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
lower_bound=2.0,
upper_bound=math.inf,
is_lazy=False,
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
ids=[2], values=[2.0]
),
),
callback_pb2.CallbackResultProto.GeneratedLinearConstraint(
lower_bound=-math.inf,
upper_bound=2.0,
is_lazy=False,
linear_expression=sparse_containers_pb2.SparseDoubleVectorProto(
ids=[2], values=[2.0]
),
),
],
suggested_solutions=[
sparse_containers_pb2.SparseDoubleVectorProto(
ids=[0, 1, 2], values=[1.0, 0.0, 1.0]
),
sparse_containers_pb2.SparseDoubleVectorProto(
ids=[0, 1, 2], values=[0.0, 0.0, 0.0]
),
],
)
self.assert_protos_equiv(result.to_proto(), expected)
def testConstraintErrors(self) -> None:
mod = model.Model(name="test_model")
x = mod.add_binary_variable(name="x")
y = mod.add_binary_variable(name="y")
z = mod.add_binary_variable(name="z")
result = callback.CallbackResult()
with self.assertRaisesRegex(
TypeError,
"unsupported operand.*\n.*two or more non-constant linear expressions",
):
result.add_lazy_constraint(x <= (y <= z))
with self.assertRaisesRegex(ValueError, "lb cannot be specified.*"):
result.add_user_cut(x + y == 1, lb=1)
def testToProtoEmpty(self) -> None:
result = callback.CallbackResult()
self.assert_protos_equiv(result.to_proto(), callback_pb2.CallbackResultProto())
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,197 @@
# Copyright 2010-2022 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.
"""Data types for the result of calling `mathopt.compute_infeasible_subsystem."""
import dataclasses
from typing import Mapping
import immutabledict
from ortools.math_opt import infeasible_subsystem_pb2
from ortools.math_opt.python import model
from ortools.math_opt.python import result
@dataclasses.dataclass(frozen=True)
class ModelSubsetBounds:
"""Presence of the upper and lower bounds in a two-sided constraint.
E.g. for 1 <= x <= 2, `lower` is the constraint 1 <= x and `upper` is the
constraint x <= 2.
Attributes:
lower: If the lower bound half of the two-sided constraint is selected.
upper: If the upper bound half of the two-sided constraint is selected.
"""
lower: bool = False
upper: bool = False
def empty(self) -> bool:
"""Is empty if both `lower` and `upper` are False."""
return not (self.lower or self.upper)
def to_proto(self) -> infeasible_subsystem_pb2.ModelSubsetProto.Bounds:
"""Returns an equivalent proto message for these bounds."""
return infeasible_subsystem_pb2.ModelSubsetProto.Bounds(
lower=self.lower, upper=self.upper
)
def parse_model_subset_bounds(
bounds: infeasible_subsystem_pb2.ModelSubsetProto.Bounds,
) -> ModelSubsetBounds:
"""Returns an equivalent `ModelSubsetBounds` to the input proto."""
return ModelSubsetBounds(lower=bounds.lower, upper=bounds.upper)
@dataclasses.dataclass(frozen=True)
class ModelSubset:
"""A subset of a Model's constraints (including variable bounds/integrality).
When returned from `solve.compute_infeasible_subsystem`, the contained
`ModelSubsetBounds` will all be nonempty.
Attributes:
variable_bounds: The upper and/or lower bound constraints on these variables
are included in the subset.
variable_integrality: The constraint that a variable is integer is included
in the subset.
linear_constraints: The upper and/or lower bounds from these linear
constraints are included in the subset.
"""
variable_bounds: Mapping[
model.Variable, ModelSubsetBounds
] = immutabledict.immutabledict()
variable_integrality: frozenset[model.Variable] = frozenset()
linear_constraints: Mapping[
model.LinearConstraint, ModelSubsetBounds
] = immutabledict.immutabledict()
def empty(self) -> bool:
"""Returns true if all the nested constraint collections are empty.
Warning: When `self.variable_bounds` or `self.linear_constraints` contain
only ModelSubsetBounds which are themselves empty, this function will return
False.
Returns:
True if this is empty.
"""
return not (
self.variable_bounds or self.variable_integrality or self.linear_constraints
)
def to_proto(self) -> infeasible_subsystem_pb2.ModelSubsetProto:
"""Returns an equivalent proto message for this `ModelSubset`."""
return infeasible_subsystem_pb2.ModelSubsetProto(
variable_bounds={
var.id: bounds.to_proto()
for (var, bounds) in self.variable_bounds.items()
},
variable_integrality=sorted(var.id for var in self.variable_integrality),
linear_constraints={
con.id: bounds.to_proto()
for (con, bounds) in self.linear_constraints.items()
},
)
def parse_model_subset(
model_subset: infeasible_subsystem_pb2.ModelSubsetProto, mod: model.Model
) -> ModelSubset:
"""Returns an equivalent `ModelSubset` to the input proto."""
if model_subset.quadratic_constraints:
raise NotImplementedError(
"quadratic_constraints not yet implemented for ModelSubset in Python"
)
if model_subset.second_order_cone_constraints:
raise NotImplementedError(
"second_order_cone_constraints not yet implemented for ModelSubset in"
" Python"
)
if model_subset.sos1_constraints:
raise NotImplementedError(
"sos1_constraints not yet implemented for ModelSubset in Python"
)
if model_subset.sos2_constraints:
raise NotImplementedError(
"sos2_constraints not yet implemented for ModelSubset in Python"
)
if model_subset.indicator_constraints:
raise NotImplementedError(
"indicator_constraints not yet implemented for ModelSubset in Python"
)
return ModelSubset(
variable_bounds={
mod.get_variable(var_id): parse_model_subset_bounds(bounds)
for var_id, bounds in model_subset.variable_bounds.items()
},
variable_integrality=frozenset(
mod.get_variable(var_id) for var_id in model_subset.variable_integrality
),
linear_constraints={
mod.get_linear_constraint(con_id): parse_model_subset_bounds(bounds)
for con_id, bounds in model_subset.linear_constraints.items()
},
)
@dataclasses.dataclass(frozen=True)
class ComputeInfeasibleSubsystemResult:
"""The result of searching for an infeasible subsystem.
This is the result of calling `mathopt.compute_infeasible_subsystem()`.
Attributes:
feasibility: If the problem was proven feasible, infeasible, or no
conclusion was reached. The fields below are ignored unless the problem
was proven infeasible.
infeasible_subsystem: Ignored unless `feasibility` is `INFEASIBLE`, a subset
of the model that is still infeasible.
is_minimal: Ignored unless `feasibility` is `INFEASIBLE`. If True, then the
removal of any constraint from `infeasible_subsystem` makes the sub-model
feasible. Note that, due to problem transformations MathOpt applies or
idiosyncrasies of the solvers contract, the returned infeasible subsystem
may not actually be minimal.
"""
feasibility: result.FeasibilityStatus = result.FeasibilityStatus.UNDETERMINED
infeasible_subsystem: ModelSubset = ModelSubset()
is_minimal: bool = False
def to_proto(
self,
) -> infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto:
"""Returns an equivalent proto for this `ComputeInfeasibleSubsystemResult`."""
return infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto(
feasibility=self.feasibility.value,
infeasible_subsystem=self.infeasible_subsystem.to_proto(),
is_minimal=self.is_minimal,
)
def parse_compute_infeasible_subsystem_result(
infeasible_system_result: infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto,
mod: model.Model,
) -> ComputeInfeasibleSubsystemResult:
"""Returns an equivalent `ComputeInfeasibleSubsystemResult` to the input proto."""
return ComputeInfeasibleSubsystemResult(
feasibility=result.FeasibilityStatus(infeasible_system_result.feasibility),
infeasible_subsystem=parse_model_subset(
infeasible_system_result.infeasible_subsystem, mod
),
is_minimal=infeasible_system_result.is_minimal,
)

View File

@@ -0,0 +1,200 @@
#!/usr/bin/env python3
# Copyright 2010-2022 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.
"""Tests for compute_infeasible_subsystem_result.py."""
import unittest
from ortools.math_opt import infeasible_subsystem_pb2
from ortools.math_opt.python import compute_infeasible_subsystem_result
from ortools.math_opt.python import model
from ortools.math_opt.python import result
from ortools.math_opt.python.testing import compare_proto
_ModelSubsetBounds = compute_infeasible_subsystem_result.ModelSubsetBounds
_ModelSubset = compute_infeasible_subsystem_result.ModelSubset
_ComputeInfeasibleSubsystemResult = (
compute_infeasible_subsystem_result.ComputeInfeasibleSubsystemResult
)
class ModelSubsetBoundsTest(unittest.TestCase, compare_proto.MathOptProtoAssertions):
def test_empty(self) -> None:
self.assertTrue(_ModelSubsetBounds().empty())
self.assertFalse(_ModelSubsetBounds(lower=True).empty())
self.assertFalse(_ModelSubsetBounds(upper=True).empty())
def test_proto(self) -> None:
start_bounds = _ModelSubsetBounds(lower=True)
self.assert_protos_equiv(
start_bounds.to_proto(),
infeasible_subsystem_pb2.ModelSubsetProto.Bounds(lower=True),
)
def test_proto_round_trip_lower(self) -> None:
start_bounds = _ModelSubsetBounds(lower=True)
self.assertEqual(
compute_infeasible_subsystem_result.parse_model_subset_bounds(
start_bounds.to_proto()
),
start_bounds,
)
def test_proto_round_trip_upper(self) -> None:
start_bounds = _ModelSubsetBounds(upper=True)
self.assertEqual(
compute_infeasible_subsystem_result.parse_model_subset_bounds(
start_bounds.to_proto()
),
start_bounds,
)
class ModelSubsetTest(unittest.TestCase, compare_proto.MathOptProtoAssertions):
def test_empty(self) -> None:
m = model.Model()
x = m.add_binary_variable()
c = m.add_linear_constraint()
self.assertTrue(_ModelSubset().empty())
self.assertFalse(_ModelSubset(variable_integrality=frozenset((x,))).empty())
self.assertFalse(
_ModelSubset(variable_bounds={x: _ModelSubsetBounds(lower=True)}).empty()
)
self.assertFalse(
_ModelSubset(linear_constraints={c: _ModelSubsetBounds(upper=True)}).empty()
)
def test_to_proto(self) -> None:
m = model.Model()
x = m.add_binary_variable()
y = m.add_binary_variable()
c = m.add_linear_constraint()
d = m.add_linear_constraint()
model_subset = _ModelSubset(
variable_integrality=frozenset((x, y)),
variable_bounds={y: _ModelSubsetBounds(upper=True)},
linear_constraints={
c: _ModelSubsetBounds(upper=True),
d: _ModelSubsetBounds(lower=True),
},
)
expected = infeasible_subsystem_pb2.ModelSubsetProto()
expected.variable_bounds[1].upper = True
expected.variable_integrality[:] = [0, 1]
expected.linear_constraints[0].upper = True
expected.linear_constraints[1].lower = True
self.assert_protos_equiv(model_subset.to_proto(), expected)
def test_proto_round_trip_empty(self) -> None:
m = model.Model()
subset = _ModelSubset()
self.assertEqual(
compute_infeasible_subsystem_result.parse_model_subset(
subset.to_proto(), m
),
subset,
)
def test_proto_round_trip_full(self) -> None:
m = model.Model()
x = m.add_binary_variable()
y = m.add_binary_variable()
c = m.add_linear_constraint()
d = m.add_linear_constraint()
start_subset = _ModelSubset(
variable_integrality=frozenset((x,)),
variable_bounds={
x: _ModelSubsetBounds(lower=True),
y: _ModelSubsetBounds(upper=True),
},
linear_constraints={
c: _ModelSubsetBounds(upper=True),
d: _ModelSubsetBounds(lower=True),
},
)
self.assertEqual(
compute_infeasible_subsystem_result.parse_model_subset(
start_subset.to_proto(), m
),
start_subset,
)
def test_parse_proto_quadratic_constraint_unsupported(self) -> None:
m = model.Model()
model_subset = infeasible_subsystem_pb2.ModelSubsetProto()
model_subset.quadratic_constraints[3].lower = True
with self.assertRaisesRegex(NotImplementedError, "quadratic_constraints"):
compute_infeasible_subsystem_result.parse_model_subset(model_subset, m)
def test_parse_proto_second_order_cone_unsupported(self) -> None:
m = model.Model()
model_subset = infeasible_subsystem_pb2.ModelSubsetProto(
second_order_cone_constraints=[2]
)
with self.assertRaisesRegex(
NotImplementedError, "second_order_cone_constraints"
):
compute_infeasible_subsystem_result.parse_model_subset(model_subset, m)
def test_parse_proto_sos1_unsupported(self) -> None:
m = model.Model()
model_subset = infeasible_subsystem_pb2.ModelSubsetProto(sos1_constraints=[2])
with self.assertRaisesRegex(NotImplementedError, "sos1_constraints"):
compute_infeasible_subsystem_result.parse_model_subset(model_subset, m)
def test_parse_proto_sos2_unsupported(self) -> None:
m = model.Model()
model_subset = infeasible_subsystem_pb2.ModelSubsetProto(sos2_constraints=[2])
with self.assertRaisesRegex(NotImplementedError, "sos2_constraints"):
compute_infeasible_subsystem_result.parse_model_subset(model_subset, m)
def test_parse_proto_indicator_unsupported(self) -> None:
m = model.Model()
model_subset = infeasible_subsystem_pb2.ModelSubsetProto(
indicator_constraints=[2]
)
with self.assertRaisesRegex(NotImplementedError, "indicator_constraints"):
compute_infeasible_subsystem_result.parse_model_subset(model_subset, m)
class ComputeInfeasibleSubsystemResultTest(unittest.TestCase):
def test_to_proto_round_trip(self) -> None:
m = model.Model()
x = m.add_binary_variable()
iis_result = _ComputeInfeasibleSubsystemResult(
feasibility=result.FeasibilityStatus.INFEASIBLE,
is_minimal=True,
infeasible_subsystem=_ModelSubset(variable_integrality=frozenset((x,))),
)
self.assertEqual(
compute_infeasible_subsystem_result.parse_compute_infeasible_subsystem_result(
iis_result.to_proto(), m
),
iis_result,
)
def test_to_proto_round_trip_empty(self) -> None:
m = model.Model()
iis_result = _ComputeInfeasibleSubsystemResult(
feasibility=result.FeasibilityStatus.UNDETERMINED
)
self.assertEqual(
compute_infeasible_subsystem_result.parse_compute_infeasible_subsystem_result(
iis_result.to_proto(), m
),
iis_result,
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,843 @@
# Copyright 2010-2022 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.
"""A minimal pure python implementation of model_storage.ModelStorage."""
from typing import Dict, Iterable, Iterator, Optional, Set, Tuple
import weakref
from ortools.math_opt import model_pb2
from ortools.math_opt import model_update_pb2
from ortools.math_opt import sparse_containers_pb2
from ortools.math_opt.python import model_storage
_QuadraticKey = model_storage.QuadraticTermIdKey
class _UpdateTracker(model_storage.StorageUpdateTracker):
"""Tracks model updates for HashModelStorage."""
def __init__(self, mod: "HashModelStorage"):
self.retired: bool = False
self.model: "HashModelStorage" = mod
# Changes for variables with id < variables_checkpoint are explicitly
# tracked.
self.variables_checkpoint: int = self.model._next_var_id
# Changes for linear constraints with id < linear_constraints_checkpoint
# are explicitly tracked.
self.linear_constraints_checkpoint: int = self.model._next_lin_con_id
self.objective_direction: bool = False
self.objective_offset: bool = False
self.variable_deletes: Set[int] = set()
self.variable_lbs: Set[int] = set()
self.variable_ubs: Set[int] = set()
self.variable_integers: Set[int] = set()
self.linear_objective_coefficients: Set[int] = set()
self.quadratic_objective_coefficients: Set[_QuadraticKey] = set()
self.linear_constraint_deletes: Set[int] = set()
self.linear_constraint_lbs: Set[int] = set()
self.linear_constraint_ubs: Set[int] = set()
self.linear_constraint_matrix: Set[Tuple[int, int]] = set()
def export_update(self) -> Optional[model_update_pb2.ModelUpdateProto]:
if self.retired:
raise model_storage.UsedUpdateTrackerAfterRemovalError()
if (
self.variables_checkpoint == self.model.next_variable_id()
and (
self.linear_constraints_checkpoint
== self.model.next_linear_constraint_id()
)
and not self.objective_direction
and not self.objective_offset
and not self.variable_deletes
and not self.variable_lbs
and not self.variable_ubs
and not self.variable_integers
and not self.linear_objective_coefficients
and not self.quadratic_objective_coefficients
and not self.linear_constraint_deletes
and not self.linear_constraint_lbs
and not self.linear_constraint_ubs
and not self.linear_constraint_matrix
):
return None
result = model_update_pb2.ModelUpdateProto()
result.deleted_variable_ids[:] = sorted(self.variable_deletes)
result.deleted_linear_constraint_ids[:] = sorted(self.linear_constraint_deletes)
# Variable updates
_set_sparse_double_vector(
sorted((vid, self.model.get_variable_lb(vid)) for vid in self.variable_lbs),
result.variable_updates.lower_bounds,
)
_set_sparse_double_vector(
sorted((vid, self.model.get_variable_ub(vid)) for vid in self.variable_ubs),
result.variable_updates.upper_bounds,
)
_set_sparse_bool_vector(
sorted(
(vid, self.model.get_variable_is_integer(vid))
for vid in self.variable_integers
),
result.variable_updates.integers,
)
# Linear constraint updates
_set_sparse_double_vector(
sorted(
(cid, self.model.get_linear_constraint_lb(cid))
for cid in self.linear_constraint_lbs
),
result.linear_constraint_updates.lower_bounds,
)
_set_sparse_double_vector(
sorted(
(cid, self.model.get_linear_constraint_ub(cid))
for cid in self.linear_constraint_ubs
),
result.linear_constraint_updates.upper_bounds,
)
# New variables and constraints
new_vars = []
for vid in range(self.variables_checkpoint, self.model.next_variable_id()):
var = self.model.variables.get(vid)
if var is not None:
new_vars.append((vid, var))
_variables_to_proto(new_vars, result.new_variables)
new_lin_cons = []
for lin_con_id in range(
self.linear_constraints_checkpoint,
self.model.next_linear_constraint_id(),
):
lin_con = self.model.linear_constraints.get(lin_con_id)
if lin_con is not None:
new_lin_cons.append((lin_con_id, lin_con))
_linear_constraints_to_proto(new_lin_cons, result.new_linear_constraints)
# Objective update
if self.objective_direction:
result.objective_updates.direction_update = self.model.get_is_maximize()
if self.objective_offset:
result.objective_updates.offset_update = self.model.get_objective_offset()
_set_sparse_double_vector(
sorted(
(var, self.model.get_linear_objective_coefficient(var))
for var in self.linear_objective_coefficients
),
result.objective_updates.linear_coefficients,
)
for new_var in range(self.variables_checkpoint, self.model.next_variable_id()):
# NOTE: the value will be 0.0 if either the coefficient is not set or the
# variable has been deleted. Calling
# model.get_linear_objective_coefficient() throws an exception if the
# variable has been deleted.
obj_coef = self.model.linear_objective_coefficient.get(new_var, 0.0)
if obj_coef:
result.objective_updates.linear_coefficients.ids.append(new_var)
result.objective_updates.linear_coefficients.values.append(obj_coef)
quadratic_objective_updates = [
(
key.id1,
key.id2,
self.model.get_quadratic_objective_coefficient(key.id1, key.id2),
)
for key in self.quadratic_objective_coefficients
]
for new_var in range(self.variables_checkpoint, self.model.next_variable_id()):
if self.model.variable_exists(new_var):
for other_var in self.model.get_quadratic_objective_adjacent_variables(
new_var
):
key = _QuadraticKey(new_var, other_var)
if new_var >= other_var:
key = _QuadraticKey(new_var, other_var)
quadratic_objective_updates.append(
(
key.id1,
key.id2,
self.model.get_quadratic_objective_coefficient(
key.id1, key.id2
),
)
)
quadratic_objective_updates.sort()
if quadratic_objective_updates:
first_var_ids, second_var_ids, coefficients = zip(
*quadratic_objective_updates
)
result.objective_updates.quadratic_coefficients.row_ids[:] = first_var_ids
result.objective_updates.quadratic_coefficients.column_ids[
:
] = second_var_ids
result.objective_updates.quadratic_coefficients.coefficients[
:
] = coefficients
# Linear constraint matrix updates
matrix_updates = [
(l, v, self.model.get_linear_constraint_coefficient(l, v))
for (l, v) in self.linear_constraint_matrix
]
for new_var in range(self.variables_checkpoint, self.model.next_variable_id()):
if self.model.variable_exists(new_var):
for lin_con in self.model.get_linear_constraints_with_variable(new_var):
matrix_updates.append(
(
lin_con,
new_var,
self.model.get_linear_constraint_coefficient(
lin_con, new_var
),
)
)
for new_lin_con in range(
self.linear_constraints_checkpoint,
self.model.next_linear_constraint_id(),
):
if self.model.linear_constraint_exists(new_lin_con):
for var in self.model.get_variables_for_linear_constraint(new_lin_con):
# We have already gotten the new variables above. Note that we do at
# most twice as much work as we should from this.
if var < self.variables_checkpoint:
matrix_updates.append(
(
new_lin_con,
var,
self.model.get_linear_constraint_coefficient(
new_lin_con, var
),
)
)
matrix_updates.sort()
if matrix_updates:
lin_cons, variables, coefs = zip(*matrix_updates)
result.linear_constraint_matrix_updates.row_ids[:] = lin_cons
result.linear_constraint_matrix_updates.column_ids[:] = variables
result.linear_constraint_matrix_updates.coefficients[:] = coefs
return result
def advance_checkpoint(self) -> None:
if self.retired:
raise model_storage.UsedUpdateTrackerAfterRemovalError()
self.objective_direction = False
self.objective_offset = False
self.variable_deletes = set()
self.variable_lbs = set()
self.variable_ubs = set()
self.variable_integers = set()
self.linear_objective_coefficients = set()
self.linear_constraint_deletes = set()
self.linear_constraint_lbs = set()
self.linear_constraint_ubs = set()
self.linear_constraint_matrix = set()
self.variables_checkpoint = self.model.next_variable_id()
self.linear_constraints_checkpoint = self.model.next_linear_constraint_id()
class _VariableStorage:
"""Data specific to each decision variable in the optimization problem."""
def __init__(self, lb: float, ub: float, is_integer: bool, name: str) -> None:
self.lower_bound: float = lb
self.upper_bound: float = ub
self.is_integer: bool = is_integer
self.name: str = name
self.linear_constraint_nonzeros: Set[int] = set()
class _LinearConstraintStorage:
"""Data specific to each linear constraint in the optimization problem."""
def __init__(self, lb: float, ub: float, name: str) -> None:
self.lower_bound: float = lb
self.upper_bound: float = ub
self.name: str = name
self.variable_nonzeros: Set[int] = set()
class _QuadraticTermStorage:
"""Data describing quadratic terms with non-zero coefficients."""
def __init__(self) -> None:
self._coefficients: Dict[_QuadraticKey, float] = {}
# For a variable i that does not appear in a quadratic objective term with
# a non-zero coefficient, we may have self._adjacent_variable[i] being an
# empty set or i not appearing in self._adjacent_variable.keys() (e.g.
# depeding on whether the variable previously appeared in a quadratic term).
self._adjacent_variables: Dict[int, Set[int]] = {}
def __bool__(self) -> bool:
"""Returns true if and only if there are any quadratic terms with non-zero coefficients."""
return bool(self._coefficients)
def get_adjacent_variables(self, variable_id: int) -> Iterator[int]:
"""Yields the variables multiplying a variable in the stored quadratic terms.
If variable_id is not in the model the function yields the empty set.
Args:
variable_id: Function yields the variables multiplying variable_id in the
stored quadratic terms.
Yields:
The variables multiplying variable_id in the stored quadratic terms.
"""
yield from self._adjacent_variables.get(variable_id, ())
def keys(self) -> Iterator[_QuadraticKey]:
"""Yields the variable-pair keys associated to the stored quadratic terms."""
yield from self._coefficients.keys()
def coefficients(self) -> Iterator[model_storage.QuadraticEntry]:
"""Yields the stored quadratic terms as QuadraticEntry."""
for key, coef in self._coefficients.items():
yield model_storage.QuadraticEntry(id_key=key, coefficient=coef)
def delete_variable(self, variable_id: int) -> None:
"""Updates the data structure to consider variable_id as deleted."""
if variable_id not in self._adjacent_variables.keys():
return
for adjacent_variable_id in self._adjacent_variables[variable_id]:
if variable_id != adjacent_variable_id:
self._adjacent_variables[adjacent_variable_id].remove(variable_id)
del self._coefficients[_QuadraticKey(variable_id, adjacent_variable_id)]
self._adjacent_variables[variable_id].clear()
def clear(self) -> None:
"""Clears the data structure."""
self._coefficients.clear()
self._adjacent_variables.clear()
def set_coefficient(
self, first_variable_id: int, second_variable_id: int, value: float
) -> bool:
"""Sets the coefficient for the quadratic term associated to the product between two variables.
The ordering of the input variables does not matter.
Args:
first_variable_id: The first variable in the product.
second_variable_id: The second variable in the product.
value: The value of the coefficient.
Returns:
True if the coefficient is updated, False otherwise.
"""
key = _QuadraticKey(first_variable_id, second_variable_id)
if value == self._coefficients.get(key, 0.0):
return False
if value == 0.0:
# Assuming self._coefficients/_adjacent_variables are filled according
# to get_coefficient(key) != 0.0.
del self._coefficients[key]
self._adjacent_variables[first_variable_id].remove(second_variable_id)
if first_variable_id != second_variable_id:
self._adjacent_variables[second_variable_id].remove(first_variable_id)
else:
if first_variable_id not in self._adjacent_variables.keys():
self._adjacent_variables[first_variable_id] = set()
if second_variable_id not in self._adjacent_variables.keys():
self._adjacent_variables[second_variable_id] = set()
self._coefficients[key] = value
self._adjacent_variables[first_variable_id].add(second_variable_id)
self._adjacent_variables[second_variable_id].add(first_variable_id)
return True
def get_coefficient(self, first_variable_id: int, second_variable_id: int) -> float:
"""Gets the objective coefficient for the quadratic term associated to the product between two variables.
The ordering of the input variables does not matter.
Args:
first_variable_id: The first variable in the product.
second_variable_id: The second variable in the product.
Returns:
The value of the coefficient.
"""
return self._coefficients.get(
_QuadraticKey(first_variable_id, second_variable_id), 0.0
)
class HashModelStorage(model_storage.ModelStorage):
"""A simple, pure python implementation of ModelStorage.
Attributes:
_linear_constraint_matrix: A dictionary with (linear_constraint_id,
variable_id) keys and numeric values, representing the matrix A for the
constraints lb_c <= A*x <= ub_c. Invariant: the values have no zeros.
linear_objective_coefficient: A dictionary with variable_id keys and
numeric values, representing the linear terms in the objective.
Invariant: the values have no zeros.
_quadratic_objective_coefficients: A data structure containing quadratic
terms in the objective.
"""
def __init__(self, name: str = "") -> None:
super().__init__()
self._name: str = name
self.variables: Dict[int, _VariableStorage] = {}
self.linear_constraints: Dict[int, _LinearConstraintStorage] = {}
self._linear_constraint_matrix: Dict[Tuple[int, int], float] = {} #
self._is_maximize: bool = False
self._objective_offset: float = 0.0
self.linear_objective_coefficient: Dict[int, float] = {}
self._quadratic_objective_coefficients: _QuadraticTermStorage = (
_QuadraticTermStorage()
)
self._next_var_id: int = 0
self._next_lin_con_id: int = 0
self._update_trackers: weakref.WeakSet[_UpdateTracker] = weakref.WeakSet()
@property
def name(self) -> str:
return self._name
def add_variable(self, lb: float, ub: float, is_integer: bool, name: str) -> int:
var_id = self._next_var_id
self._next_var_id += 1
self.variables[var_id] = _VariableStorage(lb, ub, is_integer, name)
return var_id
def delete_variable(self, variable_id: int) -> None:
self._check_variable_id(variable_id)
variable = self.variables[variable_id]
# First update the watchers
for watcher in self._update_trackers:
if variable_id < watcher.variables_checkpoint:
watcher.variable_deletes.add(variable_id)
watcher.variable_lbs.discard(variable_id)
watcher.variable_ubs.discard(variable_id)
watcher.variable_integers.discard(variable_id)
watcher.linear_objective_coefficients.discard(variable_id)
for (
other_variable_id
) in self._quadratic_objective_coefficients.get_adjacent_variables(
variable_id
):
key = _QuadraticKey(variable_id, other_variable_id)
watcher.quadratic_objective_coefficients.discard(key)
for lin_con_id in variable.linear_constraint_nonzeros:
if lin_con_id < watcher.linear_constraints_checkpoint:
watcher.linear_constraint_matrix.discard(
(lin_con_id, variable_id)
)
# Then update self.
for lin_con_id in variable.linear_constraint_nonzeros:
self.linear_constraints[lin_con_id].variable_nonzeros.remove(variable_id)
del self._linear_constraint_matrix[(lin_con_id, variable_id)]
del self.variables[variable_id]
self.linear_objective_coefficient.pop(variable_id, None)
self._quadratic_objective_coefficients.delete_variable(variable_id)
def variable_exists(self, variable_id: int) -> bool:
return variable_id in self.variables
def next_variable_id(self) -> int:
return self._next_var_id
def set_variable_lb(self, variable_id: int, lb: float) -> None:
self._check_variable_id(variable_id)
if lb == self.variables[variable_id].lower_bound:
return
self.variables[variable_id].lower_bound = lb
for watcher in self._update_trackers:
if variable_id < watcher.variables_checkpoint:
watcher.variable_lbs.add(variable_id)
def set_variable_ub(self, variable_id: int, ub: float) -> None:
self._check_variable_id(variable_id)
if ub == self.variables[variable_id].upper_bound:
return
self.variables[variable_id].upper_bound = ub
for watcher in self._update_trackers:
if variable_id < watcher.variables_checkpoint:
watcher.variable_ubs.add(variable_id)
def set_variable_is_integer(self, variable_id: int, is_integer: bool) -> None:
self._check_variable_id(variable_id)
if is_integer == self.variables[variable_id].is_integer:
return
self.variables[variable_id].is_integer = is_integer
for watcher in self._update_trackers:
if variable_id < watcher.variables_checkpoint:
watcher.variable_integers.add(variable_id)
def get_variable_lb(self, variable_id: int) -> float:
self._check_variable_id(variable_id)
return self.variables[variable_id].lower_bound
def get_variable_ub(self, variable_id: int) -> float:
self._check_variable_id(variable_id)
return self.variables[variable_id].upper_bound
def get_variable_is_integer(self, variable_id: int) -> bool:
self._check_variable_id(variable_id)
return self.variables[variable_id].is_integer
def get_variable_name(self, variable_id: int) -> str:
self._check_variable_id(variable_id)
return self.variables[variable_id].name
def get_variables(self) -> Iterator[int]:
yield from self.variables.keys()
def add_linear_constraint(self, lb: float, ub: float, name: str) -> int:
lin_con_id = self._next_lin_con_id
self._next_lin_con_id += 1
self.linear_constraints[lin_con_id] = _LinearConstraintStorage(lb, ub, name)
return lin_con_id
def delete_linear_constraint(self, linear_constraint_id: int) -> None:
self._check_linear_constraint_id(linear_constraint_id)
con = self.linear_constraints[linear_constraint_id]
# First update the watchers
for watcher in self._update_trackers:
if linear_constraint_id < watcher.linear_constraints_checkpoint:
watcher.linear_constraint_deletes.add(linear_constraint_id)
watcher.linear_constraint_lbs.discard(linear_constraint_id)
watcher.linear_constraint_ubs.discard(linear_constraint_id)
for var_id in con.variable_nonzeros:
if var_id < watcher.variables_checkpoint:
watcher.linear_constraint_matrix.discard(
(linear_constraint_id, var_id)
)
# Then update self.
for var_id in con.variable_nonzeros:
self.variables[var_id].linear_constraint_nonzeros.remove(
linear_constraint_id
)
del self._linear_constraint_matrix[(linear_constraint_id, var_id)]
del self.linear_constraints[linear_constraint_id]
def linear_constraint_exists(self, linear_constraint_id: int) -> bool:
return linear_constraint_id in self.linear_constraints
def next_linear_constraint_id(self) -> int:
return self._next_lin_con_id
def set_linear_constraint_lb(self, linear_constraint_id: int, lb: float) -> None:
self._check_linear_constraint_id(linear_constraint_id)
if lb == self.linear_constraints[linear_constraint_id].lower_bound:
return
self.linear_constraints[linear_constraint_id].lower_bound = lb
for watcher in self._update_trackers:
if linear_constraint_id < watcher.linear_constraints_checkpoint:
watcher.linear_constraint_lbs.add(linear_constraint_id)
def set_linear_constraint_ub(self, linear_constraint_id: int, ub: float) -> None:
self._check_linear_constraint_id(linear_constraint_id)
if ub == self.linear_constraints[linear_constraint_id].upper_bound:
return
self.linear_constraints[linear_constraint_id].upper_bound = ub
for watcher in self._update_trackers:
if linear_constraint_id < watcher.linear_constraints_checkpoint:
watcher.linear_constraint_ubs.add(linear_constraint_id)
def get_linear_constraint_lb(self, linear_constraint_id: int) -> float:
self._check_linear_constraint_id(linear_constraint_id)
return self.linear_constraints[linear_constraint_id].lower_bound
def get_linear_constraint_ub(self, linear_constraint_id: int) -> float:
self._check_linear_constraint_id(linear_constraint_id)
return self.linear_constraints[linear_constraint_id].upper_bound
def get_linear_constraint_name(self, linear_constraint_id: int) -> str:
self._check_linear_constraint_id(linear_constraint_id)
return self.linear_constraints[linear_constraint_id].name
def get_linear_constraints(self) -> Iterator[int]:
yield from self.linear_constraints.keys()
def set_linear_constraint_coefficient(
self, linear_constraint_id: int, variable_id: int, value: float
) -> None:
self._check_linear_constraint_id(linear_constraint_id)
self._check_variable_id(variable_id)
if value == self._linear_constraint_matrix.get(
(linear_constraint_id, variable_id), 0.0
):
return
if value == 0.0:
self._linear_constraint_matrix.pop(
(linear_constraint_id, variable_id), None
)
self.variables[variable_id].linear_constraint_nonzeros.discard(
linear_constraint_id
)
self.linear_constraints[linear_constraint_id].variable_nonzeros.discard(
variable_id
)
else:
self._linear_constraint_matrix[(linear_constraint_id, variable_id)] = value
self.variables[variable_id].linear_constraint_nonzeros.add(
linear_constraint_id
)
self.linear_constraints[linear_constraint_id].variable_nonzeros.add(
variable_id
)
for watcher in self._update_trackers:
if (
variable_id < watcher.variables_checkpoint
and linear_constraint_id < watcher.linear_constraints_checkpoint
):
watcher.linear_constraint_matrix.add(
(linear_constraint_id, variable_id)
)
def get_linear_constraint_coefficient(
self, linear_constraint_id: int, variable_id: int
) -> float:
self._check_linear_constraint_id(linear_constraint_id)
self._check_variable_id(variable_id)
return self._linear_constraint_matrix.get(
(linear_constraint_id, variable_id), 0.0
)
def get_linear_constraints_with_variable(self, variable_id: int) -> Iterator[int]:
self._check_variable_id(variable_id)
yield from self.variables[variable_id].linear_constraint_nonzeros
def get_variables_for_linear_constraint(
self, linear_constraint_id: int
) -> Iterator[int]:
self._check_linear_constraint_id(linear_constraint_id)
yield from self.linear_constraints[linear_constraint_id].variable_nonzeros
def get_linear_constraint_matrix_entries(
self,
) -> Iterator[model_storage.LinearConstraintMatrixIdEntry]:
for (constraint, variable), coef in self._linear_constraint_matrix.items():
yield model_storage.LinearConstraintMatrixIdEntry(
linear_constraint_id=constraint,
variable_id=variable,
coefficient=coef,
)
def clear_objective(self) -> None:
for variable_id in self.linear_objective_coefficient:
for watcher in self._update_trackers:
if variable_id < watcher.variables_checkpoint:
watcher.linear_objective_coefficients.add(variable_id)
self.linear_objective_coefficient.clear()
for key in self._quadratic_objective_coefficients.keys():
for watcher in self._update_trackers:
if key.id2 < watcher.variables_checkpoint:
watcher.quadratic_objective_coefficients.add(key)
self._quadratic_objective_coefficients.clear()
self.set_objective_offset(0.0)
def set_linear_objective_coefficient(self, variable_id: int, value: float) -> None:
self._check_variable_id(variable_id)
if value == self.linear_objective_coefficient.get(variable_id, 0.0):
return
if value == 0.0:
self.linear_objective_coefficient.pop(variable_id, None)
else:
self.linear_objective_coefficient[variable_id] = value
for watcher in self._update_trackers:
if variable_id < watcher.variables_checkpoint:
watcher.linear_objective_coefficients.add(variable_id)
def get_linear_objective_coefficient(self, variable_id: int) -> float:
self._check_variable_id(variable_id)
return self.linear_objective_coefficient.get(variable_id, 0.0)
def get_linear_objective_coefficients(
self,
) -> Iterator[model_storage.LinearObjectiveEntry]:
for var_id, coef in self.linear_objective_coefficient.items():
yield model_storage.LinearObjectiveEntry(
variable_id=var_id, coefficient=coef
)
def set_quadratic_objective_coefficient(
self, first_variable_id: int, second_variable_id: int, value: float
) -> None:
self._check_variable_id(first_variable_id)
self._check_variable_id(second_variable_id)
updated = self._quadratic_objective_coefficients.set_coefficient(
first_variable_id, second_variable_id, value
)
if updated:
for watcher in self._update_trackers:
if (
max(first_variable_id, second_variable_id)
< watcher.variables_checkpoint
):
watcher.quadratic_objective_coefficients.add(
_QuadraticKey(first_variable_id, second_variable_id)
)
def get_quadratic_objective_coefficient(
self, first_variable_id: int, second_variable_id: int
) -> float:
self._check_variable_id(first_variable_id)
self._check_variable_id(second_variable_id)
return self._quadratic_objective_coefficients.get_coefficient(
first_variable_id, second_variable_id
)
def get_quadratic_objective_coefficients(
self,
) -> Iterator[model_storage.QuadraticEntry]:
yield from self._quadratic_objective_coefficients.coefficients()
def get_quadratic_objective_adjacent_variables(
self, variable_id: int
) -> Iterator[int]:
self._check_variable_id(variable_id)
yield from self._quadratic_objective_coefficients.get_adjacent_variables(
variable_id
)
def set_is_maximize(self, is_maximize: bool) -> None:
if self._is_maximize == is_maximize:
return
self._is_maximize = is_maximize
for watcher in self._update_trackers:
watcher.objective_direction = True
def get_is_maximize(self) -> bool:
return self._is_maximize
def set_objective_offset(self, offset: float) -> None:
if self._objective_offset == offset:
return
self._objective_offset = offset
for watcher in self._update_trackers:
watcher.objective_offset = True
def get_objective_offset(self) -> float:
return self._objective_offset
def export_model(self) -> model_pb2.ModelProto:
m: model_pb2.ModelProto = model_pb2.ModelProto()
m.name = self._name
_variables_to_proto(self.variables.items(), m.variables)
_linear_constraints_to_proto(
self.linear_constraints.items(), m.linear_constraints
)
m.objective.maximize = self._is_maximize
m.objective.offset = self._objective_offset
if self.linear_objective_coefficient:
obj_ids, obj_coefs = zip(*sorted(self.linear_objective_coefficient.items()))
m.objective.linear_coefficients.ids.extend(obj_ids)
m.objective.linear_coefficients.values.extend(obj_coefs)
if self._quadratic_objective_coefficients:
first_var_ids, second_var_ids, coefficients = zip(
*sorted(
[
(entry.id_key.id1, entry.id_key.id2, entry.coefficient)
for entry in self._quadratic_objective_coefficients.coefficients()
]
)
)
m.objective.quadratic_coefficients.row_ids.extend(first_var_ids)
m.objective.quadratic_coefficients.column_ids.extend(second_var_ids)
m.objective.quadratic_coefficients.coefficients.extend(coefficients)
if self._linear_constraint_matrix:
flat_matrix_items = [
(con_id, var_id, coef)
for ((con_id, var_id), coef) in self._linear_constraint_matrix.items()
]
lin_con_ids, var_ids, lin_con_coefs = zip(*sorted(flat_matrix_items))
m.linear_constraint_matrix.row_ids.extend(lin_con_ids)
m.linear_constraint_matrix.column_ids.extend(var_ids)
m.linear_constraint_matrix.coefficients.extend(lin_con_coefs)
return m
def add_update_tracker(self) -> model_storage.StorageUpdateTracker:
tracker = _UpdateTracker(self)
self._update_trackers.add(tracker)
return tracker
def remove_update_tracker(
self, tracker: model_storage.StorageUpdateTracker
) -> None:
self._update_trackers.remove(tracker)
tracker.retired = True
def _check_variable_id(self, variable_id: int) -> None:
if variable_id not in self.variables:
raise model_storage.BadVariableIdError(variable_id)
def _check_linear_constraint_id(self, linear_constraint_id: int) -> None:
if linear_constraint_id not in self.linear_constraints:
raise model_storage.BadLinearConstraintIdError(linear_constraint_id)
def _set_sparse_double_vector(
id_value_pairs: Iterable[Tuple[int, float]],
proto: sparse_containers_pb2.SparseDoubleVectorProto,
) -> None:
"""id_value_pairs must be sorted, proto is filled."""
if not id_value_pairs:
return
ids, values = zip(*id_value_pairs)
proto.ids[:] = ids
proto.values[:] = values
def _set_sparse_bool_vector(
id_value_pairs: Iterable[Tuple[int, bool]],
proto: sparse_containers_pb2.SparseBoolVectorProto,
) -> None:
"""id_value_pairs must be sorted, proto is filled."""
if not id_value_pairs:
return
ids, values = zip(*id_value_pairs)
proto.ids[:] = ids
proto.values[:] = values
def _variables_to_proto(
variables: Iterable[Tuple[int, _VariableStorage]],
proto: model_pb2.VariablesProto,
) -> None:
"""Exports variables to proto."""
has_named_var = False
for _, var_storage in variables:
if var_storage.name:
has_named_var = True
break
for var_id, var_storage in variables:
proto.ids.append(var_id)
proto.lower_bounds.append(var_storage.lower_bound)
proto.upper_bounds.append(var_storage.upper_bound)
proto.integers.append(var_storage.is_integer)
if has_named_var:
proto.names.append(var_storage.name)
def _linear_constraints_to_proto(
linear_constraints: Iterable[Tuple[int, _LinearConstraintStorage]],
proto: model_pb2.LinearConstraintsProto,
) -> None:
"""Exports variables to proto."""
has_named_lin_con = False
for _, lin_con_storage in linear_constraints:
if lin_con_storage.name:
has_named_lin_con = True
break
for lin_con_id, lin_con_storage in linear_constraints:
proto.ids.append(lin_con_id)
proto.lower_bounds.append(lin_con_storage.lower_bound)
proto.upper_bounds.append(lin_con_storage.upper_bound)
if has_named_lin_con:
proto.names.append(lin_con_storage.name)

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python3
# Copyright 2010-2022 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.
"""Tests for hash_model_storage that cannot be covered by model_storage_(update)_test."""
import unittest
from ortools.math_opt.python import hash_model_storage
class HashModelStorageTest(unittest.TestCase):
def test_quadratic_term_storage(self):
storage = hash_model_storage._QuadraticTermStorage()
storage.set_coefficient(0, 1, 1.0)
storage.delete_variable(0)
self.assertEmpty(list(storage.get_adjacent_variables(0)))
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
# Copyright 2010-2022 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.
"""Module exporting all classes and functions needed for MathOpt.
This module defines aliases to all classes and functions needed for regular use
of MathOpt. It removes the need for users to have multiple imports for specific
sub-modules.
For example instead of:
from ortools.math_opt.python import model
from ortools.math_opt.python import solve
m = model.Model()
solve.solve(m)
we can simply do:
from ortools.math_opt.python import mathopt
m = mathopt.Model()
mathopt.solve(m)
"""
# pylint: disable=unused-import
# pylint: disable=g-importing-member
from ortools.math_opt.python.callback import BarrierStats
from ortools.math_opt.python.callback import CallbackData
from ortools.math_opt.python.callback import CallbackRegistration
from ortools.math_opt.python.callback import CallbackResult
from ortools.math_opt.python.callback import Event
from ortools.math_opt.python.callback import GeneratedConstraint
from ortools.math_opt.python.callback import MipStats
from ortools.math_opt.python.callback import parse_callback_data
from ortools.math_opt.python.callback import PresolveStats
from ortools.math_opt.python.callback import SimplexStats
from ortools.math_opt.python.compute_infeasible_subsystem_result import (
ComputeInfeasibleSubsystemResult,
)
from ortools.math_opt.python.compute_infeasible_subsystem_result import ModelSubset
from ortools.math_opt.python.compute_infeasible_subsystem_result import (
ModelSubsetBounds,
)
from ortools.math_opt.python.compute_infeasible_subsystem_result import (
parse_compute_infeasible_subsystem_result,
)
from ortools.math_opt.python.compute_infeasible_subsystem_result import (
parse_model_subset,
)
from ortools.math_opt.python.compute_infeasible_subsystem_result import (
parse_model_subset_bounds,
)
from ortools.math_opt.python.hash_model_storage import HashModelStorage
from ortools.math_opt.python.message_callback import list_message_callback
from ortools.math_opt.python.message_callback import log_messages
from ortools.math_opt.python.message_callback import printer_message_callback
from ortools.math_opt.python.message_callback import SolveMessageCallback
from ortools.math_opt.python.message_callback import vlog_messages
from ortools.math_opt.python.model import as_flat_linear_expression
from ortools.math_opt.python.model import as_flat_quadratic_expression
from ortools.math_opt.python.model import as_normalized_linear_inequality
from ortools.math_opt.python.model import BoundedLinearExpression
from ortools.math_opt.python.model import BoundedLinearTypes
from ortools.math_opt.python.model import BoundedLinearTypesList
from ortools.math_opt.python.model import LinearBase
from ortools.math_opt.python.model import LinearConstraint
from ortools.math_opt.python.model import LinearConstraintMatrixEntry
from ortools.math_opt.python.model import LinearExpression
from ortools.math_opt.python.model import LinearLinearProduct
from ortools.math_opt.python.model import LinearProduct
from ortools.math_opt.python.model import LinearSum
from ortools.math_opt.python.model import LinearTerm
from ortools.math_opt.python.model import LinearTypes
from ortools.math_opt.python.model import LinearTypesExceptVariable
from ortools.math_opt.python.model import LowerBoundedLinearExpression
from ortools.math_opt.python.model import Model
from ortools.math_opt.python.model import NormalizedLinearInequality
from ortools.math_opt.python.model import Objective
from ortools.math_opt.python.model import QuadraticBase
from ortools.math_opt.python.model import QuadraticExpression
from ortools.math_opt.python.model import QuadraticProduct
from ortools.math_opt.python.model import QuadraticSum
from ortools.math_opt.python.model import QuadraticTerm
from ortools.math_opt.python.model import QuadraticTermKey
from ortools.math_opt.python.model import QuadraticTypes
from ortools.math_opt.python.model import Storage
from ortools.math_opt.python.model import StorageClass
from ortools.math_opt.python.model import UpdateTracker
from ortools.math_opt.python.model import UpperBoundedLinearExpression
from ortools.math_opt.python.model import VarEqVar
from ortools.math_opt.python.model import Variable
from ortools.math_opt.python.model_parameters import ModelSolveParameters
from ortools.math_opt.python.model_parameters import parse_solution_hint
from ortools.math_opt.python.model_parameters import SolutionHint
from ortools.math_opt.python.model_storage import BadLinearConstraintIdError
from ortools.math_opt.python.model_storage import BadVariableIdError
from ortools.math_opt.python.model_storage import LinearConstraintMatrixIdEntry
from ortools.math_opt.python.model_storage import LinearObjectiveEntry
from ortools.math_opt.python.model_storage import ModelStorage
from ortools.math_opt.python.model_storage import ModelStorageImpl
from ortools.math_opt.python.model_storage import ModelStorageImplClass
from ortools.math_opt.python.model_storage import QuadraticEntry
from ortools.math_opt.python.model_storage import QuadraticTermIdKey
from ortools.math_opt.python.model_storage import StorageUpdateTracker
from ortools.math_opt.python.model_storage import UsedUpdateTrackerAfterRemovalError
from ortools.math_opt.python.parameters import Emphasis
from ortools.math_opt.python.parameters import emphasis_from_proto
from ortools.math_opt.python.parameters import emphasis_to_proto
from ortools.math_opt.python.parameters import GlpkParameters
from ortools.math_opt.python.parameters import GurobiParameters
from ortools.math_opt.python.parameters import lp_algorithm_from_proto
from ortools.math_opt.python.parameters import lp_algorithm_to_proto
from ortools.math_opt.python.parameters import LPAlgorithm
from ortools.math_opt.python.parameters import SolveParameters
from ortools.math_opt.python.parameters import solver_type_from_proto
from ortools.math_opt.python.parameters import solver_type_to_proto
from ortools.math_opt.python.parameters import SolverType
from ortools.math_opt.python.result import FeasibilityStatus
from ortools.math_opt.python.result import Limit
from ortools.math_opt.python.result import ObjectiveBounds
from ortools.math_opt.python.result import parse_objective_bounds
from ortools.math_opt.python.result import parse_problem_status
from ortools.math_opt.python.result import parse_solve_result
from ortools.math_opt.python.result import parse_solve_stats
from ortools.math_opt.python.result import parse_termination
from ortools.math_opt.python.result import ProblemStatus
from ortools.math_opt.python.result import SolveResult
from ortools.math_opt.python.result import SolveStats
from ortools.math_opt.python.result import Termination
from ortools.math_opt.python.result import TerminationReason
from ortools.math_opt.python.solution import Basis
from ortools.math_opt.python.solution import BasisStatus
from ortools.math_opt.python.solution import DualRay
from ortools.math_opt.python.solution import DualSolution
from ortools.math_opt.python.solution import parse_basis
from ortools.math_opt.python.solution import parse_dual_ray
from ortools.math_opt.python.solution import parse_dual_solution
from ortools.math_opt.python.solution import parse_primal_ray
from ortools.math_opt.python.solution import parse_primal_solution
from ortools.math_opt.python.solution import parse_solution
from ortools.math_opt.python.solution import PrimalRay
from ortools.math_opt.python.solution import PrimalSolution
from ortools.math_opt.python.solution import Solution
from ortools.math_opt.python.solution import SolutionStatus
from ortools.math_opt.python.solve import compute_infeasible_subsystem
from ortools.math_opt.python.solve import IncrementalSolver
from ortools.math_opt.python.solve import solve
from ortools.math_opt.python.solve import SolveCallback
from ortools.math_opt.python.sparse_containers import LinearConstraintFilter
from ortools.math_opt.python.sparse_containers import parse_linear_constraint_map
from ortools.math_opt.python.sparse_containers import parse_variable_map
from ortools.math_opt.python.sparse_containers import SparseVectorFilter
from ortools.math_opt.python.sparse_containers import to_sparse_double_vector_proto
from ortools.math_opt.python.sparse_containers import to_sparse_int32_vector_proto
from ortools.math_opt.python.sparse_containers import VariableFilter
from ortools.math_opt.python.sparse_containers import VarOrConstraintType
# pylint: enable=unused-import
# pylint: enable=g-importing-member

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# Copyright 2010-2022 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.
"""Tests for mathopt."""
import inspect
import types
import typing
from typing import Any, List, Set, Tuple
import unittest
from ortools.math_opt.python import callback
from ortools.math_opt.python import hash_model_storage
from ortools.math_opt.python import mathopt
from ortools.math_opt.python import message_callback
from ortools.math_opt.python import model
from ortools.math_opt.python import model_parameters
from ortools.math_opt.python import model_storage
from ortools.math_opt.python import parameters
from ortools.math_opt.python import result
from ortools.math_opt.python import solution
from ortools.math_opt.python import solve
from ortools.math_opt.python import sparse_containers
# This list does not contain some modules intentionally:
#
# - `remote_solve`: this depends on Stubby and having it in mathopt.py would
# force all users using MathOpt to depend on Stubby.
#
# - `statistics`: this is not part of the main libraries. In C++ too it is not
# included by `cpp/math_opt.h`. If we decide to change that, then maybe it
# would make sense to replace the top-level functions by member functions on
# the Model.
#
_MODULES_TO_CHECK: List[types.ModuleType] = [
callback,
hash_model_storage,
message_callback,
model,
model_parameters,
model_storage,
parameters,
result,
sparse_containers,
solution,
solve,
]
# Some symbols are not meant to be exported; we exclude them here.
_EXCLUDED_SYMBOLS: Set[Tuple[types.ModuleType, str]] = {
(solution, "T"),
}
_TYPING_PUBLIC_CONTENT = [
getattr(typing, name) for name in dir(typing) if not name.startswith("_")
]
def _is_actual_export(v: Any) -> bool:
if inspect.ismodule(v):
return False
if getattr(v, "__module__", None) != typing.__name__:
return True
return v not in _TYPING_PUBLIC_CONTENT
def _get_public_api(module: types.ModuleType) -> List[Tuple[str, Any]]:
tuple_list = inspect.getmembers(module, _is_actual_export)
return [(name, obj) for name, obj in tuple_list if not name.startswith("_")]
class MathoptTest(unittest.TestCase):
def test_imports(self) -> None:
missing_imports: List[str] = []
for module in _MODULES_TO_CHECK:
for name, obj in _get_public_api(module):
if (module, name) in _EXCLUDED_SYMBOLS:
continue
if hasattr(mathopt, name):
self.assertIs(
getattr(mathopt, name),
obj,
msg=f"module: {module.__name__} name: {name}",
)
else:
# We don't immediately asserts on a missing import so that we can get
# the list of all missing ones.
missing_imports.append(f"from {module.__name__} import {name}")
# We can't have \ in an expression inside an f-string.
nl = "\n"
self.assertFalse(
bool(missing_imports),
msg=f"missing imports:\n{nl.join(missing_imports)}",
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,142 @@
# Copyright 2010-2022 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.
"""Definition and tools for message callbacks.
Message callbacks are used to get the text messages emitted by solvers during
the solve.
Typical usage example:
# Print messages to stdout.
result = solve.solve(
model, parameters.SolverType.GSCIP,
msg_cb=message_callback.printer_message_callback(prefix='[solver] '))
# Log messages with absl.logging.
result = solve.solve(
model, parameters.SolverType.GSCIP,
msg_cb=lambda msgs: message_callback.log_messages(
msgs, prefix='[solver] '))
"""
import sys
import threading
from typing import Callable, List, Sequence, TextIO
from absl import logging
SolveMessageCallback = Callable[[Sequence[str]], None]
def printer_message_callback(
*, file: TextIO = sys.stdout, prefix: str = ""
) -> SolveMessageCallback:
"""Returns a message callback that prints to a file.
It prints its output to the given text file, prefixing each line with the
given prefix.
For each call to the returned message callback, the output_stream is flushed.
Args:
file: The file to print to. It prints to stdout by default.
prefix: The prefix to print in front of each line.
Returns:
A function matching the expected signature for message callbacks.
"""
mutex = threading.Lock()
def callback(messages: Sequence[str]) -> None:
with mutex:
for message in messages:
file.write(prefix)
file.write(message)
file.write("\n")
file.flush()
return callback
def log_messages(
messages: Sequence[str], *, level: int = logging.INFO, prefix: str = ""
) -> None:
"""Logs the input messages from a message callback using absl.logging.log().
It logs each line with the given prefix. It setups absl.logging so that the
logs use the file name and line of the caller of this function.
Typical usage example:
result = solve.solve(
model, parameters.SolverType.GSCIP,
msg_cb=lambda msgs: message_callback.log_messages(
msgs, prefix='[solver] '))
Args:
messages: The messages received in the message callback (typically a lambda
function in the caller code).
level: One of absl.logging.(DEBUG|INFO|WARNING|ERROR|FATAL).
prefix: The prefix to print in front of each line.
"""
for message in messages:
logging.log(level, "%s%s", prefix, message)
logging.ABSLLogger.register_frame_to_skip(__file__, log_messages.__name__)
def vlog_messages(messages: Sequence[str], level: int, *, prefix: str = "") -> None:
"""Logs the input messages from a message callback using absl.logging.vlog().
It logs each line with the given prefix. It setups absl.logging so that the
logs use the file name and line of the caller of this function.
Typical usage example:
result = solve.solve(
model, parameters.SolverType.GSCIP,
msg_cb=lambda msgs: message_callback.vlog_messages(
msgs, 1, prefix='[solver] '))
Args:
messages: The messages received in the message callback (typically a lambda
function in the caller code).
level: The verbose log level, e.g. 1, 2...
prefix: The prefix to print in front of each line.
"""
for message in messages:
logging.vlog(level, "%s%s", prefix, message)
logging.ABSLLogger.register_frame_to_skip(__file__, vlog_messages.__name__)
def list_message_callback(sink: List[str]) -> SolveMessageCallback:
"""Returns a message callback that logs messages to a list.
Args:
sink: The list to append messages to.
Returns:
A function matching the expected signature for message callbacks.
"""
mutex = threading.Lock()
def callback(messages: Sequence[str]) -> None:
with mutex:
for message in messages:
sink.append(message)
return callback

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
# Copyright 2010-2022 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.
"""Tests for message_callback."""
import io
import os
from absl import logging
import unittest
from ortools.math_opt.python import message_callback
class PrinterMessageCallbackTest(unittest.TestCase):
def test_no_prefix(self):
class FlushCountingStringIO(io.StringIO):
def __init__(self):
super().__init__()
self.num_flushes: int = 0
def flush(self):
super().flush()
self.num_flushes += 1
buf = FlushCountingStringIO()
cb = message_callback.printer_message_callback(file=buf)
cb(["line 1", "line 2"])
cb(["line 3"])
self.assertMultiLineEqual(buf.getvalue(), "line 1\nline 2\nline 3\n")
self.assertEqual(buf.num_flushes, 2)
def test_with_prefix(self):
buf = io.StringIO()
cb = message_callback.printer_message_callback(file=buf, prefix="test> ")
cb(["line 1", "line 2"])
cb(["line 3"])
self.assertMultiLineEqual(
buf.getvalue(), "test> line 1\ntest> line 2\ntest> line 3\n"
)
class LogMessagesTest(unittest.TestCase):
def test_defaults(self):
with self.assertLogs(logger="absl", level="INFO") as logs:
message_callback.log_messages(["line 1", "line 2"])
self.assertListEqual(logs.output, ["INFO:absl:line 1", "INFO:absl:line 2"])
def test_prefix(self):
with self.assertLogs(logger="absl") as logs:
message_callback.log_messages(["line 1", "line 2"], prefix="solver: ")
self.assertListEqual(
logs.output, ["INFO:absl:solver: line 1", "INFO:absl:solver: line 2"]
)
def test_warning(self):
with self.assertLogs(logger="absl") as logs:
message_callback.log_messages(["line 1", "line 2"], level=logging.WARNING)
self.assertListEqual(
logs.output, ["WARNING:absl:line 1", "WARNING:absl:line 2"]
)
def test_records_path(self):
with self.assertLogs(logger="absl") as logs:
message_callback.log_messages(["line 1", "line 2"])
self.assertSetEqual(
set(os.path.basename(r.pathname) for r in logs.records),
set(("message_callback_test.py",)),
)
class VLogMessagesTest(unittest.TestCase):
"""Tests of vlog_messages().
In the tests we abuse the logging level 0 since there is not API in the
`logging` module to change the verbosity.
"""
def test_defaults(self):
with self.assertLogs(logger="absl") as logs:
message_callback.vlog_messages(["line 1", "line 2"], 0)
self.assertListEqual(logs.output, ["INFO:absl:line 1", "INFO:absl:line 2"])
def test_prefix(self):
with self.assertLogs(logger="absl") as logs:
message_callback.vlog_messages(["line 1", "line 2"], 0, prefix="solver: ")
self.assertListEqual(
logs.output, ["INFO:absl:solver: line 1", "INFO:absl:solver: line 2"]
)
def test_records_path(self):
with self.assertLogs(logger="absl") as logs:
message_callback.vlog_messages(["line 1", "line 2"], 0)
self.assertSetEqual(
set(os.path.basename(r.pathname) for r in logs.records),
set(("message_callback_test.py",)),
)
class ListMessageCallbackTest(unittest.TestCase):
def test_empty(self):
msgs = []
cb = message_callback.list_message_callback(msgs)
cb(["line 1", "line 2"])
cb(["line 3"])
self.assertSequenceEqual(msgs, ("line 1", "line 2", "line 3"))
def test_not_empty(self):
msgs = ["initial", "content"]
cb = message_callback.list_message_callback(msgs)
cb(["line 1", "line 2"])
cb(["line 3"])
self.assertSequenceEqual(
msgs, ("initial", "content", "line 1", "line 2", "line 3")
)
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,155 @@
# Copyright 2010-2022 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.
"""Model specific solver configuration (e.g. starting basis)."""
import dataclasses
from typing import Dict, List, Optional
from ortools.math_opt import model_parameters_pb2
from ortools.math_opt.python import model
from ortools.math_opt.python import solution
from ortools.math_opt.python import sparse_containers
@dataclasses.dataclass
class SolutionHint:
"""A suggested starting solution for the solver.
MIP solvers generally only want primal information (`variable_values`),
while LP solvers want both primal and dual information (`dual_values`).
Many MIP solvers can work with: (1) partial solutions that do not specify all
variables or (2) infeasible solutions. In these cases, solvers typically solve
a sub-MIP to complete/correct the hint.
How the hint is used by the solver, if at all, is highly dependent on the
solver, the problem type, and the algorithm used. The most reliable way to
ensure your hint has an effect is to read the underlying solvers logs with
and without the hint.
Simplex-based LP solvers typically prefer an initial basis to a solution
hint (they need to crossover to convert the hint to a basic feasible
solution otherwise).
Floating point values should be finite and not NaN, they are validated by
MathOpt at Solve() time (resulting in an exception).
Attributes:
variable_values: a potentially partial assignment from the model's primal
variables to finite (and not NaN) double values.
dual_values: a potentially partial assignment from the model's linear
constraints to finite (and not NaN) double values.
"""
variable_values: Dict[model.Variable, float] = dataclasses.field(
default_factory=dict
)
dual_values: Dict[model.LinearConstraint, float] = dataclasses.field(
default_factory=dict
)
def to_proto(self) -> model_parameters_pb2.SolutionHintProto:
"""Returns an equivalent protocol buffer to this."""
return model_parameters_pb2.SolutionHintProto(
variable_values=sparse_containers.to_sparse_double_vector_proto(
self.variable_values
),
dual_values=sparse_containers.to_sparse_double_vector_proto(
self.dual_values
),
)
def parse_solution_hint(
hint_proto: model_parameters_pb2.SolutionHintProto, mod: model.Model
) -> SolutionHint:
"""Returns an equivalent SolutionHint to `hint_proto`.
Args:
hint_proto: The solution, as encoded by the ids of the variables and
constraints.
mod: A MathOpt Model that must contain variables and linear constraints with
the ids from hint_proto.
Returns:
A SolutionHint equivalent.
Raises:
ValueError if hint_proto is invalid or refers to variables or constraints
not in mod.
"""
return SolutionHint(
variable_values=sparse_containers.parse_variable_map(
hint_proto.variable_values, mod
),
dual_values=sparse_containers.parse_linear_constraint_map(
hint_proto.dual_values, mod
),
)
@dataclasses.dataclass
class ModelSolveParameters:
"""Model specific solver configuration, for example, an initial basis.
This class mirrors (and can generate) the related proto
model_parameters_pb2.ModelSolveParametersProto.
Attributes:
variable_values_filter: Only return solution and primal ray values for
variables accepted by this filter (default accepts all variables).
dual_values_filter: Only return dual variable values and dual ray values for
linear constraints accepted by thei filter (default accepts all linear
constraints).
reduced_costs_filter: Only return reduced cost and dual ray values for
variables accepted by this filter (default accepts all variables).
initial_basis: If set, provides a warm start for simplex based solvers.
solution_hints: Optional solution hints. If the underlying solver only
accepts a single hint, the first hint is used.
branching_priorities: Optional branching priorities. Variables with higher
values will be branched on first. Variables for which priorities are not
set get the solver's default priority (usually zero).
"""
variable_values_filter: sparse_containers.VariableFilter = (
sparse_containers.VariableFilter()
)
dual_values_filter: sparse_containers.LinearConstraintFilter = (
sparse_containers.LinearConstraintFilter()
)
reduced_costs_filter: sparse_containers.VariableFilter = (
sparse_containers.VariableFilter()
)
initial_basis: Optional[solution.Basis] = None
solution_hints: List[SolutionHint] = dataclasses.field(default_factory=list)
branching_priorities: Dict[model.Variable, int] = dataclasses.field(
default_factory=dict
)
def to_proto(self) -> model_parameters_pb2.ModelSolveParametersProto:
"""Returns an equivalent protocol buffer."""
# TODO(b/236289022): these methods should check that the variables are from
# the correct model.
result = model_parameters_pb2.ModelSolveParametersProto(
variable_values_filter=self.variable_values_filter.to_proto(),
dual_values_filter=self.dual_values_filter.to_proto(),
reduced_costs_filter=self.reduced_costs_filter.to_proto(),
branching_priorities=sparse_containers.to_sparse_int32_vector_proto(
self.branching_priorities
),
)
if self.initial_basis:
result.initial_basis.CopyFrom(self.initial_basis.to_proto())
for hint in self.solution_hints:
result.solution_hints.append(hint.to_proto())
return result

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
# Copyright 2010-2022 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.
import unittest
from ortools.math_opt import model_parameters_pb2
from ortools.math_opt import solution_pb2
from ortools.math_opt import sparse_containers_pb2
from ortools.math_opt.python import model
from ortools.math_opt.python import model_parameters
from ortools.math_opt.python import solution
from ortools.math_opt.python import sparse_containers
from ortools.math_opt.python.testing import compare_proto
class ModelParametersTest(compare_proto.MathOptProtoAssertions, unittest.TestCase):
def test_solution_hint_round_trip(self) -> None:
mod = model.Model(name="test_model")
x = mod.add_binary_variable(name="x")
y = mod.add_binary_variable(name="y")
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d")
hint = model_parameters.SolutionHint(
variable_values={x: 2.0, y: 3.0}, dual_values={c: 4.0, d: 5.0}
)
hint_round_trip = model_parameters.parse_solution_hint(hint.to_proto(), mod)
self.assertDictEqual(hint_round_trip.variable_values, hint.variable_values)
self.assertDictEqual(hint_round_trip.dual_values, hint.dual_values)
def test_model_parameters_to_proto_no_basis(self) -> None:
mod = model.Model(name="test_model")
x = mod.add_binary_variable(name="x")
y = mod.add_binary_variable(name="y")
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
params = model_parameters.ModelSolveParameters()
params.variable_values_filter = sparse_containers.SparseVectorFilter(
filtered_items=(y,)
)
params.reduced_costs_filter = sparse_containers.SparseVectorFilter(
skip_zero_values=True
)
params.dual_values_filter = sparse_containers.SparseVectorFilter(
filtered_items=(c,)
)
params.solution_hints.append(
model_parameters.SolutionHint(
variable_values={x: 1.0, y: 1.0}, dual_values={c: 3.0}
)
)
params.solution_hints.append(
model_parameters.SolutionHint(variable_values={y: 0.0})
)
params.branching_priorities[y] = 2
actual = params.to_proto()
expected = model_parameters_pb2.ModelSolveParametersProto(
variable_values_filter=sparse_containers_pb2.SparseVectorFilterProto(
filter_by_ids=True, filtered_ids=(1,)
),
reduced_costs_filter=sparse_containers_pb2.SparseVectorFilterProto(
skip_zero_values=True
),
dual_values_filter=sparse_containers_pb2.SparseVectorFilterProto(
filter_by_ids=True, filtered_ids=(0,)
),
branching_priorities=sparse_containers_pb2.SparseInt32VectorProto(
ids=[1], values=[2]
),
)
h1 = expected.solution_hints.add()
h1.variable_values.ids[:] = [0, 1]
h1.variable_values.values[:] = [1.0, 1.0]
h1.dual_values.ids[:] = [0]
h1.dual_values.values[:] = [3]
h2 = expected.solution_hints.add()
h2.variable_values.ids.append(1)
h2.variable_values.values.append(0.0)
self.assert_protos_equiv(actual, expected)
def test_model_parameters_to_proto_with_basis(self) -> None:
mod = model.Model(name="test_model")
x = mod.add_binary_variable(name="x")
params = model_parameters.ModelSolveParameters()
params.initial_basis = solution.Basis()
params.initial_basis.variable_status[x] = solution.BasisStatus.AT_UPPER_BOUND
actual = params.to_proto()
expected = model_parameters_pb2.ModelSolveParametersProto()
expected.initial_basis.variable_status.ids.append(0)
expected.initial_basis.variable_status.values.append(
solution_pb2.BASIS_STATUS_AT_UPPER_BOUND
)
expected.initial_basis.basic_dual_feasibility = (
solution_pb2.SOLUTION_STATUS_UNDETERMINED
)
self.assert_protos_equiv(expected, actual)
if __name__ == "__main__":
unittest.main()

Some files were not shown because too many files have changed in this diff Show More