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:
30
WORKSPACE
30
WORKSPACE
@@ -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(
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
)
|
||||
@@ -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(
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
@@ -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"])
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(¶meters_);
|
||||
preprocessor.SetLogger(&logger_);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
// --------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
// --------------------------------------------------------
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "ortools/base/hash.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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_)));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
98
ortools/gurobi/gurobi_util.cc
Normal file
98
ortools/gurobi/gurobi_util.cc
Normal 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, ¶m_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, ¤t_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, ¤t_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
|
||||
31
ortools/gurobi/gurobi_util.h
Normal file
31
ortools/gurobi/gurobi_util.h
Normal 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_
|
||||
@@ -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",
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
221
ortools/linear_solver/proto_solver/glop_proto_solver.cc
Normal file
221
ortools/linear_solver/proto_solver/glop_proto_solver.cc
Normal 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(), ¶ms)) {
|
||||
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
|
||||
55
ortools/linear_solver/proto_solver/glop_proto_solver.h
Normal file
55
ortools/linear_solver/proto_solver/glop_proto_solver.h
Normal 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_
|
||||
67
ortools/linear_solver/proto_solver/proto_utils.h
Normal file
67
ortools/linear_solver/proto_solver/proto_utils.h
Normal 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_
|
||||
@@ -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(), ¶ms)) {
|
||||
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(), ¶ms)) {
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
63
ortools/linear_solver/python/solve_model.py
Normal file
63
ortools/linear_solver/python/solve_model.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = """
|
||||
|
||||
@@ -30,7 +30,6 @@ def create_data_model():
|
||||
data["bins"] = data["items"]
|
||||
data["bin_capacity"] = 100
|
||||
return data
|
||||
|
||||
# [END data_model]
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
// --------------------------------------------------------
|
||||
|
||||
@@ -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_.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
33
ortools/math_opt/core/python/BUILD.bazel
Normal file
33
ortools/math_opt/core/python/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
||||
152
ortools/math_opt/core/python/solver.cc
Normal file
152
ortools/math_opt/core/python/solver.cc
Normal 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
|
||||
180
ortools/math_opt/core/python/solver_gurobi_test.py
Normal file
180
ortools/math_opt/core/python/solver_gurobi_test.py
Normal 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()
|
||||
273
ortools/math_opt/core/python/solver_test.py
Normal file
273
ortools/math_opt/core/python/solver_test.py
Normal 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()
|
||||
@@ -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_
|
||||
@@ -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"],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
190
ortools/math_opt/python/BUILD.bazel
Normal file
190
ortools/math_opt/python/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
||||
346
ortools/math_opt/python/callback.py
Normal file
346
ortools/math_opt/python/callback.py
Normal 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
|
||||
253
ortools/math_opt/python/callback_test.py
Normal file
253
ortools/math_opt/python/callback_test.py
Normal 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()
|
||||
197
ortools/math_opt/python/compute_infeasible_subsystem_result.py
Normal file
197
ortools/math_opt/python/compute_infeasible_subsystem_result.py
Normal 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,
|
||||
)
|
||||
@@ -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()
|
||||
843
ortools/math_opt/python/hash_model_storage.py
Normal file
843
ortools/math_opt/python/hash_model_storage.py
Normal 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)
|
||||
30
ortools/math_opt/python/hash_model_storage_test.py
Normal file
30
ortools/math_opt/python/hash_model_storage_test.py
Normal 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()
|
||||
2797
ortools/math_opt/python/linear_expression_test.py
Normal file
2797
ortools/math_opt/python/linear_expression_test.py
Normal file
File diff suppressed because it is too large
Load Diff
169
ortools/math_opt/python/mathopt.py
Normal file
169
ortools/math_opt/python/mathopt.py
Normal 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
|
||||
108
ortools/math_opt/python/mathopt_test.py
Normal file
108
ortools/math_opt/python/mathopt_test.py
Normal 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()
|
||||
142
ortools/math_opt/python/message_callback.py
Normal file
142
ortools/math_opt/python/message_callback.py
Normal 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
|
||||
134
ortools/math_opt/python/message_callback_test.py
Normal file
134
ortools/math_opt/python/message_callback_test.py
Normal 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()
|
||||
2253
ortools/math_opt/python/model.py
Normal file
2253
ortools/math_opt/python/model.py
Normal file
File diff suppressed because it is too large
Load Diff
155
ortools/math_opt/python/model_parameters.py
Normal file
155
ortools/math_opt/python/model_parameters.py
Normal 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
|
||||
109
ortools/math_opt/python/model_parameters_test.py
Normal file
109
ortools/math_opt/python/model_parameters_test.py
Normal 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
Reference in New Issue
Block a user