Bump math_opt
This commit is contained in:
@@ -76,6 +76,7 @@ proto_library(
|
||||
deps = [
|
||||
"//ortools/glop:parameters_proto",
|
||||
"//ortools/gscip:gscip_proto",
|
||||
"//ortools/math_opt/solvers:gurobi_proto",
|
||||
"//ortools/sat:sat_parameters_proto",
|
||||
"@com_google_protobuf//:duration_proto",
|
||||
],
|
||||
|
||||
@@ -24,60 +24,82 @@ syntax = "proto3";
|
||||
package operations_research.math_opt;
|
||||
|
||||
import "google/protobuf/duration.proto";
|
||||
import "ortools/math_opt/solution.proto";
|
||||
import "ortools/math_opt/sparse_containers.proto";
|
||||
|
||||
option java_package = "com.google.ortools.mathopt";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// This is the list of supported events.
|
||||
enum CallbackEventProto {
|
||||
CALLBACK_EVENT_UNSPECIFIED = 0;
|
||||
// Quick ping from the solver saying “I am still working, and not stuck”.
|
||||
CALLBACK_EVENT_POLLING = 1;
|
||||
|
||||
// The solver is currently running presolve.
|
||||
CALLBACK_EVENT_PRESOLVE = 2;
|
||||
//
|
||||
// This event is supported for MIP & LP models by SOLVER_TYPE_GUROBI. Other
|
||||
// solvers don't support this event.
|
||||
CALLBACK_EVENT_PRESOLVE = 1;
|
||||
|
||||
// The solver is currently running the simplex method.
|
||||
CALLBACK_EVENT_SIMPLEX = 3;
|
||||
//
|
||||
// This event is supported for MIP & LP models by SOLVER_TYPE_GUROBI. Other
|
||||
// solvers don't support this event.
|
||||
CALLBACK_EVENT_SIMPLEX = 2;
|
||||
|
||||
// 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.
|
||||
CALLBACK_EVENT_MIP = 4;
|
||||
//
|
||||
// This event is supported for MIP models only by SOLVER_TYPE_GUROBI. Other
|
||||
// solvers don't support this event.
|
||||
CALLBACK_EVENT_MIP = 3;
|
||||
|
||||
// Called every time a new MIP incumbent is found.
|
||||
CALLBACK_EVENT_MIP_SOLUTION = 5;
|
||||
//
|
||||
// This event is fully supported for MIP models by SOLVER_TYPE_GUROBI. CP-SAT
|
||||
// has partial support: you can view the solutions and request termination,
|
||||
// but you cannot add lazy constraints. Other solvers don't support this
|
||||
// event.
|
||||
CALLBACK_EVENT_MIP_SOLUTION = 4;
|
||||
|
||||
// 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.
|
||||
CALLBACK_EVENT_MIP_NODE = 6;
|
||||
//
|
||||
// Disabling cuts using CommonSolveParametersProto may interfere with this
|
||||
// event being called and/or adding cuts at this event, the behavior is solver
|
||||
// specific.
|
||||
//
|
||||
// This event is supported for MIP models only by SOLVER_TYPE_GUROBI. Other
|
||||
// solvers don't support this event.
|
||||
CALLBACK_EVENT_MIP_NODE = 5;
|
||||
|
||||
// Called in each iterate of an interior point/barrier method.
|
||||
CALLBACK_EVENT_BARRIER = 7;
|
||||
// Called when the solver wants to log a message.
|
||||
CALLBACK_EVENT_MESSAGE = 8;
|
||||
//
|
||||
// This event is supported for LP models only by SOLVER_TYPE_GUROBI. Other
|
||||
// solvers don't support this event.
|
||||
CALLBACK_EVENT_BARRIER = 6;
|
||||
}
|
||||
|
||||
// The callback function input data.
|
||||
// Note that depending on the event, some information might be unavailable.
|
||||
message CallbackDataProto {
|
||||
CallbackEventProto event = 1;
|
||||
// if event == CALLBACK_EVENT_MIP_NODE, the primal_solution contains the
|
||||
// primal solution to the current LP-node relaxation. In some cases, no
|
||||
// solution will be available (e.g. because LP was infeasible or the solve
|
||||
// was imprecise).
|
||||
// if event == CALLBACK_EVENT_MIP_SOLUTION, the primal_solution contains the
|
||||
// newly found primal (integer) feasible solution.
|
||||
// Otherwise, the primal_solution is not available.
|
||||
// if event == CALLBACK_EVENT_MIP_NODE, the primal_solution_vector contains
|
||||
// the variable values of the primal solution for the current LP-node
|
||||
// relaxation. In some cases, no solution will be available (e.g. because
|
||||
// LP was infeasible or the solve was imprecise).
|
||||
// if event == CALLBACK_EVENT_MIP_SOLUTION, the primal_solution_vector
|
||||
// contains variable values for the newly found primal (integer) feasible
|
||||
// solution.
|
||||
// Otherwise, the primal_solution_vector is not available.
|
||||
//
|
||||
// Note that, because of variable filters, it is possible that when a solution
|
||||
// is found, it is empty. The message will be set but left empty in this case,
|
||||
// while it will be unset when no solution is available.
|
||||
//
|
||||
// TODO(b/186740537): change the type to SparseDoubleVectorProto.
|
||||
PrimalSolutionProto primal_solution = 2;
|
||||
|
||||
// If event == CALLBACK_EVENT_MESSAGE, return the messages from the solver.
|
||||
// Each message represents a single output line from the solver, and each
|
||||
// message does not contain any '\n' character in it.
|
||||
repeated string messages = 3;
|
||||
SparseDoubleVectorProto primal_solution_vector = 2;
|
||||
|
||||
// Running time since the `Solve` call.
|
||||
google.protobuf.Duration runtime = 4;
|
||||
google.protobuf.Duration runtime = 3;
|
||||
|
||||
// Presolve stats. Only available during CALLBACK_EVENT_PRESOLVE.
|
||||
message PresolveStats {
|
||||
@@ -86,7 +108,7 @@ message CallbackDataProto {
|
||||
optional int64 bound_changes = 3;
|
||||
optional int64 coefficient_changes = 4;
|
||||
}
|
||||
PresolveStats presolve_stats = 5;
|
||||
PresolveStats presolve_stats = 4;
|
||||
|
||||
// Simplex stats. Only available during CALLBACK_EVENT_SIMPLEX.
|
||||
message SimplexStats {
|
||||
@@ -96,7 +118,7 @@ message CallbackDataProto {
|
||||
optional double dual_infeasibility = 4;
|
||||
optional bool is_pertubated = 5;
|
||||
}
|
||||
SimplexStats simplex_stats = 6;
|
||||
SimplexStats simplex_stats = 5;
|
||||
|
||||
// Barrier stats. Only available during CALLBACK_EVENT_BARRIER.
|
||||
message BarrierStats {
|
||||
@@ -107,9 +129,10 @@ message CallbackDataProto {
|
||||
optional double primal_infeasibility = 5;
|
||||
optional double dual_infeasibility = 6;
|
||||
}
|
||||
BarrierStats barrier_stats = 7;
|
||||
BarrierStats barrier_stats = 6;
|
||||
|
||||
// MIP B&B stats. Only available during CALLBACK_EVENT_MIPxxxx events.
|
||||
// Not supported for CP-SAT.
|
||||
message MipStats {
|
||||
optional double primal_bound = 1;
|
||||
optional double dual_bound = 2;
|
||||
@@ -119,7 +142,7 @@ message CallbackDataProto {
|
||||
optional int32 number_of_solutions_found = 6;
|
||||
optional int32 cutting_planes_in_lp = 7;
|
||||
}
|
||||
MipStats mip_stats = 8;
|
||||
MipStats mip_stats = 7;
|
||||
}
|
||||
|
||||
// Return value of a callback function.
|
||||
@@ -142,6 +165,7 @@ message CallbackResultProto {
|
||||
|
||||
// Ends the solve early.
|
||||
bool terminate = 1;
|
||||
|
||||
// TODO(b/172214608): SCIP allows to reject a feasible solution without
|
||||
// providing a cut. This is something we might support at a later stage.
|
||||
|
||||
@@ -149,21 +173,26 @@ message CallbackResultProto {
|
||||
// GeneratedLinearConstraint::is_lazy for details.
|
||||
repeated GeneratedLinearConstraint cuts = 4;
|
||||
|
||||
// Use only for CALLBACK_EVENT_MIP_NODE.
|
||||
//
|
||||
// Note that some solvers (e.g. Gurobi) support partially-defined solutions.
|
||||
// The most common use case is to specify a value for each variable in the
|
||||
// model. If a variable is not present in the primal solution, its value is
|
||||
// taken to be undefined, and is up to the underlying solver to deal with it.
|
||||
// For example, Gurobi will try to solve a Sub-MIP to get a fully feasible
|
||||
// solution if necessary.
|
||||
//
|
||||
// TODO(b/183631989) rename to suggested_solutions.
|
||||
// TODO(b/186740537): change the type to SparseDoubleVectorProto.
|
||||
repeated PrimalSolutionProto suggested_solution = 5;
|
||||
repeated SparseDoubleVectorProto suggested_solutions = 5;
|
||||
}
|
||||
|
||||
message CallbackRegistrationProto {
|
||||
// What events you want to get a callback at.
|
||||
// By default, there are no callbacks on any event.
|
||||
// The events the solver should invoke the callback at.
|
||||
//
|
||||
// A solver will return an InvalidArgument status when called with registered
|
||||
// events that are not supported for the selected solver and the type of
|
||||
// model. For example registring for CALLBACK_EVENT_MIP with a model that only
|
||||
// contains continuous variables will fail for most solvers (see the
|
||||
// documentation of each event to see which solvers support them and in which
|
||||
// case).
|
||||
repeated CallbackEventProto request_registration = 1;
|
||||
|
||||
// If CALLBACK_EVENT_MIP_SOLUTION is in `request_registration`, then
|
||||
|
||||
@@ -10,6 +10,7 @@ cc_library(
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
"//ortools/math_opt:model_cc_proto",
|
||||
"//ortools/math_opt:model_update_cc_proto",
|
||||
"//ortools/math_opt:result_cc_proto",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
],
|
||||
@@ -19,6 +20,8 @@ cc_library(
|
||||
name = "sparse_vector_view",
|
||||
hdrs = ["sparse_vector_view.h"],
|
||||
deps = [
|
||||
":arrow_operator_proxy",
|
||||
":sparse_vector",
|
||||
"//ortools/base",
|
||||
"//ortools/base:map_util",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
@@ -42,24 +45,12 @@ cc_library(
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "model_update_merge",
|
||||
srcs = ["model_update_merge.cc"],
|
||||
hdrs = ["model_update_merge.h"],
|
||||
name = "model_storage",
|
||||
srcs = ["model_storage.cc"],
|
||||
hdrs = ["model_storage.h"],
|
||||
deps = [
|
||||
"//ortools/base",
|
||||
"//ortools/math_opt:model_cc_proto",
|
||||
"//ortools/math_opt:model_update_cc_proto",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "indexed_model",
|
||||
srcs = ["indexed_model.cc"],
|
||||
hdrs = ["indexed_model.h"],
|
||||
deps = [
|
||||
":sparse_vector_view",
|
||||
":model_update_merge",
|
||||
":sparse_vector_view",
|
||||
"//ortools/base",
|
||||
"//ortools/base:int_type",
|
||||
"//ortools/base:map_util",
|
||||
@@ -68,10 +59,15 @@ cc_library(
|
||||
"//ortools/math_opt:result_cc_proto",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"//ortools/math_opt/validators:model_validator",
|
||||
"@com_google_absl//absl/base:core_headers",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"@com_google_absl//absl/memory",
|
||||
"@com_google_absl//absl/meta:type_traits",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
"@com_google_absl//absl/types:span",
|
||||
],
|
||||
)
|
||||
@@ -81,6 +77,8 @@ cc_library(
|
||||
srcs = ["solver_interface.cc"],
|
||||
hdrs = ["solver_interface.h"],
|
||||
deps = [
|
||||
":non_streamable_solver_init_arguments",
|
||||
":solve_interrupter",
|
||||
"//ortools/base",
|
||||
"//ortools/base:map_util",
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
@@ -105,6 +103,9 @@ cc_library(
|
||||
hdrs = ["solver.h"],
|
||||
deps = [
|
||||
":model_summary",
|
||||
":non_streamable_solver_init_arguments",
|
||||
":solve_interrupter",
|
||||
":solver_debug",
|
||||
":solver_interface",
|
||||
"//ortools/base",
|
||||
"//ortools/base:status_macros",
|
||||
@@ -117,11 +118,64 @@ cc_library(
|
||||
"//ortools/math_opt/validators:callback_validator",
|
||||
"//ortools/math_opt/validators:model_parameters_validator",
|
||||
"//ortools/math_opt/validators:model_validator",
|
||||
"//ortools/math_opt/validators:solution_validator",
|
||||
"//ortools/math_opt/validators:result_validator",
|
||||
"//ortools/math_opt/validators:solver_parameters_validator",
|
||||
"//ortools/port:proto_utils",
|
||||
"@com_google_absl//absl/memory",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/types:span",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "model_update_merge",
|
||||
srcs = ["model_update_merge.cc"],
|
||||
hdrs = ["model_update_merge.h"],
|
||||
deps = [
|
||||
":sparse_vector_view",
|
||||
"//ortools/base",
|
||||
"//ortools/base:protobuf_util",
|
||||
"//ortools/math_opt:model_cc_proto",
|
||||
"//ortools/math_opt:model_update_cc_proto",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "solve_interrupter",
|
||||
srcs = ["solve_interrupter.cc"],
|
||||
hdrs = ["solve_interrupter.h"],
|
||||
deps = [
|
||||
"//ortools/base",
|
||||
"//ortools/base:int_type",
|
||||
"//ortools/base:linked_hash_map",
|
||||
"//ortools/base:map_util",
|
||||
"@com_google_absl//absl/base:core_headers",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "non_streamable_solver_init_arguments",
|
||||
hdrs = ["non_streamable_solver_init_arguments.h"],
|
||||
deps = ["//ortools/math_opt:parameters_cc_proto"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "solver_debug",
|
||||
srcs = ["solver_debug.cc"],
|
||||
hdrs = ["solver_debug.h"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "arrow_operator_proxy",
|
||||
hdrs = ["arrow_operator_proxy.h"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "sparse_vector",
|
||||
hdrs = ["sparse_vector.h"],
|
||||
)
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_ARROW_OPERATOR_PROXY_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_ARROW_OPERATOR_PROXY_H_
|
||||
#ifndef OR_TOOLS_MATH_OPT_CORE_ARROW_OPERATOR_PROXY_H_
|
||||
#define OR_TOOLS_MATH_OPT_CORE_ARROW_OPERATOR_PROXY_H_
|
||||
|
||||
#include <utility> // IWYU pragma: keep
|
||||
|
||||
@@ -40,4 +40,4 @@ class ArrowOperatorProxy {
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_ARROW_OPERATOR_PROXY_H_
|
||||
#endif // OR_TOOLS_MATH_OPT_CORE_ARROW_OPERATOR_PROXY_H_
|
||||
@@ -1,627 +0,0 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "absl/types/optional.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/base/map_util.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/math_opt/core/model_update_merge.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_update.pb.h"
|
||||
#include "ortools/math_opt/result.pb.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename K, typename V>
|
||||
std::vector<K> MapKeys(const absl::flat_hash_map<K, V>& in_map) {
|
||||
std::vector<K> keys;
|
||||
keys.reserve(in_map.size());
|
||||
for (const auto& key_pair : in_map) {
|
||||
keys.push_back(key_pair.first);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
template <typename K, typename V>
|
||||
std::vector<K> SortedMapKeys(const absl::flat_hash_map<K, V>& in_map) {
|
||||
std::vector<K> keys = MapKeys(in_map);
|
||||
std::sort(keys.begin(), keys.end());
|
||||
return keys;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::vector<T> SortedSetKeys(const absl::flat_hash_set<T>& in_set) {
|
||||
std::vector<T> keys;
|
||||
keys.reserve(in_set.size());
|
||||
for (const auto& key : in_set) {
|
||||
keys.push_back(key);
|
||||
}
|
||||
std::sort(keys.begin(), keys.end());
|
||||
return keys;
|
||||
}
|
||||
|
||||
// ids should be sorted.
|
||||
template <typename IdType>
|
||||
void AppendFromMapOrDefault(const absl::Span<const IdType> ids,
|
||||
const absl::flat_hash_map<IdType, double>& values,
|
||||
SparseDoubleVectorProto& sparse_vector) {
|
||||
for (const IdType id : ids) {
|
||||
sparse_vector.add_ids(id.value());
|
||||
sparse_vector.add_values(gtl::FindWithDefault(values, id));
|
||||
}
|
||||
}
|
||||
|
||||
// ids should be sorted.
|
||||
template <typename IdType, typename IdIterable>
|
||||
void AppendFromMapIfPresent(const IdIterable& ids,
|
||||
const absl::flat_hash_map<IdType, double>& values,
|
||||
SparseDoubleVectorProto& sparse_vector) {
|
||||
for (const IdType id : ids) {
|
||||
const double* const double_value = gtl::FindOrNull(values, id);
|
||||
if (double_value != nullptr) {
|
||||
sparse_vector.add_ids(id.value());
|
||||
sparse_vector.add_values(*double_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename IdType, typename DataType>
|
||||
void AppendFromMap(const absl::flat_hash_set<IdType>& dirty_keys,
|
||||
const absl::flat_hash_map<IdType, DataType>& values,
|
||||
double DataType::*field,
|
||||
SparseDoubleVectorProto& sparse_vector) {
|
||||
for (const IdType id : SortedSetKeys(dirty_keys)) {
|
||||
sparse_vector.add_ids(id.value());
|
||||
sparse_vector.add_values(values.at(id).*field);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
absl::flat_hash_map<T, BasisStatus> SparseBasisVectorToMap(
|
||||
const SparseBasisStatusVector& sparse_vector) {
|
||||
absl::flat_hash_map<T, BasisStatus> result;
|
||||
CHECK_EQ(sparse_vector.ids_size(), sparse_vector.values_size());
|
||||
result.reserve(sparse_vector.ids_size());
|
||||
for (const auto [id, value] : MakeView(sparse_vector)) {
|
||||
gtl::InsertOrDie(&result, T(id), static_cast<BasisStatus>(value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
VariableId IndexedModel::AddVariable(const double lower_bound,
|
||||
const double upper_bound,
|
||||
const bool is_integer,
|
||||
const absl::string_view name) {
|
||||
const VariableId result = next_variable_id_++;
|
||||
VariableData& var_data = variables_[result];
|
||||
var_data.lower_bound = lower_bound;
|
||||
var_data.upper_bound = upper_bound;
|
||||
var_data.is_integer = is_integer;
|
||||
var_data.name = name;
|
||||
if (!lazy_matrix_columns_.empty()) {
|
||||
gtl::InsertOrDie(&lazy_matrix_columns_, result, {});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void IndexedModel::DeleteVariable(const VariableId id) {
|
||||
CHECK(variables_.contains(id));
|
||||
EnsureLazyMatrixColumns();
|
||||
EnsureLazyMatrixRows();
|
||||
linear_objective_.erase(id);
|
||||
variables_.erase(id);
|
||||
if (id < variables_checkpoint_) {
|
||||
dirty_variable_deletes_.insert(id);
|
||||
dirty_variable_lower_bounds_.erase(id);
|
||||
dirty_variable_upper_bounds_.erase(id);
|
||||
dirty_variable_is_integer_.erase(id);
|
||||
dirty_linear_objective_coefficients_.erase(id);
|
||||
}
|
||||
for (const LinearConstraintId related_constraint :
|
||||
lazy_matrix_columns_.at(id)) {
|
||||
CHECK_GT(lazy_matrix_rows_.at(related_constraint).erase(id), 0);
|
||||
CHECK_GT(linear_constraint_matrix_.erase({related_constraint, id}), 0);
|
||||
if (id < variables_checkpoint_ &&
|
||||
related_constraint < linear_constraints_checkpoint_) {
|
||||
dirty_linear_constraint_matrix_keys_.erase({related_constraint, id});
|
||||
}
|
||||
}
|
||||
CHECK_GT(lazy_matrix_columns_.erase(id), 0);
|
||||
}
|
||||
|
||||
std::vector<VariableId> IndexedModel::variables() const {
|
||||
return MapKeys(variables_);
|
||||
}
|
||||
|
||||
std::vector<VariableId> IndexedModel::SortedVariables() const {
|
||||
return SortedMapKeys(variables_);
|
||||
}
|
||||
|
||||
LinearConstraintId IndexedModel::AddLinearConstraint(
|
||||
const double lower_bound, const double upper_bound,
|
||||
const absl::string_view name) {
|
||||
const LinearConstraintId result = next_linear_constraint_id_++;
|
||||
LinearConstraintData& lin_con_data = linear_constraints_[result];
|
||||
lin_con_data.lower_bound = lower_bound;
|
||||
lin_con_data.upper_bound = upper_bound;
|
||||
lin_con_data.name = name;
|
||||
if (!lazy_matrix_rows_.empty()) {
|
||||
gtl::InsertOrDie(&lazy_matrix_rows_, result, {});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void IndexedModel::DeleteLinearConstraint(const LinearConstraintId id) {
|
||||
CHECK(linear_constraints_.contains(id));
|
||||
EnsureLazyMatrixColumns();
|
||||
EnsureLazyMatrixRows();
|
||||
linear_constraints_.erase(id);
|
||||
if (id < linear_constraints_checkpoint_) {
|
||||
dirty_linear_constraint_deletes_.insert(id);
|
||||
dirty_linear_constraint_lower_bounds_.erase(id);
|
||||
dirty_linear_constraint_upper_bounds_.erase(id);
|
||||
}
|
||||
for (const VariableId related_variable : lazy_matrix_rows_.at(id)) {
|
||||
CHECK_GT(lazy_matrix_columns_.at(related_variable).erase(id), 0);
|
||||
CHECK_GT(linear_constraint_matrix_.erase({id, related_variable}), 0);
|
||||
if (id < linear_constraints_checkpoint_ &&
|
||||
related_variable < variables_checkpoint_) {
|
||||
dirty_linear_constraint_matrix_keys_.erase({id, related_variable});
|
||||
}
|
||||
}
|
||||
CHECK_GT(lazy_matrix_rows_.erase(id), 0);
|
||||
}
|
||||
|
||||
std::vector<LinearConstraintId> IndexedModel::linear_constraints() const {
|
||||
return MapKeys(linear_constraints_);
|
||||
}
|
||||
|
||||
std::vector<LinearConstraintId> IndexedModel::SortedLinearConstraints() const {
|
||||
return SortedMapKeys(linear_constraints_);
|
||||
}
|
||||
|
||||
std::vector<VariableId> IndexedModel::SortedLinearObjectiveNonzeroVariables()
|
||||
const {
|
||||
return SortedMapKeys(linear_objective_);
|
||||
}
|
||||
|
||||
void IndexedModel::AppendVariable(const VariableId id,
|
||||
VariablesProto& variables_proto) const {
|
||||
const VariableData& var_data = variables_.at(id);
|
||||
variables_proto.add_ids(id.value());
|
||||
variables_proto.add_lower_bounds(var_data.lower_bound);
|
||||
variables_proto.add_upper_bounds(var_data.upper_bound);
|
||||
variables_proto.add_integers(var_data.is_integer);
|
||||
variables_proto.add_names(var_data.name);
|
||||
}
|
||||
|
||||
void IndexedModel::AppendLinearConstraint(
|
||||
const LinearConstraintId id,
|
||||
LinearConstraintsProto& linear_constraints_proto) const {
|
||||
const LinearConstraintData& con_impl = linear_constraints_.at(id);
|
||||
linear_constraints_proto.add_ids(id.value());
|
||||
linear_constraints_proto.add_lower_bounds(con_impl.lower_bound);
|
||||
linear_constraints_proto.add_upper_bounds(con_impl.upper_bound);
|
||||
linear_constraints_proto.add_names(con_impl.name);
|
||||
}
|
||||
|
||||
void IndexedModel::ExportLinearConstraintMatrix(
|
||||
const absl::Span<const std::pair<LinearConstraintId, VariableId>> entries,
|
||||
SparseDoubleMatrixProto& matrix) const {
|
||||
matrix.mutable_row_ids()->Reserve(entries.size());
|
||||
matrix.mutable_column_ids()->Reserve(entries.size());
|
||||
matrix.mutable_coefficients()->Reserve(entries.size());
|
||||
for (const auto [constraint_id, variable_id] : entries) {
|
||||
matrix.add_row_ids(constraint_id.value());
|
||||
matrix.add_column_ids(variable_id.value());
|
||||
matrix.add_coefficients(gtl::FindWithDefault(linear_constraint_matrix_,
|
||||
{constraint_id, variable_id}));
|
||||
}
|
||||
}
|
||||
|
||||
ModelProto IndexedModel::ExportModel() const {
|
||||
ModelProto result;
|
||||
result.set_name(name_);
|
||||
// Export the variables.
|
||||
for (const VariableId variable : SortedMapKeys(variables_)) {
|
||||
AppendVariable(variable, *result.mutable_variables());
|
||||
}
|
||||
|
||||
// Pull out the objective.
|
||||
result.mutable_objective()->set_maximize(is_maximize_);
|
||||
result.mutable_objective()->set_offset(objective_offset_);
|
||||
AppendFromMapOrDefault<VariableId>(
|
||||
SortedMapKeys(linear_objective_), linear_objective_,
|
||||
*result.mutable_objective()->mutable_linear_coefficients());
|
||||
|
||||
// Pull out the linear constraints.
|
||||
for (const LinearConstraintId con : SortedMapKeys(linear_constraints_)) {
|
||||
AppendLinearConstraint(con, *result.mutable_linear_constraints());
|
||||
}
|
||||
|
||||
// Pull out the constraint matrix.
|
||||
ExportLinearConstraintMatrix(SortedMapKeys(linear_constraint_matrix_),
|
||||
*result.mutable_linear_constraint_matrix());
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::optional<ModelUpdateProto> IndexedModel::ExportSharedModelUpdate() {
|
||||
// We must detect the empty case to prevent unneeded copies and merging in
|
||||
// UpdateTracker::ExportModelUpdate().
|
||||
if (variables_checkpoint_ == next_variable_id_ &&
|
||||
linear_constraints_checkpoint_ == next_linear_constraint_id_ &&
|
||||
!dirty_objective_direction_ && !dirty_objective_offset_ &&
|
||||
dirty_variable_deletes_.empty() && dirty_variable_lower_bounds_.empty() &&
|
||||
dirty_variable_upper_bounds_.empty() &&
|
||||
dirty_variable_is_integer_.empty() &&
|
||||
dirty_linear_objective_coefficients_.empty() &&
|
||||
dirty_linear_constraint_deletes_.empty() &&
|
||||
dirty_linear_constraint_lower_bounds_.empty() &&
|
||||
dirty_linear_constraint_upper_bounds_.empty() &&
|
||||
dirty_linear_constraint_matrix_keys_.empty()) {
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
// TODO(user): these are used to efficiently extract the constraint matrix
|
||||
// update, but it would be good to avoid calling these because they result in
|
||||
// a large allocation.
|
||||
EnsureLazyMatrixRows();
|
||||
EnsureLazyMatrixColumns();
|
||||
|
||||
ModelUpdateProto result;
|
||||
|
||||
// Variable/constraint deletions.
|
||||
for (const VariableId del_var : SortedSetKeys(dirty_variable_deletes_)) {
|
||||
result.add_deleted_variable_ids(del_var.value());
|
||||
}
|
||||
for (const LinearConstraintId del_lin_con :
|
||||
SortedSetKeys(dirty_linear_constraint_deletes_)) {
|
||||
result.add_deleted_linear_constraint_ids(del_lin_con.value());
|
||||
}
|
||||
|
||||
// Update the variables.
|
||||
auto var_updates = result.mutable_variable_updates();
|
||||
AppendFromMap(dirty_variable_lower_bounds_, variables_,
|
||||
&VariableData::lower_bound,
|
||||
*var_updates->mutable_lower_bounds());
|
||||
AppendFromMap(dirty_variable_upper_bounds_, variables_,
|
||||
&VariableData::upper_bound,
|
||||
*var_updates->mutable_upper_bounds());
|
||||
|
||||
for (const VariableId integer_var :
|
||||
SortedSetKeys(dirty_variable_is_integer_)) {
|
||||
var_updates->mutable_integers()->add_ids(integer_var.value());
|
||||
var_updates->mutable_integers()->add_values(
|
||||
variables_.at(integer_var).is_integer);
|
||||
}
|
||||
for (VariableId new_id = variables_checkpoint_; new_id < next_variable_id_;
|
||||
++new_id) {
|
||||
if (variables_.contains(new_id)) {
|
||||
AppendVariable(new_id, *result.mutable_new_variables());
|
||||
}
|
||||
}
|
||||
|
||||
// Update the objective
|
||||
auto obj_updates = result.mutable_objective_updates();
|
||||
if (dirty_objective_direction_) {
|
||||
obj_updates->set_direction_update(is_maximize_);
|
||||
}
|
||||
if (dirty_objective_offset_) {
|
||||
obj_updates->set_offset_update(objective_offset_);
|
||||
}
|
||||
AppendFromMapOrDefault<VariableId>(
|
||||
SortedSetKeys(dirty_linear_objective_coefficients_), linear_objective_,
|
||||
*obj_updates->mutable_linear_coefficients());
|
||||
// TODO(b/182567749): Once StrongInt is in absl, use
|
||||
// AppendFromMapIfPresent<VariableId>(
|
||||
// MakeStrongIntRange(variables_checkpoint_, next_variable_id_),
|
||||
// linear_objective_, *obj_updates->mutable_linear_coefficients());
|
||||
for (VariableId var_id = variables_checkpoint_; var_id < next_variable_id_;
|
||||
++var_id) {
|
||||
const double* const double_value =
|
||||
gtl::FindOrNull(linear_objective_, var_id);
|
||||
if (double_value != nullptr) {
|
||||
obj_updates->mutable_linear_coefficients()->add_ids(var_id.value());
|
||||
obj_updates->mutable_linear_coefficients()->add_values(*double_value);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the linear constraints
|
||||
auto lin_con_updates = result.mutable_linear_constraint_updates();
|
||||
AppendFromMap(dirty_linear_constraint_lower_bounds_, linear_constraints_,
|
||||
&LinearConstraintData::lower_bound,
|
||||
*lin_con_updates->mutable_lower_bounds());
|
||||
AppendFromMap(dirty_linear_constraint_upper_bounds_, linear_constraints_,
|
||||
&LinearConstraintData::upper_bound,
|
||||
*lin_con_updates->mutable_upper_bounds());
|
||||
|
||||
for (LinearConstraintId new_id = linear_constraints_checkpoint_;
|
||||
new_id < next_linear_constraint_id_; ++new_id) {
|
||||
if (linear_constraints_.contains(new_id)) {
|
||||
AppendLinearConstraint(new_id, *result.mutable_new_linear_constraints());
|
||||
}
|
||||
}
|
||||
|
||||
// Extract changes to the matrix of linear constraint coefficients
|
||||
std::vector<std::pair<LinearConstraintId, VariableId>>
|
||||
constraint_matrix_updates(dirty_linear_constraint_matrix_keys_.begin(),
|
||||
dirty_linear_constraint_matrix_keys_.end());
|
||||
for (VariableId new_var = variables_checkpoint_; new_var < next_variable_id_;
|
||||
++new_var) {
|
||||
if (variables_.contains(new_var)) {
|
||||
for (const LinearConstraintId lin_con :
|
||||
lazy_matrix_columns_.at(new_var)) {
|
||||
constraint_matrix_updates.emplace_back(lin_con, new_var);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (LinearConstraintId new_lin_con = linear_constraints_checkpoint_;
|
||||
new_lin_con < next_linear_constraint_id_; ++new_lin_con) {
|
||||
if (linear_constraints_.contains(new_lin_con)) {
|
||||
for (const VariableId var : lazy_matrix_rows_.at(new_lin_con)) {
|
||||
// NOTE(user): we will do at most twice as much as needed here.
|
||||
if (var < variables_checkpoint_) {
|
||||
constraint_matrix_updates.emplace_back(new_lin_con, var);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(constraint_matrix_updates.begin(), constraint_matrix_updates.end());
|
||||
ExportLinearConstraintMatrix(
|
||||
constraint_matrix_updates,
|
||||
*result.mutable_linear_constraint_matrix_updates());
|
||||
|
||||
// Named returned value optimization (NRVO) does not apply here since the
|
||||
// return type if not the same type as `result`. To make things clear, we
|
||||
// explicitly call the constructor here.
|
||||
return {std::move(result)};
|
||||
}
|
||||
|
||||
void IndexedModel::EnsureLazyMatrixColumns() {
|
||||
if (lazy_matrix_columns_.empty()) {
|
||||
for (const auto& var_pair : variables_) {
|
||||
lazy_matrix_columns_.insert({var_pair.first, {}});
|
||||
}
|
||||
for (const auto& mat_entry : linear_constraint_matrix_) {
|
||||
lazy_matrix_columns_.at(mat_entry.first.second)
|
||||
.insert(mat_entry.first.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IndexedModel::EnsureLazyMatrixRows() {
|
||||
if (lazy_matrix_rows_.empty()) {
|
||||
for (const auto& lin_con_pair : linear_constraints_) {
|
||||
lazy_matrix_rows_.insert({lin_con_pair.first, {}});
|
||||
}
|
||||
for (const auto& mat_entry : linear_constraint_matrix_) {
|
||||
lazy_matrix_rows_.at(mat_entry.first.first)
|
||||
.insert(mat_entry.first.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IndexedModel::SharedCheckpoint() {
|
||||
variables_checkpoint_ = next_variable_id_;
|
||||
linear_constraints_checkpoint_ = next_linear_constraint_id_;
|
||||
dirty_objective_direction_ = false;
|
||||
dirty_objective_offset_ = false;
|
||||
|
||||
dirty_variable_deletes_.clear();
|
||||
dirty_variable_lower_bounds_.clear();
|
||||
dirty_variable_upper_bounds_.clear();
|
||||
dirty_variable_is_integer_.clear();
|
||||
|
||||
dirty_linear_objective_coefficients_.clear();
|
||||
|
||||
dirty_linear_constraint_deletes_.clear();
|
||||
dirty_linear_constraint_lower_bounds_.clear();
|
||||
dirty_linear_constraint_upper_bounds_.clear();
|
||||
dirty_linear_constraint_matrix_keys_.clear();
|
||||
}
|
||||
|
||||
IndexedSolutions IndexedSolutionsFromProto(
|
||||
const SolveResultProto& solve_result) {
|
||||
IndexedSolutions solutions;
|
||||
for (const PrimalSolutionProto& primal_solution :
|
||||
solve_result.primal_solutions()) {
|
||||
IndexedPrimalSolution p;
|
||||
p.variable_values =
|
||||
MakeView(primal_solution.variable_values()).as_map<VariableId>();
|
||||
p.objective_value = primal_solution.objective_value();
|
||||
solutions.primal_solutions.push_back(std::move(p));
|
||||
}
|
||||
for (const PrimalRayProto& primal_ray : solve_result.primal_rays()) {
|
||||
IndexedPrimalRay pr;
|
||||
pr.variable_values =
|
||||
MakeView(primal_ray.variable_values()).as_map<VariableId>();
|
||||
solutions.primal_rays.push_back(std::move(pr));
|
||||
}
|
||||
for (const DualSolutionProto& dual_solution : solve_result.dual_solutions()) {
|
||||
IndexedDualSolution d;
|
||||
d.reduced_costs =
|
||||
MakeView(dual_solution.reduced_costs()).as_map<VariableId>();
|
||||
d.dual_values =
|
||||
MakeView(dual_solution.dual_values()).as_map<LinearConstraintId>();
|
||||
d.objective_value = dual_solution.objective_value();
|
||||
solutions.dual_solutions.push_back(std::move(d));
|
||||
}
|
||||
for (const DualRayProto& dual_ray : solve_result.dual_rays()) {
|
||||
IndexedDualRay dr;
|
||||
dr.reduced_costs = MakeView(dual_ray.reduced_costs()).as_map<VariableId>();
|
||||
dr.dual_values =
|
||||
MakeView(dual_ray.dual_values()).as_map<LinearConstraintId>();
|
||||
solutions.dual_rays.push_back(std::move(dr));
|
||||
}
|
||||
for (const BasisProto& basis : solve_result.basis()) {
|
||||
IndexedBasis indexed_basis;
|
||||
indexed_basis.constraint_status =
|
||||
SparseBasisVectorToMap<LinearConstraintId>(basis.constraint_status());
|
||||
indexed_basis.variable_status =
|
||||
SparseBasisVectorToMap<VariableId>(basis.variable_status());
|
||||
solutions.basis.push_back(std::move(indexed_basis));
|
||||
}
|
||||
return solutions;
|
||||
}
|
||||
|
||||
std::unique_ptr<IndexedModel::UpdateTracker> IndexedModel::NewUpdateTracker() {
|
||||
// UpdateTracker constructor will call UpdateTracker::Checkpoint() that
|
||||
// flushes the current update to all other trackers and updates the checkpoint
|
||||
// of this model to the current state of the model as returned by
|
||||
// ExportModel().
|
||||
return absl::WrapUnique(new UpdateTracker(*this));
|
||||
}
|
||||
|
||||
IndexedModel::UpdateTracker::UpdateTracker(IndexedModel& indexed_model)
|
||||
: indexed_model_(indexed_model) {
|
||||
absl::MutexLock lock(&indexed_model_.update_trackers_lock_);
|
||||
CHECK(indexed_model_.update_trackers_.insert(this).second);
|
||||
CheckpointLocked();
|
||||
}
|
||||
|
||||
IndexedModel::UpdateTracker::~UpdateTracker() {
|
||||
absl::MutexLock lock(&indexed_model_.update_trackers_lock_);
|
||||
CHECK(indexed_model_.update_trackers_.erase(this));
|
||||
}
|
||||
|
||||
absl::optional<ModelUpdateProto>
|
||||
IndexedModel::UpdateTracker::ExportModelUpdate() {
|
||||
absl::MutexLock lock(&indexed_model_.update_trackers_lock_);
|
||||
|
||||
// No updates have been pushed, the checkpoint of this tracker is in sync with
|
||||
// the shared checkpoint of IndexedModel. We can return the IndexedModel
|
||||
// shared update without merging.
|
||||
if (updates_.empty()) {
|
||||
return indexed_model_.ExportSharedModelUpdate();
|
||||
}
|
||||
|
||||
// Find all trackers with the same checkpoint. By construction, all trackers
|
||||
// that have the same first update also share all next updates.
|
||||
std::vector<UpdateTracker*> all_trackers_at_checkpoint;
|
||||
bool found_this = false;
|
||||
for (UpdateTracker* const tracker : indexed_model_.update_trackers_) {
|
||||
if (!tracker->updates_.empty() &&
|
||||
tracker->updates_.front() == updates_.front()) {
|
||||
// Note that we set `found_this` inside the if branch to make sure we also
|
||||
// detect a bug in this code that would not include `this` in the list of
|
||||
// trackers.
|
||||
if (tracker == this) {
|
||||
found_this = true;
|
||||
}
|
||||
all_trackers_at_checkpoint.push_back(tracker);
|
||||
|
||||
// Validate that we have the same updates in debug mode only. In optimized
|
||||
// mode, only test the size of the updates_ vectors.
|
||||
CHECK_EQ(updates_.size(), tracker->updates_.size());
|
||||
if (DEBUG_MODE) {
|
||||
for (int i = 0; i < updates_.size(); ++i) {
|
||||
CHECK_EQ(updates_[i], tracker->updates_[i])
|
||||
<< "Two trackers have the same checkpoint but different updates.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CHECK(found_this);
|
||||
|
||||
// Possible optimizations here:
|
||||
//
|
||||
// * Maybe optimize the case where the first update is singly used by `this`
|
||||
// and use it as starting point instead of making a copy. This may be more
|
||||
// complicated if it is shared with multiple trackers since in that case we
|
||||
// must make sure to only update the shared instance if and only if only
|
||||
// trackers have a pointer to it, not external code (i.e. its use count is
|
||||
// the same as the number of trackers).
|
||||
//
|
||||
// * Use n-way merge here if the performances justify it.
|
||||
const auto merge = std::make_shared<ModelUpdateProto>();
|
||||
for (const auto& update : updates_) {
|
||||
MergeIntoUpdate(/*from=*/*update, /*into=*/*merge);
|
||||
}
|
||||
|
||||
// Push the merge to all trackers that have the same checkpoint (including
|
||||
// this tracker).
|
||||
for (UpdateTracker* const tracker : all_trackers_at_checkpoint) {
|
||||
tracker->updates_.clear();
|
||||
tracker->updates_.push_back(merge);
|
||||
}
|
||||
|
||||
ModelUpdateProto update = *merge;
|
||||
const absl::optional<ModelUpdateProto> pending_update =
|
||||
indexed_model_.ExportSharedModelUpdate();
|
||||
if (pending_update) {
|
||||
MergeIntoUpdate(/*from=*/*pending_update, /*into=*/update);
|
||||
}
|
||||
|
||||
// Named returned value optimization (NRVO) does not apply here since the
|
||||
// return type if not the same type as `result`. To make things clear, we
|
||||
// explicitly call the constructor here.
|
||||
return {std::move(update)};
|
||||
}
|
||||
|
||||
void IndexedModel::UpdateTracker::Checkpoint() {
|
||||
absl::MutexLock lock(&indexed_model_.update_trackers_lock_);
|
||||
|
||||
CheckpointLocked();
|
||||
}
|
||||
|
||||
void IndexedModel::UpdateTracker::CheckpointLocked() {
|
||||
// Optimize the case where we have a single tracker and we don't want to
|
||||
// update it. In that case we don't need to update trackers since we would
|
||||
// only update this one and clear it immediately.
|
||||
if (indexed_model_.update_trackers_.size() == 1) {
|
||||
CHECK(*indexed_model_.update_trackers_.begin() == this);
|
||||
} else {
|
||||
absl::optional<ModelUpdateProto> update =
|
||||
indexed_model_.ExportSharedModelUpdate();
|
||||
if (update) {
|
||||
const auto shared_update =
|
||||
std::make_shared<ModelUpdateProto>(*std::move(update));
|
||||
|
||||
bool found_this = false;
|
||||
for (UpdateTracker* const tracker : indexed_model_.update_trackers_) {
|
||||
if (tracker == this) {
|
||||
found_this = true;
|
||||
}
|
||||
tracker->updates_.push_back(shared_update);
|
||||
}
|
||||
CHECK(found_this);
|
||||
}
|
||||
}
|
||||
indexed_model_.SharedCheckpoint();
|
||||
updates_.clear();
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
@@ -75,5 +75,26 @@ absl::flat_hash_set<CallbackEventProto> EventSet(
|
||||
return events;
|
||||
}
|
||||
|
||||
TerminationProto TerminateForLimit(const LimitProto limit,
|
||||
const absl::string_view detail) {
|
||||
TerminationProto result;
|
||||
result.set_reason(TERMINATION_REASON_LIMIT_REACHED);
|
||||
result.set_limit(limit);
|
||||
if (!detail.empty()) {
|
||||
result.set_detail(std::string(detail));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
TerminationProto TerminateForReason(const TerminationReasonProto reason,
|
||||
const absl::string_view detail) {
|
||||
TerminationProto result;
|
||||
result.set_reason(reason);
|
||||
if (!detail.empty()) {
|
||||
result.set_detail(std::string(detail));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_update.pb.h"
|
||||
#include "ortools/math_opt/result.pb.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
@@ -90,6 +91,12 @@ class SparseVectorFilterPredicate {
|
||||
absl::flat_hash_set<CallbackEventProto> EventSet(
|
||||
const CallbackRegistrationProto& callback_registration);
|
||||
|
||||
TerminationProto TerminateForLimit(LimitProto limit,
|
||||
absl::string_view detail = {});
|
||||
|
||||
TerminationProto TerminateForReason(TerminationReasonProto reason,
|
||||
absl::string_view detail = {});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Inline functions implementations.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
881
ortools/math_opt/core/model_storage.cc
Normal file
881
ortools/math_opt/core/model_storage.cc
Normal file
@@ -0,0 +1,881 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/meta/type_traits.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/base/map_util.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/math_opt/core/model_update_merge.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_update.pb.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
#include "ortools/math_opt/validators/model_validator.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename K, typename V>
|
||||
std::vector<K> MapKeys(const absl::flat_hash_map<K, V>& in_map) {
|
||||
std::vector<K> keys;
|
||||
keys.reserve(in_map.size());
|
||||
for (const auto& key_pair : in_map) {
|
||||
keys.push_back(key_pair.first);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
template <typename K, typename V>
|
||||
std::vector<K> SortedMapKeys(const absl::flat_hash_map<K, V>& in_map) {
|
||||
std::vector<K> keys = MapKeys(in_map);
|
||||
std::sort(keys.begin(), keys.end());
|
||||
return keys;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::vector<T> SortedSetKeys(const absl::flat_hash_set<T>& in_set) {
|
||||
std::vector<T> keys;
|
||||
keys.reserve(in_set.size());
|
||||
for (const auto& key : in_set) {
|
||||
keys.push_back(key);
|
||||
}
|
||||
std::sort(keys.begin(), keys.end());
|
||||
return keys;
|
||||
}
|
||||
|
||||
// ids should be sorted.
|
||||
template <typename IdType>
|
||||
void AppendFromMapOrDefault(const absl::Span<const IdType> ids,
|
||||
const absl::flat_hash_map<IdType, double>& values,
|
||||
SparseDoubleVectorProto& sparse_vector) {
|
||||
for (const IdType id : ids) {
|
||||
sparse_vector.add_ids(id.value());
|
||||
sparse_vector.add_values(gtl::FindWithDefault(values, id));
|
||||
}
|
||||
}
|
||||
|
||||
// ids should be sorted.
|
||||
template <typename IdType, typename IdIterable>
|
||||
void AppendFromMapIfPresent(const IdIterable& ids,
|
||||
const absl::flat_hash_map<IdType, double>& values,
|
||||
SparseDoubleVectorProto& sparse_vector) {
|
||||
for (const IdType id : ids) {
|
||||
const double* const double_value = gtl::FindOrNull(values, id);
|
||||
if (double_value != nullptr) {
|
||||
sparse_vector.add_ids(id.value());
|
||||
sparse_vector.add_values(*double_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename IdType, typename DataType>
|
||||
void AppendFromMap(const absl::flat_hash_set<IdType>& dirty_keys,
|
||||
const absl::flat_hash_map<IdType, DataType>& values,
|
||||
double DataType::*field,
|
||||
SparseDoubleVectorProto& sparse_vector) {
|
||||
for (const IdType id : SortedSetKeys(dirty_keys)) {
|
||||
sparse_vector.add_ids(id.value());
|
||||
sparse_vector.add_values(values.at(id).*field);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
absl::flat_hash_map<T, BasisStatusProto> SparseBasisVectorToMap(
|
||||
const SparseBasisStatusVector& sparse_vector) {
|
||||
absl::flat_hash_map<T, BasisStatusProto> result;
|
||||
CHECK_EQ(sparse_vector.ids_size(), sparse_vector.values_size());
|
||||
result.reserve(sparse_vector.ids_size());
|
||||
for (const auto [id, value] : MakeView(sparse_vector)) {
|
||||
gtl::InsertOrDie(&result, T(id), static_cast<BasisStatusProto>(value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// If an element in keys is not found in coefficients, it is set to 0.0 in
|
||||
// matrix. Keys must be in lexicographic ordering (i.e. sorted).
|
||||
// NOTE: This signature can be updated to take a Span instead of a vector if
|
||||
// needed in the future, but it required specifying parameters at the callsites.
|
||||
template <typename RK, typename CK>
|
||||
SparseDoubleMatrixProto ExportMatrix(
|
||||
const absl::flat_hash_map<std::pair<RK, CK>, double>& coefficients,
|
||||
const std::vector<std::pair<RK, CK>>& keys) {
|
||||
SparseDoubleMatrixProto matrix;
|
||||
matrix.mutable_row_ids()->Reserve(keys.size());
|
||||
matrix.mutable_column_ids()->Reserve(keys.size());
|
||||
matrix.mutable_coefficients()->Reserve(keys.size());
|
||||
for (const auto [row_id, column_id] : keys) {
|
||||
matrix.add_row_ids(row_id.value());
|
||||
matrix.add_column_ids(column_id.value());
|
||||
matrix.add_coefficients(
|
||||
gtl::FindWithDefault(coefficients, {row_id, column_id}));
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
absl::StatusOr<std::unique_ptr<ModelStorage>> ModelStorage::FromModelProto(
|
||||
const ModelProto& model_proto) {
|
||||
// We don't check names since ModelStorage does not do so before exporting
|
||||
// models. Thus a model built by ModelStorage can contain duplicated
|
||||
// names. And since we use FromModelProto() to implement Clone(), we must make
|
||||
// sure duplicated names don't fail.
|
||||
RETURN_IF_ERROR(ValidateModel(model_proto, /*check_names=*/false));
|
||||
|
||||
auto storage = std::make_unique<ModelStorage>(model_proto.name());
|
||||
|
||||
// Add variables.
|
||||
storage->AddVariables(model_proto.variables());
|
||||
|
||||
// Set the objective.
|
||||
storage->set_is_maximize(model_proto.objective().maximize());
|
||||
storage->set_objective_offset(model_proto.objective().offset());
|
||||
storage->UpdateLinearObjectiveCoefficients(
|
||||
model_proto.objective().linear_coefficients());
|
||||
storage->UpdateQuadraticObjectiveCoefficients(
|
||||
model_proto.objective().quadratic_coefficients());
|
||||
|
||||
// Add linear constraints.
|
||||
storage->AddLinearConstraints(model_proto.linear_constraints());
|
||||
|
||||
// Set the linear constraints coefficients.
|
||||
storage->UpdateLinearConstraintCoefficients(
|
||||
model_proto.linear_constraint_matrix());
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
void ModelStorage::UpdateLinearObjectiveCoefficients(
|
||||
const SparseDoubleVectorProto& coefficients) {
|
||||
for (const auto [var_id, value] : MakeView(coefficients)) {
|
||||
set_linear_objective_coefficient(VariableId(var_id), value);
|
||||
}
|
||||
}
|
||||
|
||||
void ModelStorage::UpdateQuadraticObjectiveCoefficients(
|
||||
const SparseDoubleMatrixProto& coefficients) {
|
||||
for (int i = 0; i < coefficients.row_ids_size(); ++i) {
|
||||
// This call is valid since this is an upper triangular matrix; there is no
|
||||
// duplicated terms.
|
||||
set_quadratic_objective_coefficient(VariableId(coefficients.row_ids(i)),
|
||||
VariableId(coefficients.column_ids(i)),
|
||||
coefficients.coefficients(i));
|
||||
}
|
||||
}
|
||||
|
||||
void ModelStorage::UpdateLinearConstraintCoefficients(
|
||||
const SparseDoubleMatrixProto& coefficients) {
|
||||
for (int i = 0; i < coefficients.row_ids_size(); ++i) {
|
||||
// This call is valid since there are no duplicated pairs.
|
||||
set_linear_constraint_coefficient(
|
||||
LinearConstraintId(coefficients.row_ids(i)),
|
||||
VariableId(coefficients.column_ids(i)), coefficients.coefficients(i));
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<ModelStorage> ModelStorage::Clone() const {
|
||||
absl::StatusOr<std::unique_ptr<ModelStorage>> clone =
|
||||
ModelStorage::FromModelProto(ExportModel());
|
||||
// Unless there is a very serious bug, a model exported by ExportModel()
|
||||
// should always be valid.
|
||||
CHECK_OK(clone.status());
|
||||
|
||||
// Update the next ids so that the clone does not reused any deleted id from
|
||||
// the original.
|
||||
CHECK_LE(clone.value()->next_variable_id_, next_variable_id_);
|
||||
clone.value()->next_variable_id_ = next_variable_id_;
|
||||
CHECK_LE(clone.value()->next_linear_constraint_id_,
|
||||
next_linear_constraint_id_);
|
||||
clone.value()->next_linear_constraint_id_ = next_linear_constraint_id_;
|
||||
|
||||
return std::move(clone).value();
|
||||
}
|
||||
|
||||
VariableId ModelStorage::AddVariable(const double lower_bound,
|
||||
const double upper_bound,
|
||||
const bool is_integer,
|
||||
const absl::string_view name) {
|
||||
const VariableId id = next_variable_id_;
|
||||
AddVariableInternal(/*id=*/id,
|
||||
/*lower_bound=*/lower_bound,
|
||||
/*upper_bound=*/upper_bound,
|
||||
/*is_integer=*/is_integer,
|
||||
/*name=*/name);
|
||||
return id;
|
||||
}
|
||||
|
||||
void ModelStorage::AddVariableInternal(const VariableId id,
|
||||
const double lower_bound,
|
||||
const double upper_bound,
|
||||
const bool is_integer,
|
||||
const absl::string_view name) {
|
||||
CHECK_GE(id, next_variable_id_);
|
||||
next_variable_id_ = id + VariableId(1);
|
||||
|
||||
VariableData& var_data = variables_[id];
|
||||
var_data.lower_bound = lower_bound;
|
||||
var_data.upper_bound = upper_bound;
|
||||
var_data.is_integer = is_integer;
|
||||
var_data.name = std::string(name);
|
||||
if (!lazy_matrix_columns_.empty()) {
|
||||
gtl::InsertOrDie(&lazy_matrix_columns_, id, {});
|
||||
}
|
||||
if (!lazy_quadratic_objective_by_variable_.empty()) {
|
||||
gtl::InsertOrDie(&lazy_quadratic_objective_by_variable_, id, {});
|
||||
}
|
||||
}
|
||||
|
||||
void ModelStorage::AddVariables(const VariablesProto& variables) {
|
||||
const bool has_names = !variables.names().empty();
|
||||
for (int v = 0; v < variables.ids_size(); ++v) {
|
||||
// This call is valid since ids are unique and increasing.
|
||||
AddVariableInternal(VariableId(variables.ids(v)),
|
||||
/*lower_bound=*/variables.lower_bounds(v),
|
||||
/*upper_bound=*/variables.upper_bounds(v),
|
||||
/*is_integer=*/variables.integers(v),
|
||||
has_names ? variables.names(v) : absl::string_view());
|
||||
}
|
||||
}
|
||||
|
||||
void ModelStorage::DeleteVariable(const VariableId id) {
|
||||
CHECK(variables_.contains(id));
|
||||
EnsureLazyMatrixColumns();
|
||||
EnsureLazyMatrixRows();
|
||||
linear_objective_.erase(id);
|
||||
if (id < variables_checkpoint_) {
|
||||
dirty_variable_deletes_.insert(id);
|
||||
dirty_variable_lower_bounds_.erase(id);
|
||||
dirty_variable_upper_bounds_.erase(id);
|
||||
dirty_variable_is_integer_.erase(id);
|
||||
dirty_linear_objective_coefficients_.erase(id);
|
||||
}
|
||||
// If we do not have any quadratic updates to delete, we would like to avoid
|
||||
// initializing the lazy data structures. The updates might tracked in:
|
||||
// 1. dirty_quadratic_objective_coefficients_ (both variables old)
|
||||
// 2. quadratic_objective_ (at least one new variable)
|
||||
// If both maps are empty, we can skip the update and initializiation. Note
|
||||
// that we could be a bit more clever here based on whether the deleted
|
||||
// variable is new or old, but that makes the logic more complex.
|
||||
if (!quadratic_objective_.empty() ||
|
||||
!dirty_quadratic_objective_coefficients_.empty()) {
|
||||
EnsureLazyQuadraticObjective();
|
||||
const auto related_variables =
|
||||
lazy_quadratic_objective_by_variable_.extract(id);
|
||||
for (const VariableId other_id : related_variables.mapped()) {
|
||||
// Due to the extract above, the at lookup will fail if other_id == id.
|
||||
if (id != other_id) {
|
||||
CHECK_GT(lazy_quadratic_objective_by_variable_.at(other_id).erase(id),
|
||||
0);
|
||||
}
|
||||
const auto ordered_pair = internal::MakeOrderedPair(id, other_id);
|
||||
quadratic_objective_.erase(ordered_pair);
|
||||
// We can only have a dirty update to wipe clean if both variables are old
|
||||
if (id < variables_checkpoint_ && other_id < variables_checkpoint_) {
|
||||
dirty_quadratic_objective_coefficients_.erase(ordered_pair);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const LinearConstraintId related_constraint :
|
||||
lazy_matrix_columns_.at(id)) {
|
||||
CHECK_GT(lazy_matrix_rows_.at(related_constraint).erase(id), 0);
|
||||
CHECK_GT(linear_constraint_matrix_.erase({related_constraint, id}), 0);
|
||||
if (id < variables_checkpoint_ &&
|
||||
related_constraint < linear_constraints_checkpoint_) {
|
||||
dirty_linear_constraint_matrix_keys_.erase({related_constraint, id});
|
||||
}
|
||||
}
|
||||
CHECK_GT(lazy_matrix_columns_.erase(id), 0);
|
||||
variables_.erase(id);
|
||||
}
|
||||
|
||||
std::vector<VariableId> ModelStorage::variables() const {
|
||||
return MapKeys(variables_);
|
||||
}
|
||||
|
||||
std::vector<VariableId> ModelStorage::SortedVariables() const {
|
||||
return SortedMapKeys(variables_);
|
||||
}
|
||||
|
||||
LinearConstraintId ModelStorage::AddLinearConstraint(
|
||||
const double lower_bound, const double upper_bound,
|
||||
const absl::string_view name) {
|
||||
const LinearConstraintId id = next_linear_constraint_id_;
|
||||
AddLinearConstraintInternal(/*id=*/id, /*lower_bound=*/lower_bound,
|
||||
/*upper_bound=*/upper_bound,
|
||||
/*name=*/name);
|
||||
return id;
|
||||
}
|
||||
|
||||
void ModelStorage::AddLinearConstraintInternal(const LinearConstraintId id,
|
||||
const double lower_bound,
|
||||
const double upper_bound,
|
||||
const absl::string_view name) {
|
||||
CHECK_GE(id, next_linear_constraint_id_);
|
||||
next_linear_constraint_id_ = id + LinearConstraintId(1);
|
||||
|
||||
LinearConstraintData& lin_con_data = linear_constraints_[id];
|
||||
lin_con_data.lower_bound = lower_bound;
|
||||
lin_con_data.upper_bound = upper_bound;
|
||||
lin_con_data.name = std::string(name);
|
||||
if (!lazy_matrix_rows_.empty()) {
|
||||
gtl::InsertOrDie(&lazy_matrix_rows_, id, {});
|
||||
}
|
||||
}
|
||||
|
||||
void ModelStorage::AddLinearConstraints(
|
||||
const LinearConstraintsProto& linear_constraints) {
|
||||
const bool has_names = !linear_constraints.names().empty();
|
||||
for (int c = 0; c < linear_constraints.ids_size(); ++c) {
|
||||
// This call is valid since ids are unique and increasing.
|
||||
AddLinearConstraintInternal(
|
||||
LinearConstraintId(linear_constraints.ids(c)),
|
||||
/*lower_bound=*/linear_constraints.lower_bounds(c),
|
||||
/*upper_bound=*/linear_constraints.upper_bounds(c),
|
||||
has_names ? linear_constraints.names(c) : absl::string_view());
|
||||
}
|
||||
}
|
||||
|
||||
void ModelStorage::DeleteLinearConstraint(const LinearConstraintId id) {
|
||||
CHECK(linear_constraints_.contains(id));
|
||||
EnsureLazyMatrixColumns();
|
||||
EnsureLazyMatrixRows();
|
||||
linear_constraints_.erase(id);
|
||||
if (id < linear_constraints_checkpoint_) {
|
||||
dirty_linear_constraint_deletes_.insert(id);
|
||||
dirty_linear_constraint_lower_bounds_.erase(id);
|
||||
dirty_linear_constraint_upper_bounds_.erase(id);
|
||||
}
|
||||
for (const VariableId related_variable : lazy_matrix_rows_.at(id)) {
|
||||
CHECK_GT(lazy_matrix_columns_.at(related_variable).erase(id), 0);
|
||||
CHECK_GT(linear_constraint_matrix_.erase({id, related_variable}), 0);
|
||||
if (id < linear_constraints_checkpoint_ &&
|
||||
related_variable < variables_checkpoint_) {
|
||||
dirty_linear_constraint_matrix_keys_.erase({id, related_variable});
|
||||
}
|
||||
}
|
||||
CHECK_GT(lazy_matrix_rows_.erase(id), 0);
|
||||
}
|
||||
|
||||
std::vector<LinearConstraintId> ModelStorage::linear_constraints() const {
|
||||
return MapKeys(linear_constraints_);
|
||||
}
|
||||
|
||||
std::vector<LinearConstraintId> ModelStorage::SortedLinearConstraints() const {
|
||||
return SortedMapKeys(linear_constraints_);
|
||||
}
|
||||
|
||||
std::vector<VariableId> ModelStorage::SortedLinearObjectiveNonzeroVariables()
|
||||
const {
|
||||
return SortedMapKeys(linear_objective_);
|
||||
}
|
||||
|
||||
void ModelStorage::AppendVariable(const VariableId id,
|
||||
VariablesProto& variables_proto) const {
|
||||
const VariableData& var_data = variables_.at(id);
|
||||
variables_proto.add_ids(id.value());
|
||||
variables_proto.add_lower_bounds(var_data.lower_bound);
|
||||
variables_proto.add_upper_bounds(var_data.upper_bound);
|
||||
variables_proto.add_integers(var_data.is_integer);
|
||||
variables_proto.add_names(var_data.name);
|
||||
}
|
||||
|
||||
void ModelStorage::AppendLinearConstraint(
|
||||
const LinearConstraintId id,
|
||||
LinearConstraintsProto& linear_constraints_proto) const {
|
||||
const LinearConstraintData& con_impl = linear_constraints_.at(id);
|
||||
linear_constraints_proto.add_ids(id.value());
|
||||
linear_constraints_proto.add_lower_bounds(con_impl.lower_bound);
|
||||
linear_constraints_proto.add_upper_bounds(con_impl.upper_bound);
|
||||
linear_constraints_proto.add_names(con_impl.name);
|
||||
}
|
||||
|
||||
ModelProto ModelStorage::ExportModel() const {
|
||||
ModelProto result;
|
||||
result.set_name(name_);
|
||||
// Export the variables.
|
||||
for (const VariableId variable : SortedMapKeys(variables_)) {
|
||||
AppendVariable(variable, *result.mutable_variables());
|
||||
}
|
||||
|
||||
// Pull out the objective.
|
||||
result.mutable_objective()->set_maximize(is_maximize_);
|
||||
result.mutable_objective()->set_offset(objective_offset_);
|
||||
AppendFromMapOrDefault<VariableId>(
|
||||
SortedMapKeys(linear_objective_), linear_objective_,
|
||||
*result.mutable_objective()->mutable_linear_coefficients());
|
||||
*result.mutable_objective()->mutable_quadratic_coefficients() =
|
||||
ExportMatrix(quadratic_objective_, SortedMapKeys(quadratic_objective_));
|
||||
|
||||
// Pull out the linear constraints.
|
||||
for (const LinearConstraintId con : SortedMapKeys(linear_constraints_)) {
|
||||
AppendLinearConstraint(con, *result.mutable_linear_constraints());
|
||||
}
|
||||
|
||||
// Pull out the constraint matrix.
|
||||
*result.mutable_linear_constraint_matrix() =
|
||||
ExportMatrix<LinearConstraintId, VariableId>(
|
||||
linear_constraint_matrix_, SortedMapKeys(linear_constraint_matrix_));
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<ModelUpdateProto> ModelStorage::ExportSharedModelUpdate() {
|
||||
// We must detect the empty case to prevent unneeded copies and merging in
|
||||
// ExportModelUpdate().
|
||||
if (variables_checkpoint_ == next_variable_id_ &&
|
||||
linear_constraints_checkpoint_ == next_linear_constraint_id_ &&
|
||||
!dirty_objective_direction_ && !dirty_objective_offset_ &&
|
||||
dirty_variable_deletes_.empty() && dirty_variable_lower_bounds_.empty() &&
|
||||
dirty_variable_upper_bounds_.empty() &&
|
||||
dirty_variable_is_integer_.empty() &&
|
||||
dirty_linear_objective_coefficients_.empty() &&
|
||||
dirty_quadratic_objective_coefficients_.empty() &&
|
||||
dirty_linear_constraint_deletes_.empty() &&
|
||||
dirty_linear_constraint_lower_bounds_.empty() &&
|
||||
dirty_linear_constraint_upper_bounds_.empty() &&
|
||||
dirty_linear_constraint_matrix_keys_.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// TODO(b/185608026): these are used to efficiently extract the constraint
|
||||
// matrix update, but it would be good to avoid calling these because they
|
||||
// result in a large allocation.
|
||||
EnsureLazyMatrixRows();
|
||||
EnsureLazyMatrixColumns();
|
||||
|
||||
ModelUpdateProto result;
|
||||
|
||||
// Variable/constraint deletions.
|
||||
for (const VariableId del_var : SortedSetKeys(dirty_variable_deletes_)) {
|
||||
result.add_deleted_variable_ids(del_var.value());
|
||||
}
|
||||
for (const LinearConstraintId del_lin_con :
|
||||
SortedSetKeys(dirty_linear_constraint_deletes_)) {
|
||||
result.add_deleted_linear_constraint_ids(del_lin_con.value());
|
||||
}
|
||||
|
||||
// Update the variables.
|
||||
auto var_updates = result.mutable_variable_updates();
|
||||
AppendFromMap(dirty_variable_lower_bounds_, variables_,
|
||||
&VariableData::lower_bound,
|
||||
*var_updates->mutable_lower_bounds());
|
||||
AppendFromMap(dirty_variable_upper_bounds_, variables_,
|
||||
&VariableData::upper_bound,
|
||||
*var_updates->mutable_upper_bounds());
|
||||
|
||||
for (const VariableId integer_var :
|
||||
SortedSetKeys(dirty_variable_is_integer_)) {
|
||||
var_updates->mutable_integers()->add_ids(integer_var.value());
|
||||
var_updates->mutable_integers()->add_values(
|
||||
variables_.at(integer_var).is_integer);
|
||||
}
|
||||
for (VariableId new_id = variables_checkpoint_; new_id < next_variable_id_;
|
||||
++new_id) {
|
||||
if (variables_.contains(new_id)) {
|
||||
AppendVariable(new_id, *result.mutable_new_variables());
|
||||
}
|
||||
}
|
||||
|
||||
// Update the objective
|
||||
auto obj_updates = result.mutable_objective_updates();
|
||||
if (dirty_objective_direction_) {
|
||||
obj_updates->set_direction_update(is_maximize_);
|
||||
}
|
||||
if (dirty_objective_offset_) {
|
||||
obj_updates->set_offset_update(objective_offset_);
|
||||
}
|
||||
AppendFromMapOrDefault<VariableId>(
|
||||
SortedSetKeys(dirty_linear_objective_coefficients_), linear_objective_,
|
||||
*obj_updates->mutable_linear_coefficients());
|
||||
// TODO(b/182567749): Once StrongInt is in absl, use
|
||||
// AppendFromMapIfPresent<VariableId>(
|
||||
// MakeStrongIntRange(variables_checkpoint_, next_variable_id_),
|
||||
// linear_objective_, *obj_updates->mutable_linear_coefficients());
|
||||
for (VariableId var_id = variables_checkpoint_; var_id < next_variable_id_;
|
||||
++var_id) {
|
||||
const double* const double_value =
|
||||
gtl::FindOrNull(linear_objective_, var_id);
|
||||
if (double_value != nullptr) {
|
||||
obj_updates->mutable_linear_coefficients()->add_ids(var_id.value());
|
||||
obj_updates->mutable_linear_coefficients()->add_values(*double_value);
|
||||
}
|
||||
}
|
||||
// If we do not have any quadratic updates to push, we would like to avoid
|
||||
// initializing the lazy data structures. The updates might tracked in:
|
||||
// 1. dirty_quadratic_objective_coefficients_ (both variables old)
|
||||
// 2. quadratic_objective_ (at least one new variable)
|
||||
// If both maps are empty, we can skip the update and initializiation.
|
||||
if (!quadratic_objective_.empty() ||
|
||||
!dirty_quadratic_objective_coefficients_.empty()) {
|
||||
EnsureLazyQuadraticObjective();
|
||||
// NOTE: dirty_quadratic_objective_coefficients_ only tracks terms where
|
||||
// both variables are "old".
|
||||
std::vector<std::pair<VariableId, VariableId>> quadratic_objective_updates(
|
||||
dirty_quadratic_objective_coefficients_.begin(),
|
||||
dirty_quadratic_objective_coefficients_.end());
|
||||
// Now, we loop through the "new" variables and track updates involving
|
||||
// them. We need to look out for two things:
|
||||
// * The "other" variable in the term can either be new or old.
|
||||
// * We cannot doubly insert terms when both variables are new.
|
||||
// Note that this traversal is doing at most twice as much work as
|
||||
// necessary.
|
||||
for (VariableId new_var = variables_checkpoint_;
|
||||
new_var < next_variable_id_; ++new_var) {
|
||||
if (variables_.contains(new_var)) {
|
||||
for (const VariableId other_var :
|
||||
lazy_quadratic_objective_by_variable_.at(new_var)) {
|
||||
if (other_var <= new_var) {
|
||||
quadratic_objective_updates.push_back(
|
||||
internal::MakeOrderedPair(new_var, other_var));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(quadratic_objective_updates.begin(),
|
||||
quadratic_objective_updates.end());
|
||||
*result.mutable_objective_updates()->mutable_quadratic_coefficients() =
|
||||
ExportMatrix(quadratic_objective_, quadratic_objective_updates);
|
||||
}
|
||||
|
||||
// Update the linear constraints
|
||||
auto lin_con_updates = result.mutable_linear_constraint_updates();
|
||||
AppendFromMap(dirty_linear_constraint_lower_bounds_, linear_constraints_,
|
||||
&LinearConstraintData::lower_bound,
|
||||
*lin_con_updates->mutable_lower_bounds());
|
||||
AppendFromMap(dirty_linear_constraint_upper_bounds_, linear_constraints_,
|
||||
&LinearConstraintData::upper_bound,
|
||||
*lin_con_updates->mutable_upper_bounds());
|
||||
|
||||
for (LinearConstraintId new_id = linear_constraints_checkpoint_;
|
||||
new_id < next_linear_constraint_id_; ++new_id) {
|
||||
if (linear_constraints_.contains(new_id)) {
|
||||
AppendLinearConstraint(new_id, *result.mutable_new_linear_constraints());
|
||||
}
|
||||
}
|
||||
|
||||
// Extract changes to the matrix of linear constraint coefficients
|
||||
std::vector<std::pair<LinearConstraintId, VariableId>>
|
||||
constraint_matrix_updates(dirty_linear_constraint_matrix_keys_.begin(),
|
||||
dirty_linear_constraint_matrix_keys_.end());
|
||||
for (VariableId new_var = variables_checkpoint_; new_var < next_variable_id_;
|
||||
++new_var) {
|
||||
if (variables_.contains(new_var)) {
|
||||
for (const LinearConstraintId lin_con :
|
||||
lazy_matrix_columns_.at(new_var)) {
|
||||
constraint_matrix_updates.emplace_back(lin_con, new_var);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (LinearConstraintId new_lin_con = linear_constraints_checkpoint_;
|
||||
new_lin_con < next_linear_constraint_id_; ++new_lin_con) {
|
||||
if (linear_constraints_.contains(new_lin_con)) {
|
||||
for (const VariableId var : lazy_matrix_rows_.at(new_lin_con)) {
|
||||
// NOTE(user): we will do at most twice as much as needed here.
|
||||
if (var < variables_checkpoint_) {
|
||||
constraint_matrix_updates.emplace_back(new_lin_con, var);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(constraint_matrix_updates.begin(), constraint_matrix_updates.end());
|
||||
*result.mutable_linear_constraint_matrix_updates() =
|
||||
ExportMatrix(linear_constraint_matrix_, constraint_matrix_updates);
|
||||
|
||||
// Named returned value optimization (NRVO) does not apply here since the
|
||||
// return type if not the same type as `result`. To make things clear, we
|
||||
// explicitly call the constructor here.
|
||||
return {std::move(result)};
|
||||
}
|
||||
|
||||
void ModelStorage::EnsureLazyMatrixColumns() {
|
||||
if (lazy_matrix_columns_.empty()) {
|
||||
for (const auto& var_pair : variables_) {
|
||||
lazy_matrix_columns_.insert({var_pair.first, {}});
|
||||
}
|
||||
for (const auto& mat_entry : linear_constraint_matrix_) {
|
||||
lazy_matrix_columns_.at(mat_entry.first.second)
|
||||
.insert(mat_entry.first.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModelStorage::EnsureLazyMatrixRows() {
|
||||
if (lazy_matrix_rows_.empty()) {
|
||||
for (const auto& lin_con_pair : linear_constraints_) {
|
||||
lazy_matrix_rows_.insert({lin_con_pair.first, {}});
|
||||
}
|
||||
for (const auto& mat_entry : linear_constraint_matrix_) {
|
||||
lazy_matrix_rows_.at(mat_entry.first.first)
|
||||
.insert(mat_entry.first.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModelStorage::EnsureLazyQuadraticObjective() {
|
||||
if (lazy_quadratic_objective_by_variable_.empty()) {
|
||||
for (const auto& [var, data] : variables_) {
|
||||
lazy_quadratic_objective_by_variable_.insert({var, {}});
|
||||
}
|
||||
for (const auto& [vars, coeff] : quadratic_objective_) {
|
||||
lazy_quadratic_objective_by_variable_.at(vars.first).insert(vars.second);
|
||||
lazy_quadratic_objective_by_variable_.at(vars.second).insert(vars.first);
|
||||
}
|
||||
for (const auto& vars : dirty_quadratic_objective_coefficients_) {
|
||||
lazy_quadratic_objective_by_variable_.at(vars.first).insert(vars.second);
|
||||
lazy_quadratic_objective_by_variable_.at(vars.second).insert(vars.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModelStorage::SharedCheckpoint() {
|
||||
variables_checkpoint_ = next_variable_id_;
|
||||
linear_constraints_checkpoint_ = next_linear_constraint_id_;
|
||||
dirty_objective_direction_ = false;
|
||||
dirty_objective_offset_ = false;
|
||||
|
||||
dirty_variable_deletes_.clear();
|
||||
dirty_variable_lower_bounds_.clear();
|
||||
dirty_variable_upper_bounds_.clear();
|
||||
dirty_variable_is_integer_.clear();
|
||||
|
||||
dirty_linear_objective_coefficients_.clear();
|
||||
dirty_quadratic_objective_coefficients_.clear();
|
||||
|
||||
dirty_linear_constraint_deletes_.clear();
|
||||
dirty_linear_constraint_lower_bounds_.clear();
|
||||
dirty_linear_constraint_upper_bounds_.clear();
|
||||
dirty_linear_constraint_matrix_keys_.clear();
|
||||
}
|
||||
|
||||
UpdateTrackerId ModelStorage::NewUpdateTracker() {
|
||||
const absl::MutexLock lock(&update_trackers_lock_);
|
||||
|
||||
const UpdateTrackerId update_tracker = next_update_tracker_;
|
||||
++next_update_tracker_;
|
||||
|
||||
CHECK(update_trackers_
|
||||
.try_emplace(update_tracker, std::make_unique<UpdateTrackerData>())
|
||||
.second);
|
||||
|
||||
CheckpointLocked(update_tracker);
|
||||
|
||||
return update_tracker;
|
||||
}
|
||||
|
||||
void ModelStorage::DeleteUpdateTracker(const UpdateTrackerId update_tracker) {
|
||||
const absl::MutexLock lock(&update_trackers_lock_);
|
||||
const auto found = update_trackers_.find(update_tracker);
|
||||
CHECK(found != update_trackers_.end())
|
||||
<< "Update tracker " << update_tracker << " does not exist";
|
||||
update_trackers_.erase(found);
|
||||
}
|
||||
|
||||
std::optional<ModelUpdateProto> ModelStorage::ExportModelUpdate(
|
||||
const UpdateTrackerId update_tracker) {
|
||||
const absl::MutexLock lock(&update_trackers_lock_);
|
||||
|
||||
const auto found_data = update_trackers_.find(update_tracker);
|
||||
CHECK(found_data != update_trackers_.end())
|
||||
<< "Update tracker " << update_tracker << " does not exist";
|
||||
const std::unique_ptr<UpdateTrackerData>& data = found_data->second;
|
||||
|
||||
// No updates have been pushed, the checkpoint of this tracker is in sync with
|
||||
// the shared checkpoint of ModelStorage. We can return the ModelStorage
|
||||
// shared update without merging.
|
||||
if (data->updates.empty()) {
|
||||
return ExportSharedModelUpdate();
|
||||
}
|
||||
|
||||
// Find all trackers with the same checkpoint. By construction, all trackers
|
||||
// that have the same first update also share all next updates.
|
||||
std::vector<UpdateTrackerData*> all_trackers_at_checkpoint;
|
||||
for (const auto& [other_id, other_data] : update_trackers_) {
|
||||
if (!other_data->updates.empty() &&
|
||||
other_data->updates.front() == data->updates.front()) {
|
||||
all_trackers_at_checkpoint.push_back(other_data.get());
|
||||
|
||||
// Validate that we have the same updates in debug mode only. In optimized
|
||||
// mode, only test the size of the updates vectors.
|
||||
CHECK_EQ(data->updates.size(), other_data->updates.size());
|
||||
if (DEBUG_MODE) {
|
||||
for (int i = 0; i < data->updates.size(); ++i) {
|
||||
CHECK_EQ(data->updates[i], other_data->updates[i])
|
||||
<< "Two trackers have the same checkpoint but different updates.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Possible optimizations here:
|
||||
//
|
||||
// * Maybe optimize the case where the first update is singly used by `this`
|
||||
// and use it as starting point instead of making a copy. This may be more
|
||||
// complicated if it is shared with multiple trackers since in that case we
|
||||
// must make sure to only update the shared instance if and only if only
|
||||
// trackers have a pointer to it, not external code (i.e. its use count is
|
||||
// the same as the number of trackers).
|
||||
//
|
||||
// * Use n-way merge here if the performances justify it.
|
||||
const auto merge = std::make_shared<ModelUpdateProto>();
|
||||
for (const auto& update : data->updates) {
|
||||
MergeIntoUpdate(/*from=*/*update, /*into=*/*merge);
|
||||
}
|
||||
|
||||
// Push the merge to all trackers that have the same checkpoint (including
|
||||
// this tracker).
|
||||
for (UpdateTrackerData* const other_data : all_trackers_at_checkpoint) {
|
||||
other_data->updates.clear();
|
||||
other_data->updates.push_back(merge);
|
||||
}
|
||||
|
||||
ModelUpdateProto update = *merge;
|
||||
const std::optional<ModelUpdateProto> pending_update =
|
||||
ExportSharedModelUpdate();
|
||||
if (pending_update) {
|
||||
MergeIntoUpdate(/*from=*/*pending_update, /*into=*/update);
|
||||
}
|
||||
|
||||
// Named returned value optimization (NRVO) does not apply here since the
|
||||
// return type if not the same type as `result`. To make things clear, we
|
||||
// explicitly call the constructor here.
|
||||
return {std::move(update)};
|
||||
}
|
||||
|
||||
void ModelStorage::Checkpoint(const UpdateTrackerId update_tracker) {
|
||||
const absl::MutexLock lock(&update_trackers_lock_);
|
||||
|
||||
CheckpointLocked(update_tracker);
|
||||
}
|
||||
|
||||
void ModelStorage::CheckpointLocked(const UpdateTrackerId update_tracker) {
|
||||
const auto found_data = update_trackers_.find(update_tracker);
|
||||
CHECK(found_data != update_trackers_.end())
|
||||
<< "Update tracker " << update_tracker << " does not exist";
|
||||
const std::unique_ptr<UpdateTrackerData>& data = found_data->second;
|
||||
|
||||
// Optimize the case where we have a single tracker and we don't want to
|
||||
// update it. In that case we don't need to update trackers since we would
|
||||
// only update this one and clear it immediately.
|
||||
if (update_trackers_.size() > 1) {
|
||||
std::optional<ModelUpdateProto> update = ExportSharedModelUpdate();
|
||||
if (update) {
|
||||
const auto shared_update =
|
||||
std::make_shared<ModelUpdateProto>(*std::move(update));
|
||||
|
||||
for (const auto& [other_id, other_data] : update_trackers_) {
|
||||
other_data->updates.push_back(shared_update);
|
||||
}
|
||||
}
|
||||
}
|
||||
SharedCheckpoint();
|
||||
data->updates.clear();
|
||||
}
|
||||
|
||||
absl::Status ModelStorage::ApplyUpdateProto(
|
||||
const ModelUpdateProto& update_proto) {
|
||||
// Check the update first.
|
||||
{
|
||||
ModelSummary summary;
|
||||
// We have to use sorted keys since IdNameBiMap expect Insert() to be called
|
||||
// in sorted order.
|
||||
for (const auto id : SortedVariables()) {
|
||||
summary.variables.Insert(id.value(), variable_name(id));
|
||||
}
|
||||
summary.variables.SetNextFreeId(next_variable_id_.value());
|
||||
for (const auto id : SortedLinearConstraints()) {
|
||||
summary.linear_constraints.Insert(id.value(), linear_constraint_name(id));
|
||||
}
|
||||
summary.linear_constraints.SetNextFreeId(
|
||||
next_linear_constraint_id_.value());
|
||||
// We don't check the names for the same reason as in FromModelProto().
|
||||
RETURN_IF_ERROR(ValidateModelUpdateAndSummary(update_proto, summary,
|
||||
/*check_names=*/false));
|
||||
}
|
||||
|
||||
// Remove deleted variables and constraints.
|
||||
for (const int64_t v_id : update_proto.deleted_variable_ids()) {
|
||||
DeleteVariable(VariableId(v_id));
|
||||
}
|
||||
for (const int64_t c_id : update_proto.deleted_linear_constraint_ids()) {
|
||||
DeleteLinearConstraint(LinearConstraintId(c_id));
|
||||
}
|
||||
|
||||
// Update existing variables' properties.
|
||||
for (const auto [v_id, lb] :
|
||||
MakeView(update_proto.variable_updates().lower_bounds())) {
|
||||
set_variable_lower_bound(VariableId(v_id), lb);
|
||||
}
|
||||
for (const auto [v_id, ub] :
|
||||
MakeView(update_proto.variable_updates().upper_bounds())) {
|
||||
set_variable_upper_bound(VariableId(v_id), ub);
|
||||
}
|
||||
for (const auto [v_id, is_integer] :
|
||||
MakeView(update_proto.variable_updates().integers())) {
|
||||
set_variable_is_integer(VariableId(v_id), is_integer);
|
||||
}
|
||||
|
||||
// Update existing constraints' properties.
|
||||
for (const auto [c_id, lb] :
|
||||
MakeView(update_proto.linear_constraint_updates().lower_bounds())) {
|
||||
set_linear_constraint_lower_bound(LinearConstraintId(c_id), lb);
|
||||
}
|
||||
for (const auto [c_id, ub] :
|
||||
MakeView(update_proto.linear_constraint_updates().upper_bounds())) {
|
||||
set_linear_constraint_upper_bound(LinearConstraintId(c_id), ub);
|
||||
}
|
||||
|
||||
// Add the new variables and constraints.
|
||||
AddVariables(update_proto.new_variables());
|
||||
AddLinearConstraints(update_proto.new_linear_constraints());
|
||||
|
||||
// Update the objective.
|
||||
if (update_proto.objective_updates().has_direction_update()) {
|
||||
set_is_maximize(update_proto.objective_updates().direction_update());
|
||||
}
|
||||
if (update_proto.objective_updates().has_offset_update()) {
|
||||
set_objective_offset(update_proto.objective_updates().offset_update());
|
||||
}
|
||||
UpdateLinearObjectiveCoefficients(
|
||||
update_proto.objective_updates().linear_coefficients());
|
||||
UpdateQuadraticObjectiveCoefficients(
|
||||
update_proto.objective_updates().quadratic_coefficients());
|
||||
|
||||
// Update the linear constraints' coefficients.
|
||||
UpdateLinearConstraintCoefficients(
|
||||
update_proto.linear_constraint_matrix_updates());
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,11 @@ namespace math_opt {
|
||||
//
|
||||
// The following invariants are enforced:
|
||||
// * Ids must be unique and increasing (in insertion order).
|
||||
// * Ids are non-negative.
|
||||
// * Ids are not equal to std::numeric_limits<int64_t>::max()
|
||||
// TODO(b/213918209): make sure this is enforced in validators or remove this
|
||||
// restriction.
|
||||
// * Ids removed are never reused.
|
||||
// * Names must be either empty or unique.
|
||||
class IdNameBiMap {
|
||||
public:
|
||||
@@ -44,18 +49,30 @@ class IdNameBiMap {
|
||||
// validation code.
|
||||
IdNameBiMap(std::initializer_list<std::pair<int64_t, absl::string_view>> ids);
|
||||
|
||||
// Will CHECK fail if id is <= largest_id().
|
||||
// Will CHECK fail if id is present or if name is nonempty and present.
|
||||
// Inserts the provided id and associate the provided name to it. CHECKs that
|
||||
// id >= next_free_id() and that when the name is nonempty it is not already
|
||||
// present. As a side effect it updates next_free_id to id + 1.
|
||||
inline void Insert(int64_t id, std::string name);
|
||||
|
||||
// Will CHECK fail if id is not present.
|
||||
// Removes the given id. CHECKs that it is present.
|
||||
inline void Erase(int64_t id);
|
||||
|
||||
inline bool HasId(int64_t id) const;
|
||||
inline bool HasName(absl::string_view name) const;
|
||||
inline bool Empty() const;
|
||||
inline int Size() const;
|
||||
inline int64_t LargestId() const;
|
||||
|
||||
// The next id that has never been used (0 initially since ids are
|
||||
// non-negative).
|
||||
inline int64_t next_free_id() const;
|
||||
|
||||
// Updates next_free_id(). CHECKs that the provided id is greater than any
|
||||
// exiting id and non negative.
|
||||
//
|
||||
// In practice this should only be used to increase the next_free_id() value
|
||||
// in cases where a ModelSummary is built with an existing model but we know
|
||||
// some ids of removed elements have already been used.
|
||||
inline void SetNextFreeId(int64_t new_next_free_id);
|
||||
|
||||
// Iteration order is in increasing id order.
|
||||
const gtl::linked_hash_map<int64_t, std::string>& id_to_name() const {
|
||||
@@ -67,6 +84,9 @@ class IdNameBiMap {
|
||||
}
|
||||
|
||||
private:
|
||||
// Next unused id.
|
||||
int64_t next_free_id_ = 0;
|
||||
|
||||
// Pointer stability for name strings and iterating in insertion order are
|
||||
// both needed (so we do not use flat_hash_map).
|
||||
gtl::linked_hash_map<int64_t, std::string> id_to_name_;
|
||||
@@ -83,9 +103,14 @@ struct ModelSummary {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void IdNameBiMap::Insert(const int64_t id, std::string name) {
|
||||
if (!id_to_name_.empty()) {
|
||||
CHECK_GT(id, LargestId()) << name;
|
||||
}
|
||||
CHECK_GE(id, next_free_id_);
|
||||
// TODO(b/213918209): this is not mandatory for a valid model at this point so
|
||||
// this is a bit incorrect. The correct thing to do would be to have an
|
||||
// optional<int64_t> for the next_free_id_ and forbid any new id when we reach
|
||||
// the max but this may be overkill.
|
||||
CHECK_LT(id, std::numeric_limits<int64_t>::max());
|
||||
next_free_id_ = id + 1;
|
||||
|
||||
const auto [it, success] = id_to_name_.emplace(id, std::move(name));
|
||||
CHECK(success) << "id: " << id;
|
||||
const absl::string_view name_view(it->second);
|
||||
@@ -116,9 +141,16 @@ bool IdNameBiMap::Empty() const { return id_to_name_.empty(); }
|
||||
|
||||
int IdNameBiMap::Size() const { return id_to_name_.size(); }
|
||||
|
||||
int64_t IdNameBiMap::LargestId() const {
|
||||
CHECK(!Empty());
|
||||
return id_to_name_.back().first;
|
||||
int64_t IdNameBiMap::next_free_id() const { return next_free_id_; }
|
||||
|
||||
void IdNameBiMap::SetNextFreeId(const int64_t new_next_free_id) {
|
||||
if (!Empty()) {
|
||||
const int64_t largest_id = id_to_name_.back().first;
|
||||
CHECK_GT(new_next_free_id, largest_id);
|
||||
} else {
|
||||
CHECK_GE(new_next_free_id, 0);
|
||||
}
|
||||
next_free_id_ = new_next_free_id;
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "ortools/base/integral_types.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_update.pb.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
@@ -27,167 +30,327 @@
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
void MergeIntoUpdate(const ModelUpdateProto& from, ModelUpdateProto& into) {
|
||||
internal::MergeIntoSortedIds(from.deleted_variable_ids(),
|
||||
*into.mutable_deleted_variable_ids());
|
||||
internal::MergeIntoSortedIds(from.deleted_linear_constraint_ids(),
|
||||
*into.mutable_deleted_linear_constraint_ids());
|
||||
void MergeIntoUpdate(const ModelUpdateProto& from_new,
|
||||
ModelUpdateProto& into_old) {
|
||||
// Merge the deleted variables. Note that we remove from the merge the
|
||||
// variables that were created in `into_old`. Below we will simply remove
|
||||
// those variables from the list of new variables in the merge; thus making
|
||||
// the update as if those variables never existed.
|
||||
internal::MergeIntoSortedIds(from_new.deleted_variable_ids(),
|
||||
*into_old.mutable_deleted_variable_ids(),
|
||||
/*deleted=*/into_old.new_variables().ids());
|
||||
internal::MergeIntoSortedIds(
|
||||
from_new.deleted_linear_constraint_ids(),
|
||||
*into_old.mutable_deleted_linear_constraint_ids(),
|
||||
/*deleted=*/into_old.new_linear_constraints().ids());
|
||||
|
||||
internal::MergeIntoSparseVector(
|
||||
from.variable_updates().lower_bounds(),
|
||||
*into.mutable_variable_updates()->mutable_lower_bounds());
|
||||
internal::MergeIntoSparseVector(
|
||||
from.variable_updates().upper_bounds(),
|
||||
*into.mutable_variable_updates()->mutable_upper_bounds());
|
||||
internal::MergeIntoSparseVector(
|
||||
from.variable_updates().integers(),
|
||||
*into.mutable_variable_updates()->mutable_integers());
|
||||
// For variables and linear constraints updates, we want to ignore updates of:
|
||||
//
|
||||
// 1. variable or linear constraints deleted in `from_new` (that could have
|
||||
// been updated in `into_old`).
|
||||
//
|
||||
// 2. variable or linear constraints created in `into_old`. For those the code
|
||||
// of UpdateNewElementProperty() will use the new value directly as the
|
||||
// value of the created variable.
|
||||
//
|
||||
// Thus we create here the list of indices to ignore when filtering updates
|
||||
// for both variables and linear constraints.
|
||||
google::protobuf::RepeatedField<int64_t> from_deleted_and_into_new_variable_ids =
|
||||
from_new.deleted_variable_ids();
|
||||
from_deleted_and_into_new_variable_ids.MergeFrom(
|
||||
into_old.new_variables().ids());
|
||||
|
||||
internal::MergeIntoSparseVector(
|
||||
from.linear_constraint_updates().lower_bounds(),
|
||||
*into.mutable_linear_constraint_updates()->mutable_lower_bounds());
|
||||
internal::MergeIntoSparseVector(
|
||||
from.linear_constraint_updates().upper_bounds(),
|
||||
*into.mutable_linear_constraint_updates()->mutable_upper_bounds());
|
||||
google::protobuf::RepeatedField<int64_t>
|
||||
from_deleted_and_into_new_linear_constraint_ids =
|
||||
from_new.deleted_linear_constraint_ids();
|
||||
from_deleted_and_into_new_linear_constraint_ids.MergeFrom(
|
||||
into_old.new_linear_constraints().ids());
|
||||
|
||||
if (!from.new_variables().ids().empty() &&
|
||||
!into.new_variables().ids().empty()) {
|
||||
CHECK_GT(*from.new_variables().ids().begin(),
|
||||
*into.new_variables().ids().rbegin());
|
||||
// Merge updates of variable properties.
|
||||
internal::MergeIntoSparseVector(
|
||||
from_new.variable_updates().lower_bounds(),
|
||||
*into_old.mutable_variable_updates()->mutable_lower_bounds(),
|
||||
from_deleted_and_into_new_variable_ids);
|
||||
internal::MergeIntoSparseVector(
|
||||
from_new.variable_updates().upper_bounds(),
|
||||
*into_old.mutable_variable_updates()->mutable_upper_bounds(),
|
||||
from_deleted_and_into_new_variable_ids);
|
||||
internal::MergeIntoSparseVector(
|
||||
from_new.variable_updates().integers(),
|
||||
*into_old.mutable_variable_updates()->mutable_integers(),
|
||||
from_deleted_and_into_new_variable_ids);
|
||||
|
||||
// Merge updates of linear constraints properties.
|
||||
internal::MergeIntoSparseVector(
|
||||
from_new.linear_constraint_updates().lower_bounds(),
|
||||
*into_old.mutable_linear_constraint_updates()->mutable_lower_bounds(),
|
||||
from_deleted_and_into_new_linear_constraint_ids);
|
||||
internal::MergeIntoSparseVector(
|
||||
from_new.linear_constraint_updates().upper_bounds(),
|
||||
*into_old.mutable_linear_constraint_updates()->mutable_upper_bounds(),
|
||||
from_deleted_and_into_new_linear_constraint_ids);
|
||||
|
||||
// Merge new variables.
|
||||
//
|
||||
// The merge occurs in two steps:
|
||||
//
|
||||
// 1. For each property we remove from the merge the new variables from
|
||||
// `into_old` that are removed in `from_new` since those don't have to
|
||||
// exist. The code above has removed those from the deleted set to).
|
||||
//
|
||||
// We also update the value of the property to the one of its update in
|
||||
// `from_new` if it exists. The code above has removed those updates
|
||||
// already.
|
||||
//
|
||||
// 2. We append all new variables of `from_new` at once by using MergeFrom()
|
||||
// on the VariablesProto. No merges are needed for those since they can't
|
||||
// have been know by `into_old`.
|
||||
if (!from_new.new_variables().ids().empty() &&
|
||||
!into_old.new_variables().ids().empty()) {
|
||||
CHECK_GT(*from_new.new_variables().ids().begin(),
|
||||
*into_old.new_variables().ids().rbegin());
|
||||
}
|
||||
into.mutable_new_variables()->MergeFrom(from.new_variables());
|
||||
internal::UpdateNewElementProperty(
|
||||
/*ids=*/into_old.new_variables().ids(),
|
||||
/*values=*/*into_old.mutable_new_variables()->mutable_lower_bounds(),
|
||||
/*deleted=*/from_new.deleted_variable_ids(),
|
||||
/*updates=*/from_new.variable_updates().lower_bounds());
|
||||
internal::UpdateNewElementProperty(
|
||||
/*ids=*/into_old.new_variables().ids(),
|
||||
/*values=*/*into_old.mutable_new_variables()->mutable_upper_bounds(),
|
||||
/*deleted=*/from_new.deleted_variable_ids(),
|
||||
/*updates=*/from_new.variable_updates().upper_bounds());
|
||||
internal::UpdateNewElementProperty(
|
||||
/*ids=*/into_old.new_variables().ids(),
|
||||
/*values=*/*into_old.mutable_new_variables()->mutable_integers(),
|
||||
/*deleted=*/from_new.deleted_variable_ids(),
|
||||
/*updates=*/from_new.variable_updates().integers());
|
||||
internal::UpdateNewElementProperty(
|
||||
/*ids=*/into_old.new_variables().ids(),
|
||||
/*values=*/*into_old.mutable_new_variables()->mutable_names(),
|
||||
/*deleted=*/from_new.deleted_variable_ids(),
|
||||
// We use an empty view here since names can't be updated.
|
||||
/*updates=*/SparseVectorView<std::string>());
|
||||
internal::RemoveDeletedIds(
|
||||
/*ids=*/*into_old.mutable_new_variables()->mutable_ids(),
|
||||
/*deleted=*/from_new.deleted_variable_ids());
|
||||
into_old.mutable_new_variables()->MergeFrom(from_new.new_variables());
|
||||
|
||||
if (!from.new_linear_constraints().ids().empty() &&
|
||||
!into.new_linear_constraints().ids().empty()) {
|
||||
CHECK_GT(*from.new_linear_constraints().ids().begin(),
|
||||
*into.new_linear_constraints().ids().rbegin());
|
||||
// Merge of new linear constraints. The algorithm is similar to variables; see
|
||||
// comment above for details.
|
||||
if (!from_new.new_linear_constraints().ids().empty() &&
|
||||
!into_old.new_linear_constraints().ids().empty()) {
|
||||
CHECK_GT(*from_new.new_linear_constraints().ids().begin(),
|
||||
*into_old.new_linear_constraints().ids().rbegin());
|
||||
}
|
||||
into.mutable_new_linear_constraints()->MergeFrom(
|
||||
from.new_linear_constraints());
|
||||
internal::UpdateNewElementProperty(
|
||||
/*ids=*/into_old.new_linear_constraints().ids(),
|
||||
/*values=*/
|
||||
*into_old.mutable_new_linear_constraints()->mutable_lower_bounds(),
|
||||
/*deleted=*/from_new.deleted_linear_constraint_ids(),
|
||||
/*updates=*/from_new.linear_constraint_updates().lower_bounds());
|
||||
internal::UpdateNewElementProperty(
|
||||
/*ids=*/into_old.new_linear_constraints().ids(),
|
||||
/*values=*/
|
||||
*into_old.mutable_new_linear_constraints()->mutable_upper_bounds(),
|
||||
/*deleted=*/from_new.deleted_linear_constraint_ids(),
|
||||
/*updates=*/from_new.linear_constraint_updates().upper_bounds());
|
||||
internal::UpdateNewElementProperty(
|
||||
/*ids=*/into_old.new_linear_constraints().ids(),
|
||||
/*values=*/*into_old.mutable_new_linear_constraints()->mutable_names(),
|
||||
/*deleted=*/from_new.deleted_linear_constraint_ids(),
|
||||
// We use an empty view here since names can't be updated.
|
||||
/*updates=*/SparseVectorView<std::string>());
|
||||
internal::RemoveDeletedIds(
|
||||
/*ids=*/*into_old.mutable_new_linear_constraints()->mutable_ids(),
|
||||
/*deleted=*/from_new.deleted_linear_constraint_ids());
|
||||
into_old.mutable_new_linear_constraints()->MergeFrom(
|
||||
from_new.new_linear_constraints());
|
||||
|
||||
if (from.objective_updates().has_direction_update()) {
|
||||
into.mutable_objective_updates()->set_direction_update(
|
||||
from.objective_updates().direction_update());
|
||||
// Merge the objective.
|
||||
if (from_new.objective_updates().has_direction_update()) {
|
||||
into_old.mutable_objective_updates()->set_direction_update(
|
||||
from_new.objective_updates().direction_update());
|
||||
}
|
||||
if (from.objective_updates().has_offset_update()) {
|
||||
into.mutable_objective_updates()->set_offset_update(
|
||||
from.objective_updates().offset_update());
|
||||
if (from_new.objective_updates().has_offset_update()) {
|
||||
into_old.mutable_objective_updates()->set_offset_update(
|
||||
from_new.objective_updates().offset_update());
|
||||
}
|
||||
internal::MergeIntoSparseVector(
|
||||
from.objective_updates().linear_coefficients(),
|
||||
*into.mutable_objective_updates()->mutable_linear_coefficients());
|
||||
|
||||
from_new.objective_updates().linear_coefficients(),
|
||||
*into_old.mutable_objective_updates()->mutable_linear_coefficients(),
|
||||
from_new.deleted_variable_ids());
|
||||
internal::MergeIntoSparseDoubleMatrix(
|
||||
from.linear_constraint_matrix_updates(),
|
||||
*into.mutable_linear_constraint_matrix_updates());
|
||||
from_new.objective_updates().quadratic_coefficients(),
|
||||
*into_old.mutable_objective_updates()->mutable_quadratic_coefficients(),
|
||||
/*deleted_rows=*/from_new.deleted_variable_ids(),
|
||||
/*deleted_columns=*/from_new.deleted_variable_ids());
|
||||
|
||||
// Merge the linear constraints coefficients.
|
||||
internal::MergeIntoSparseDoubleMatrix(
|
||||
from_new.linear_constraint_matrix_updates(),
|
||||
*into_old.mutable_linear_constraint_matrix_updates(),
|
||||
/*deleted_rows=*/from_new.deleted_linear_constraint_ids(),
|
||||
/*deleted_columns=*/from_new.deleted_variable_ids());
|
||||
}
|
||||
|
||||
namespace internal {
|
||||
|
||||
void MergeIntoSortedIds(const google::protobuf::RepeatedField<int64_t>& from,
|
||||
google::protobuf::RepeatedField<int64_t>& into) {
|
||||
void RemoveDeletedIds(google::protobuf::RepeatedField<int64_t>& ids,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted) {
|
||||
int next_insertion_point = 0;
|
||||
int deleted_i = 0;
|
||||
for (const int64_t id : ids) {
|
||||
while (deleted_i < deleted.size() && deleted[deleted_i] < id) {
|
||||
++deleted_i;
|
||||
}
|
||||
if (deleted_i < deleted.size() && deleted[deleted_i] == id) {
|
||||
continue;
|
||||
}
|
||||
ids[next_insertion_point] = id;
|
||||
++next_insertion_point;
|
||||
}
|
||||
ids.Truncate(next_insertion_point);
|
||||
}
|
||||
|
||||
void MergeIntoSortedIds(const google::protobuf::RepeatedField<int64_t>& from_new,
|
||||
google::protobuf::RepeatedField<int64_t>& into_old,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted) {
|
||||
google::protobuf::RepeatedField<int64_t> result;
|
||||
|
||||
// We don't reserve the sum of the sizes of both repeated fields since they
|
||||
// can contain overlapping ids. But we know that we will have at least the max
|
||||
// length of either repeated field.
|
||||
result.Reserve(std::max(from.size(), into.size()));
|
||||
int from_new_i = 0;
|
||||
int into_old_i = 0;
|
||||
int deleted_i = 0;
|
||||
|
||||
int from_i = 0;
|
||||
int into_i = 0;
|
||||
while (from_i < from.size() && into_i < into.size()) {
|
||||
if (from[from_i] < into[into_i]) {
|
||||
result.Add(from[from_i]);
|
||||
++from_i;
|
||||
} else if (from[from_i] > into[into_i]) {
|
||||
result.Add(into[into_i]);
|
||||
++into_i;
|
||||
} else { // from[from_i] == into[into_i]
|
||||
result.Add(from[from_i]);
|
||||
++from_i;
|
||||
++into_i;
|
||||
// Functions that adds the input id to the result if it is not in deleted. It
|
||||
// updates deleted_i as a side effect too.
|
||||
const auto add_if_not_deleted = [&](const int64_t id) {
|
||||
while (deleted_i < deleted.size() && deleted[deleted_i] < id) {
|
||||
++deleted_i;
|
||||
}
|
||||
if (deleted_i == deleted.size() || deleted[deleted_i] != id) {
|
||||
result.Add(id);
|
||||
}
|
||||
};
|
||||
|
||||
while (from_new_i < from_new.size() && into_old_i < into_old.size()) {
|
||||
if (from_new[from_new_i] < into_old[into_old_i]) {
|
||||
add_if_not_deleted(from_new[from_new_i]);
|
||||
++from_new_i;
|
||||
} else if (from_new[from_new_i] > into_old[into_old_i]) {
|
||||
add_if_not_deleted(into_old[into_old_i]);
|
||||
++into_old_i;
|
||||
} else { // from_new[from_new_i] == into_old[into_old_i]
|
||||
add_if_not_deleted(from_new[from_new_i]);
|
||||
++from_new_i;
|
||||
++into_old_i;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point either from_i == from.size() or to_i == to.size() or
|
||||
// At this point either from_new_i == from_new.size() or to_i == to.size() or
|
||||
// both. And the one that is not empty, if it exists, has elements greater
|
||||
// than all other elements already inserted.
|
||||
result.Reserve(result.size() +
|
||||
std::max(from.size() - from_i, into.size() - into_i));
|
||||
for (; from_i < from.size(); ++from_i) {
|
||||
result.Add(from[from_i]);
|
||||
for (; from_new_i < from_new.size(); ++from_new_i) {
|
||||
add_if_not_deleted(from_new[from_new_i]);
|
||||
}
|
||||
for (; into_i < into.size(); ++into_i) {
|
||||
result.Add(into[into_i]);
|
||||
for (; into_old_i < into_old.size(); ++into_old_i) {
|
||||
add_if_not_deleted(into_old[into_old_i]);
|
||||
}
|
||||
|
||||
into.Swap(&result);
|
||||
into_old.Swap(&result);
|
||||
}
|
||||
|
||||
void MergeIntoSparseDoubleMatrix(const SparseDoubleMatrixProto& from,
|
||||
SparseDoubleMatrixProto& into) {
|
||||
void MergeIntoSparseDoubleMatrix(
|
||||
const SparseDoubleMatrixProto& from_new, SparseDoubleMatrixProto& into_old,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted_rows,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted_columns) {
|
||||
SparseDoubleMatrixProto result;
|
||||
auto& result_row_ids = *result.mutable_row_ids();
|
||||
auto& result_column_ids = *result.mutable_column_ids();
|
||||
auto& result_coefficients = *result.mutable_coefficients();
|
||||
|
||||
// We don't reserve the sum of the sizes of both sparse matrices since they
|
||||
// can contain overlapping tuples. But we know that we will have at least the
|
||||
// max length of either matrix.
|
||||
const int max_size = std::max(from.row_ids_size(), into.row_ids_size());
|
||||
result_row_ids.Reserve(max_size);
|
||||
result_column_ids.Reserve(max_size);
|
||||
result_coefficients.Reserve(max_size);
|
||||
// Contrary to rows that are traversed in order (the matrix is using row-major
|
||||
// order), columns are not. Thus we would have to start the iteration on
|
||||
// deleted_columns for each new row of the matrix if we wanted to use the same
|
||||
// approach as with rows. This would be O(num_rows * num_deleted_columns).
|
||||
//
|
||||
// Here we use a hash-set to be O(num_matrix_elements +
|
||||
// num_deleted_columns). The downside is that we consumed
|
||||
// O(num_deleted_columns) additional memory.
|
||||
//
|
||||
// We could have used binary search that would be O(num_matrix_elements *
|
||||
// lg(num_deleted_columns)) but without additional memory.
|
||||
const absl::flat_hash_set<int64_t> deleted_columns_set(
|
||||
deleted_columns.begin(), deleted_columns.end());
|
||||
|
||||
int from_i = 0;
|
||||
int into_i = 0;
|
||||
while (from_i < from.row_ids_size() && into_i < into.row_ids_size()) {
|
||||
int from_new_i = 0;
|
||||
int into_old_i = 0;
|
||||
int deleted_rows_i = 0;
|
||||
|
||||
// Functions that adds the input tuple (row_id, col_id, coefficient) to the
|
||||
// result if the input row_id and col_id are not in deleted_rows or
|
||||
// deleted_columns. It updates deleted_rows_i and deleted_columns_i as a side
|
||||
// effect too.
|
||||
const auto add_if_not_deleted = [&](const int64_t row_id,
|
||||
const int64_t col_id,
|
||||
const double coefficient) {
|
||||
while (deleted_rows_i < deleted_rows.size() &&
|
||||
deleted_rows[deleted_rows_i] < row_id) {
|
||||
++deleted_rows_i;
|
||||
}
|
||||
if ((deleted_rows_i != deleted_rows.size() &&
|
||||
deleted_rows[deleted_rows_i] == row_id) ||
|
||||
deleted_columns_set.contains(col_id)) {
|
||||
return;
|
||||
}
|
||||
result_row_ids.Add(row_id);
|
||||
result_column_ids.Add(col_id);
|
||||
result_coefficients.Add(coefficient);
|
||||
};
|
||||
|
||||
while (from_new_i < from_new.row_ids_size() &&
|
||||
into_old_i < into_old.row_ids_size()) {
|
||||
// Matrices are in row-major order and std::pair comparison is
|
||||
// lexicographical, thus matrices are sorted in the natural order of pairs
|
||||
// of coordinates (row, col).
|
||||
const auto from_coordinates =
|
||||
std::make_pair(from.row_ids(from_i), from.column_ids(from_i));
|
||||
const auto into_coordinates =
|
||||
std::make_pair(into.row_ids(into_i), into.column_ids(into_i));
|
||||
if (from_coordinates < into_coordinates) {
|
||||
result_row_ids.Add(from_coordinates.first);
|
||||
result_column_ids.Add(from_coordinates.second);
|
||||
result_coefficients.Add(from.coefficients(from_i));
|
||||
++from_i;
|
||||
} else if (from_coordinates > into_coordinates) {
|
||||
result_row_ids.Add(into_coordinates.first);
|
||||
result_column_ids.Add(into_coordinates.second);
|
||||
result_coefficients.Add(into.coefficients(into_i));
|
||||
++into_i;
|
||||
} else { // from_coordinates == into_coordinates
|
||||
result_row_ids.Add(from_coordinates.first);
|
||||
result_column_ids.Add(from_coordinates.second);
|
||||
result_coefficients.Add(from.coefficients(from_i));
|
||||
++from_i;
|
||||
++into_i;
|
||||
const auto from_new_coordinates = std::make_pair(
|
||||
from_new.row_ids(from_new_i), from_new.column_ids(from_new_i));
|
||||
const auto into_old_coordinates = std::make_pair(
|
||||
into_old.row_ids(into_old_i), into_old.column_ids(into_old_i));
|
||||
if (from_new_coordinates < into_old_coordinates) {
|
||||
add_if_not_deleted(from_new_coordinates.first,
|
||||
from_new_coordinates.second,
|
||||
from_new.coefficients(from_new_i));
|
||||
++from_new_i;
|
||||
} else if (from_new_coordinates > into_old_coordinates) {
|
||||
add_if_not_deleted(into_old_coordinates.first,
|
||||
into_old_coordinates.second,
|
||||
into_old.coefficients(into_old_i));
|
||||
++into_old_i;
|
||||
} else { // from_new_coordinates == into_old_coordinates
|
||||
add_if_not_deleted(from_new_coordinates.first,
|
||||
from_new_coordinates.second,
|
||||
from_new.coefficients(from_new_i));
|
||||
++from_new_i;
|
||||
++into_old_i;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point either from_i == from.row_ids_size() or
|
||||
// At this point either from_new_i == from_new.row_ids_size() or
|
||||
// to_i == to.row_ids_size() (or both). And the one that is not empty, if it
|
||||
// exists, has elements greater than all other elements already inserted.
|
||||
const int remaining_size =
|
||||
std::max(from.row_ids_size() - from_i, into.row_ids_size() - into_i);
|
||||
result_row_ids.Reserve(result_row_ids.size() + remaining_size);
|
||||
result_column_ids.Reserve(result_column_ids.size() + remaining_size);
|
||||
result_coefficients.Reserve(result_coefficients.size() + remaining_size);
|
||||
for (; from_i < from.row_ids_size(); ++from_i) {
|
||||
result_row_ids.Add(from.row_ids(from_i));
|
||||
result_column_ids.Add(from.column_ids(from_i));
|
||||
result_coefficients.Add(from.coefficients(from_i));
|
||||
for (; from_new_i < from_new.row_ids_size(); ++from_new_i) {
|
||||
add_if_not_deleted(from_new.row_ids(from_new_i),
|
||||
from_new.column_ids(from_new_i),
|
||||
from_new.coefficients(from_new_i));
|
||||
}
|
||||
for (; into_i < into.row_ids_size(); ++into_i) {
|
||||
result_row_ids.Add(into.row_ids(into_i));
|
||||
result_column_ids.Add(into.column_ids(into_i));
|
||||
result_coefficients.Add(into.coefficients(into_i));
|
||||
for (; into_old_i < into_old.row_ids_size(); ++into_old_i) {
|
||||
add_if_not_deleted(into_old.row_ids(into_old_i),
|
||||
into_old.column_ids(into_old_i),
|
||||
into_old.coefficients(into_old_i));
|
||||
}
|
||||
|
||||
into.Swap(&result);
|
||||
into_old.Swap(&result);
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
@@ -18,47 +18,92 @@
|
||||
#include <cstdint>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/protobuf_util.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/model_update.pb.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// Merges the `from` update into the `into` one.
|
||||
// Merges the `from_new` update into the `into_old` one.
|
||||
//
|
||||
// The `from` update must represent an update that happens after the `into` one
|
||||
// is applied. Thus when the two updates have overlaps, the `from` one overrides
|
||||
// the value of the `into` one (i.e. the `from` update is expected to be more
|
||||
// recent).
|
||||
// The `from_new` update must represent an update that happens after the
|
||||
// `into_old` one is applied. Thus when the two updates have overlaps, the
|
||||
// `from_new` one overrides the value of the `into_old` one (i.e. the `from_new`
|
||||
// update is expected to be more recent).
|
||||
//
|
||||
// This function also CHECKs that the ids of new variables and constraints in
|
||||
// `from` are greater than the ones in `into` (as expected if `from` happens
|
||||
// after `into`).
|
||||
// `from_new` are greater than the ones in `into_old` (as expected if `from_new`
|
||||
// happens after `into_old`).
|
||||
//
|
||||
// Note that the complexity is O(size(from) + size(into)) thus if you need to
|
||||
// merge a long list of updates this may be not efficient enough. In that case
|
||||
// an n-way merge would be needed to be implemented here.
|
||||
void MergeIntoUpdate(const ModelUpdateProto& from, ModelUpdateProto& into);
|
||||
// Note that the complexity is O(size(from_new) + size(into_old)) thus if you
|
||||
// need to merge a long list of updates this may be not efficient enough. In
|
||||
// that case an n-way merge would be needed to be implemented here.
|
||||
void MergeIntoUpdate(const ModelUpdateProto& from_new,
|
||||
ModelUpdateProto& into_old);
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Merges the `from` list of sorted ids into the `into` one. Duplicates are
|
||||
// removed.
|
||||
void MergeIntoSortedIds(const google::protobuf::RepeatedField<int64_t>& from,
|
||||
google::protobuf::RepeatedField<int64_t>& into);
|
||||
// Removes from the sorted list `ids` all elements found in the sorted list
|
||||
// `deleted`. The elements should be unique in each sorted list.
|
||||
void RemoveDeletedIds(google::protobuf::RepeatedField<int64_t>& ids,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted);
|
||||
|
||||
// Merges the `from` sparse vector into the `into` one. When the two vectors
|
||||
// have overlaps, the value in `from` is used to overwrite the one in `into`.
|
||||
// Merges the `from_new` list of sorted ids into the `into_old` one. Elements
|
||||
// appearing in `from_new` that already exist in `into_old` are ignored.
|
||||
//
|
||||
// The input `deleted` should contains a sorted list of ids of elements that
|
||||
// have been deleted and should be removed from the merge.
|
||||
//
|
||||
// The elements should be unique in each sorted list.
|
||||
void MergeIntoSortedIds(const google::protobuf::RepeatedField<int64_t>& from_new,
|
||||
google::protobuf::RepeatedField<int64_t>& into_old,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted);
|
||||
|
||||
// Merges the `from_new` sparse vector into the `into_old` one. When the two
|
||||
// vectors have overlaps, the value in `from_new` is used to overwrite the one
|
||||
// in `into_old`.
|
||||
//
|
||||
// The input `deleted` should contains a sorted list of unique ids of elements
|
||||
// that have been deleted and should be removed from the merge.
|
||||
//
|
||||
// The SparseVector type is either SparseDoubleVectorProto or
|
||||
// SparseBoolVectorProto.
|
||||
template <typename SparseVector>
|
||||
inline void MergeIntoSparseVector(const SparseVector& from, SparseVector& into);
|
||||
void MergeIntoSparseVector(const SparseVector& from_new, SparseVector& into_old,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted);
|
||||
|
||||
// Merges the `from` sparse matrix into the `into` one. When the two matrices
|
||||
// have overlaps, the value in `from` is used to overwrite the one in `into`.
|
||||
void MergeIntoSparseDoubleMatrix(const SparseDoubleMatrixProto& from,
|
||||
SparseDoubleMatrixProto& into);
|
||||
// Merges the `from_new` sparse matrix into the `into_old` one. When the two
|
||||
// matrices have overlaps, the value in `from_new` is used to overwrite the one
|
||||
// in `into_old`.
|
||||
//
|
||||
// The input `deleted_rows` and `deleted_columns` should contains sorted lists
|
||||
// of unique ids of rows and cols that have been deleted and should be removed
|
||||
// from the merge.
|
||||
void MergeIntoSparseDoubleMatrix(
|
||||
const SparseDoubleMatrixProto& from_new, SparseDoubleMatrixProto& into_old,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted_rows,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted_columns);
|
||||
|
||||
// Updates a "property" repeated field of a ModelUpdateProto.new_variables or
|
||||
// ModelUpdateProto.new_linear_constraints.
|
||||
//
|
||||
// The `ids` input corresponds to VariablesProto.ids (or
|
||||
// LinearConstraintsProto.ids), and the values one to one property (for example
|
||||
// VariablesProto.lower_bounds). Values corresponding to ids in `deleted` are
|
||||
// removed. For the ids that have a value in `updates`, this value is used to
|
||||
// replace the existing one.
|
||||
//
|
||||
// The type SparseVector can either be a sparse proto like
|
||||
// SparseDoubleVectorProto or a SparseVectorView. The type RepeatedField is
|
||||
// usually a google::protobuf::RepeatedField but it can be also a
|
||||
// RepeatedPtrField<std::string> to deal with the `names` property.
|
||||
template <typename RepeatedField, typename SparseVector>
|
||||
void UpdateNewElementProperty(const google::protobuf::RepeatedField<int64_t>& ids,
|
||||
RepeatedField& values,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted,
|
||||
const SparseVector& updates);
|
||||
|
||||
} // namespace internal
|
||||
|
||||
@@ -69,57 +114,94 @@ void MergeIntoSparseDoubleMatrix(const SparseDoubleMatrixProto& from,
|
||||
namespace internal {
|
||||
|
||||
template <typename SparseVector>
|
||||
void MergeIntoSparseVector(const SparseVector& from, SparseVector& into) {
|
||||
CHECK_EQ(from.ids_size(), from.values_size());
|
||||
CHECK_EQ(into.ids_size(), into.values_size());
|
||||
void MergeIntoSparseVector(const SparseVector& from_new, SparseVector& into_old,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted) {
|
||||
CHECK_EQ(from_new.ids_size(), from_new.values_size());
|
||||
CHECK_EQ(into_old.ids_size(), into_old.values_size());
|
||||
|
||||
SparseVector result;
|
||||
auto& result_ids = *result.mutable_ids();
|
||||
auto& result_values = *result.mutable_values();
|
||||
|
||||
// We don't reserve the sum of the sizes of both sparse vectors since they can
|
||||
// contain overlapping ids. But we know that we will have at least the max
|
||||
// length of either vector.
|
||||
const int max_size = std::max(from.ids_size(), into.ids_size());
|
||||
result_ids.Reserve(max_size);
|
||||
result_values.Reserve(max_size);
|
||||
int from_new_i = 0;
|
||||
int into_old_i = 0;
|
||||
int deleted_i = 0;
|
||||
|
||||
int from_i = 0;
|
||||
int into_i = 0;
|
||||
while (from_i < from.ids_size() && into_i < into.ids_size()) {
|
||||
if (from.ids(from_i) < into.ids(into_i)) {
|
||||
result_ids.Add(from.ids(from_i));
|
||||
result_values.Add(from.values(from_i));
|
||||
++from_i;
|
||||
} else if (from.ids(from_i) > into.ids(into_i)) {
|
||||
result_ids.Add(into.ids(into_i));
|
||||
result_values.Add(into.values(into_i));
|
||||
++into_i;
|
||||
} else { // from.ids(from_i) == into.ids(into_i)
|
||||
result_ids.Add(from.ids(from_i));
|
||||
result_values.Add(from.values(from_i));
|
||||
++from_i;
|
||||
++into_i;
|
||||
// Functions that adds the input pair (id, value) to the result if the input
|
||||
// id is not in deleted. It updates deleted_i as a side effect too.
|
||||
const auto add_if_not_deleted =
|
||||
[&](const int64_t id, const sparse_value_type<SparseVector>& value) {
|
||||
while (deleted_i < deleted.size() && deleted[deleted_i] < id) {
|
||||
++deleted_i;
|
||||
}
|
||||
if (deleted_i == deleted.size() || deleted[deleted_i] != id) {
|
||||
result_ids.Add(id);
|
||||
result_values.Add(value);
|
||||
}
|
||||
};
|
||||
|
||||
while (from_new_i < from_new.ids_size() && into_old_i < into_old.ids_size()) {
|
||||
if (from_new.ids(from_new_i) < into_old.ids(into_old_i)) {
|
||||
add_if_not_deleted(from_new.ids(from_new_i), from_new.values(from_new_i));
|
||||
++from_new_i;
|
||||
} else if (from_new.ids(from_new_i) > into_old.ids(into_old_i)) {
|
||||
add_if_not_deleted(into_old.ids(into_old_i), into_old.values(into_old_i));
|
||||
++into_old_i;
|
||||
} else { // from_new.ids(from_new_i) == into_old.ids(into_old_i)
|
||||
add_if_not_deleted(from_new.ids(from_new_i), from_new.values(from_new_i));
|
||||
++from_new_i;
|
||||
++into_old_i;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point either from_i == from.ids_size() or to_i == to.ids_size() (or
|
||||
// both). And the one that is not empty, if it exists, has elements greater
|
||||
// than all other elements already inserted.
|
||||
const int remaining_size =
|
||||
std::max(from.ids_size() - from_i, into.ids_size() - into_i);
|
||||
result_ids.Reserve(result_ids.size() + remaining_size);
|
||||
result_values.Reserve(result_values.size() + remaining_size);
|
||||
for (; from_i < from.ids_size(); ++from_i) {
|
||||
result_ids.Add(from.ids(from_i));
|
||||
result_values.Add(from.values(from_i));
|
||||
// At this point either from_new_i == from_new.ids_size() or to_i ==
|
||||
// to.ids_size() (or both). And the one that is not empty, if it exists, has
|
||||
// elements greater than all other elements already inserted.
|
||||
for (; from_new_i < from_new.ids_size(); ++from_new_i) {
|
||||
add_if_not_deleted(from_new.ids(from_new_i), from_new.values(from_new_i));
|
||||
}
|
||||
for (; into_i < into.ids_size(); ++into_i) {
|
||||
result_ids.Add(into.ids(into_i));
|
||||
result_values.Add(into.values(into_i));
|
||||
for (; into_old_i < into_old.ids_size(); ++into_old_i) {
|
||||
add_if_not_deleted(into_old.ids(into_old_i), into_old.values(into_old_i));
|
||||
}
|
||||
|
||||
into.Swap(&result);
|
||||
into_old.Swap(&result);
|
||||
}
|
||||
|
||||
template <typename RepeatedField, typename SparseVector>
|
||||
void UpdateNewElementProperty(const google::protobuf::RepeatedField<int64_t>& ids,
|
||||
RepeatedField& values,
|
||||
const google::protobuf::RepeatedField<int64_t>& deleted,
|
||||
const SparseVector& updates) {
|
||||
int next_insertion_point = 0;
|
||||
int deleted_i = 0;
|
||||
int updates_i = 0;
|
||||
|
||||
for (int i = 0; i < ids.size(); ++i) {
|
||||
const int id = ids[i];
|
||||
|
||||
while (deleted_i < deleted.size() && deleted[deleted_i] < id) {
|
||||
++deleted_i;
|
||||
}
|
||||
if (deleted_i < deleted.size() && deleted[deleted_i] == id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while (updates_i < updates.ids_size() && updates.ids(updates_i) < id) {
|
||||
++updates_i;
|
||||
}
|
||||
if (updates_i < updates.ids_size() && updates.ids(updates_i) == id) {
|
||||
values[next_insertion_point] = updates.values(updates_i);
|
||||
} else {
|
||||
// Here we use SwapElements() to prevent copies when `values` is a
|
||||
// RepeatedPtrField<std::string>.
|
||||
values.SwapElements(next_insertion_point, i);
|
||||
}
|
||||
++next_insertion_point;
|
||||
}
|
||||
|
||||
// We can't use value.Truncate() here since RepeatedPtrField<std::string> does
|
||||
// not implement it.
|
||||
google::protobuf::util::Truncate(&values, next_insertion_point);
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
138
ortools/math_opt/core/non_streamable_solver_init_arguments.h
Normal file
138
ortools/math_opt/core/non_streamable_solver_init_arguments.h
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CORE_NON_STREAMABLE_SOLVER_INIT_ARGUMENTS_H_
|
||||
#define OR_TOOLS_MATH_OPT_CORE_NON_STREAMABLE_SOLVER_INIT_ARGUMENTS_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "ortools/math_opt/parameters.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
struct NonStreamableBiscoInitArguments;
|
||||
struct NonStreamableCpSatInitArguments;
|
||||
struct NonStreamableGScipInitArguments;
|
||||
struct NonStreamableGlopInitArguments;
|
||||
struct NonStreamableGlpkInitArguments;
|
||||
struct NonStreamableGurobiInitArguments;
|
||||
|
||||
// Interface for solver specific parameters used at the solver instantiation
|
||||
// that can't be streamed (for example instances of C/C++ types that only exist
|
||||
// in the process memory).
|
||||
//
|
||||
// Since implementations of this interface usually depend on solver specific
|
||||
// C/C++ types, they are in a dedicated header in the solver library.
|
||||
//
|
||||
// This class is the interface shared by the parameters of each solver, users
|
||||
// should instantiate the solver specific class below.
|
||||
//
|
||||
// To enable safe cast of a pointer to this interface, there is an
|
||||
// ToNonStreamableXxxInitArguments() function for each solver. Only one of these
|
||||
// function will return a non-null value, depending on the type of the
|
||||
// implementation class.
|
||||
//
|
||||
// Implementation should use NonStreamableSolverInitArgumentsHelper to
|
||||
// automatically implements some methods.
|
||||
struct NonStreamableSolverInitArguments {
|
||||
virtual ~NonStreamableSolverInitArguments() = default;
|
||||
|
||||
// Returns the type of solver that the implementation is for.
|
||||
virtual SolverTypeProto solver_type() const = 0;
|
||||
|
||||
// Returns this for the NonStreamableBiscoInitArguments class, nullptr for
|
||||
// other classes.
|
||||
virtual const NonStreamableBiscoInitArguments*
|
||||
ToNonStreamableBiscoInitArguments() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Returns this for the NonStreamableCpSatInitArguments class, nullptr for
|
||||
// other classes.
|
||||
virtual const NonStreamableCpSatInitArguments*
|
||||
ToNonStreamableCpSatInitArguments() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Returns this for the NonStreamableGScipInitArguments class, nullptr for
|
||||
// other classes.
|
||||
virtual const NonStreamableGScipInitArguments*
|
||||
ToNonStreamableGScipInitArguments() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Returns this for the NonStreamableGlopInitArguments class, nullptr for
|
||||
// other classes.
|
||||
virtual const NonStreamableGlopInitArguments*
|
||||
ToNonStreamableGlopInitArguments() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Returns this for the NonStreamableGlpkInitArguments class, nullptr for
|
||||
// other classes.
|
||||
virtual const NonStreamableGlpkInitArguments*
|
||||
ToNonStreamableGlpkInitArguments() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Returns this for the NonStreamableGurobiInitArguments class, nullptr for
|
||||
// other classes.
|
||||
virtual const NonStreamableGurobiInitArguments*
|
||||
ToNonStreamableGurobiInitArguments() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Return a copy of this.
|
||||
//
|
||||
// The NonStreamableSolverInitArgumentsHelper implements this automatically
|
||||
// using the copy constructor (this base class is copyable intentionally).
|
||||
virtual std::unique_ptr<const NonStreamableSolverInitArguments> Clone()
|
||||
const = 0;
|
||||
};
|
||||
|
||||
// Base struct for implementations that automatically implements solver_type()
|
||||
// and Clone() virtual methods.
|
||||
//
|
||||
// The Clone() method is implemented with the copy constructor of the struct.
|
||||
//
|
||||
// All that is left to the implementation is to provide are the solver specific
|
||||
// field and the implementation of the ToNonStreamableXxxInitArguments()
|
||||
// corresponding to the solver type.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// struct NonStreamableXxxInitArguments
|
||||
// : public NonStreamableSolverInitArgumentsHelper<
|
||||
// NonStreamableXxxInitArguments, SOLVER_TYPE_XXX> {
|
||||
//
|
||||
// ... some data member here ...
|
||||
//
|
||||
// const NonStreamableXxxInitArguments*
|
||||
// ToNonStreamableXxxInitArguments() const { return this; }
|
||||
// };
|
||||
template <typename Implementation, SolverTypeProto impl_solver_type>
|
||||
struct NonStreamableSolverInitArgumentsHelper
|
||||
: public NonStreamableSolverInitArguments {
|
||||
SolverTypeProto solver_type() const final { return impl_solver_type; }
|
||||
|
||||
std::unique_ptr<const NonStreamableSolverInitArguments> Clone() const final {
|
||||
return std::make_unique<Implementation>(
|
||||
*static_cast<const Implementation*>(this));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CORE_NON_STREAMABLE_SOLVER_INIT_ARGUMENTS_H_
|
||||
104
ortools/math_opt/core/solve_interrupter.cc
Normal file
104
ortools/math_opt/core/solve_interrupter.cc
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "ortools/base/linked_hash_map.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
void SolveInterrupter::Interrupt() {
|
||||
const absl::MutexLock lock(&mutex_);
|
||||
|
||||
// Here we don't use compare_exchange_strong since we need to hold the lock
|
||||
// before changing the value of interrupted_ anyway. So there is no need to
|
||||
// use this complex function.
|
||||
if (interrupted_.load()) {
|
||||
// We must not call the callbacks more than once.
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to change this value while holding the lock since in
|
||||
// AddInterruptionCallback() we must know if we need to call the new callback
|
||||
// of if this function has called it.
|
||||
interrupted_ = true;
|
||||
|
||||
// We are holding the lock while calling callbacks. This make it impossible to
|
||||
// call Interrupt(), AddInterruptionCallback(), or
|
||||
// RemoveInterruptionCallback() from a callback but it ensures that external
|
||||
// code that can modify callbacks_ will wait the end of Interrupt.
|
||||
for (const auto& [callback_id, callback] : callbacks_) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
SolveInterrupter::CallbackId SolveInterrupter::AddInterruptionCallback(
|
||||
Callback callback) {
|
||||
const absl::MutexLock lock(&mutex_);
|
||||
|
||||
// We must make this call while holding the lock since we want to be sure that
|
||||
// the calls to the callbacks_ won't occur before we registered the new
|
||||
// one. If we were not holding the lock, this could return false and before we
|
||||
// could add the new callback to callbacks_, the Interrupt() function may
|
||||
// still have called them.
|
||||
//
|
||||
// We make the call before putting the callback in the map to since we need to
|
||||
// move it in place.
|
||||
if (interrupted_.load()) {
|
||||
callback();
|
||||
}
|
||||
|
||||
const CallbackId id = next_callback_id_;
|
||||
++next_callback_id_;
|
||||
CHECK(callbacks_.try_emplace(id, std::move(callback)).second);
|
||||
return id;
|
||||
}
|
||||
|
||||
void SolveInterrupter::RemoveInterruptionCallback(CallbackId id) {
|
||||
const absl::MutexLock lock(&mutex_);
|
||||
CHECK_EQ(callbacks_.erase(id), 1) << "unregistered callback id: " << id;
|
||||
}
|
||||
|
||||
ScopedSolveInterrupterCallback::ScopedSolveInterrupterCallback(
|
||||
SolveInterrupter* const interrupter, SolveInterrupter::Callback callback)
|
||||
: interrupter_(interrupter),
|
||||
callback_id_(
|
||||
interrupter != nullptr
|
||||
? std::make_optional(
|
||||
interrupter->AddInterruptionCallback(std::move(callback)))
|
||||
: std::nullopt) {}
|
||||
|
||||
ScopedSolveInterrupterCallback::~ScopedSolveInterrupterCallback() {
|
||||
RemoveCallbackIfNecessary();
|
||||
}
|
||||
|
||||
void ScopedSolveInterrupterCallback::RemoveCallbackIfNecessary() {
|
||||
if (callback_id_) {
|
||||
CHECK_NE(interrupter_, nullptr);
|
||||
interrupter_->RemoveInterruptionCallback(*callback_id_);
|
||||
callback_id_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
143
ortools/math_opt/core/solve_interrupter.h
Normal file
143
ortools/math_opt/core/solve_interrupter.h
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CORE_SOLVE_INTERRUPTER_H_
|
||||
#define OR_TOOLS_MATH_OPT_CORE_SOLVE_INTERRUPTER_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "absl/base/thread_annotations.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "ortools/base/linked_hash_map.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// Interrupter used by solvers to know if/when they should interrupt the solve.
|
||||
//
|
||||
// Once triggered with Interrupt(), an interrupter can't be reset. It can be
|
||||
// triggered from any thread.
|
||||
//
|
||||
// Thread-safety: APIs on this class are safe to call concurrently from multiple
|
||||
// threads.
|
||||
class SolveInterrupter {
|
||||
public:
|
||||
// Id used to identify a callback.
|
||||
DEFINE_INT_TYPE(CallbackId, int64_t);
|
||||
|
||||
using Callback = std::function<void()>;
|
||||
|
||||
SolveInterrupter() = default;
|
||||
|
||||
SolveInterrupter(const SolveInterrupter&) = delete;
|
||||
SolveInterrupter& operator=(const SolveInterrupter&) = delete;
|
||||
|
||||
// Interrupts the solve as soon as possible.
|
||||
//
|
||||
// Once requested the interruption can't be reset. The user should use a new
|
||||
// SolveInterrupter for later solves.
|
||||
//
|
||||
// It is safe to call this function multiple times. Only the first call will
|
||||
// have visible effects; other calls will be ignored.
|
||||
void Interrupt();
|
||||
|
||||
// Returns true if the solve interruption has been requested.
|
||||
//
|
||||
// This API is fast; it costs the read of an atomic.
|
||||
inline bool IsInterrupted() const { return interrupted_.load(); }
|
||||
|
||||
// Registers a callback to be called when the interruption is requested.
|
||||
//
|
||||
// The callback is immediately called if the interrupter has already been
|
||||
// triggered or if it is triggered during the registration. This is typically
|
||||
// useful for a solver implementation so that it does not have to test
|
||||
// IsInterrupted() to do the same thing it does in the callback. Simply
|
||||
// registering the callback is enough.
|
||||
//
|
||||
// The callback function can't make calls to AddInterruptionCallback(),
|
||||
// RemoveInterruptionCallback() and Interrupt(). This would result is a
|
||||
// deadlock. Calling IsInterrupted() is fine though.
|
||||
CallbackId AddInterruptionCallback(Callback callback);
|
||||
|
||||
// Unregisters a callback previously registered. It fails (with a CHECK) if
|
||||
// the callback was already unregistered or unkonwn. After this calls returns,
|
||||
// the caller can assume the callback won't be called.
|
||||
//
|
||||
// This function can't be called from a callback since this would result in a
|
||||
// deadlock.
|
||||
void RemoveInterruptionCallback(CallbackId id);
|
||||
|
||||
private:
|
||||
// This atomic must never be reset to true!
|
||||
//
|
||||
// The mutex_ should be held when setting it to true.
|
||||
std::atomic<bool> interrupted_ = false;
|
||||
|
||||
absl::Mutex mutex_;
|
||||
|
||||
// The id to use for the next registered callback.
|
||||
CallbackId next_callback_id_ ABSL_GUARDED_BY(mutex_) = {};
|
||||
|
||||
// The list of callbacks. We use a linked_hash_map to make sure the order of
|
||||
// calls to callback when the interrupter is triggered is stable.
|
||||
gtl::linked_hash_map<CallbackId, Callback> callbacks_ ABSL_GUARDED_BY(mutex_);
|
||||
};
|
||||
|
||||
// Class implementing RAII for interruption callbacks.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// SolveInterrupter* const interrupter = ...;
|
||||
// {
|
||||
// const ScopedSolveInterrupterCallback scoped_intr_cb(interrupter, [](){
|
||||
// // Do something when/if interrupter is not nullptr and is triggered.
|
||||
// }
|
||||
// ...
|
||||
// }
|
||||
// // At this point, the callback will have been removed.
|
||||
//
|
||||
// The function RemoveCallbackIfNecessary() can be used to remove the callback
|
||||
// before the destruction of this object.
|
||||
class ScopedSolveInterrupterCallback {
|
||||
public:
|
||||
// Adds a callback to the interrupter if it is not nullptr. Does nothing when
|
||||
// interrupter is nullptr.
|
||||
ScopedSolveInterrupterCallback(SolveInterrupter* interrupter,
|
||||
SolveInterrupter::Callback callback);
|
||||
|
||||
// Removes the callback if necessary.
|
||||
~ScopedSolveInterrupterCallback();
|
||||
|
||||
// Removes the callback from the interrupter. If it has already been removed
|
||||
// by a previous call or if a null interrupter was passed to the constructor,
|
||||
// this function has no effect.
|
||||
void RemoveCallbackIfNecessary();
|
||||
|
||||
private:
|
||||
// Optional interrupter.
|
||||
SolveInterrupter* const interrupter_;
|
||||
|
||||
// Unset after the callback has been reset.
|
||||
std::optional<SolveInterrupter::CallbackId> callback_id_;
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CORE_SOLVE_INTERRUPTER_H_
|
||||
@@ -22,23 +22,28 @@
|
||||
|
||||
#include "ortools/base/integral_types.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/base/thread_annotations.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/model_summary.h"
|
||||
#include "ortools/math_opt/core/non_streamable_solver_init_arguments.h"
|
||||
#include "ortools/math_opt/core/solver_debug.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_parameters.pb.h"
|
||||
#include "ortools/math_opt/model_update.pb.h"
|
||||
#include "ortools/math_opt/parameters.pb.h"
|
||||
#include "ortools/math_opt/result.pb.h"
|
||||
#include "ortools/math_opt/validators/callback_validator.h"
|
||||
#include "ortools/math_opt/validators/model_parameters_validator.h"
|
||||
#include "ortools/math_opt/validators/model_validator.h"
|
||||
#include "ortools/math_opt/validators/solution_validator.h"
|
||||
#include "ortools/math_opt/validators/result_validator.h"
|
||||
#include "ortools/math_opt/validators/solver_parameters_validator.h"
|
||||
#include "ortools/port/proto_utils.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
|
||||
namespace operations_research {
|
||||
@@ -89,71 +94,149 @@ absl::Status ToInternalError(const absl::Status original) {
|
||||
return absl::InternalError(original.message());
|
||||
}
|
||||
|
||||
// RAII class that is used to return an error when concurrent calls to some
|
||||
// functions are made.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // Calling f() and/or g() concurrently will return an error.
|
||||
// class A {
|
||||
// public:
|
||||
// absl::StatusOr<...> f() {
|
||||
// ASSIGN_OR_RETURN(const auto guard,
|
||||
// ConcurrentCallsGuard::TryAcquire(mutex_));
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// absl::StatusOr<...> g() {
|
||||
// ASSIGN_OR_RETURN(const auto guard,
|
||||
// ConcurrentCallsGuard::TryAcquire(mutex_));
|
||||
// ...
|
||||
// }
|
||||
|
||||
// private:
|
||||
// absl::Mutex mutex_;
|
||||
// };
|
||||
//
|
||||
class ConcurrentCallsGuard {
|
||||
public:
|
||||
// Returns an errors status when concurrent calls are made, or a guard that
|
||||
// must only be kept on stack during the execution of the call.
|
||||
static absl::StatusOr<ConcurrentCallsGuard> TryAcquire(absl::Mutex& mutex)
|
||||
ABSL_NO_THREAD_SAFETY_ANALYSIS {
|
||||
// ABSL_NO_THREAD_SAFETY_ANALYSIS is needed since the analyser is confused
|
||||
// by TryLock. See b/34113867, b/16712284.
|
||||
|
||||
if (!mutex.TryLock()) {
|
||||
return absl::InvalidArgumentError("concurrent calls are forbidden");
|
||||
}
|
||||
return ConcurrentCallsGuard(mutex);
|
||||
}
|
||||
|
||||
ConcurrentCallsGuard(const ConcurrentCallsGuard&) = delete;
|
||||
ConcurrentCallsGuard& operator=(const ConcurrentCallsGuard&) = delete;
|
||||
ConcurrentCallsGuard& operator=(ConcurrentCallsGuard&&) = delete;
|
||||
|
||||
ConcurrentCallsGuard(ConcurrentCallsGuard&& other)
|
||||
: mutex_(std::exchange(other.mutex_, nullptr)) {}
|
||||
|
||||
// Release the guard.
|
||||
~ConcurrentCallsGuard() {
|
||||
if (mutex_ != nullptr) {
|
||||
mutex_->Unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
explicit ConcurrentCallsGuard(absl::Mutex& mutex) : mutex_(&mutex) {
|
||||
mutex_->AssertHeld();
|
||||
}
|
||||
|
||||
// Reset to nullptr when the class is moved by the move constructor.
|
||||
absl::Mutex* mutex_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
absl::StatusOr<SolveResultProto> Solver::NonIncrementalSolve(
|
||||
const ModelProto& model, const SolverTypeProto solver_type,
|
||||
const InitArgs& init_args, const SolveArgs& solve_args) {
|
||||
ASSIGN_OR_RETURN(std::unique_ptr<Solver> solver,
|
||||
Solver::New(solver_type, model, init_args));
|
||||
return solver->Solve(solve_args);
|
||||
}
|
||||
|
||||
Solver::Solver(std::unique_ptr<SolverInterface> underlying_solver,
|
||||
ModelSummary model_summary)
|
||||
: underlying_solver_(std::move(underlying_solver)),
|
||||
model_summary_(std::move(model_summary)) {
|
||||
CHECK(underlying_solver_ != nullptr);
|
||||
++internal::debug_num_solver;
|
||||
}
|
||||
|
||||
Solver::~Solver() { --internal::debug_num_solver; }
|
||||
|
||||
absl::StatusOr<std::unique_ptr<Solver>> Solver::New(
|
||||
const SolverType solver_type, const ModelProto& model,
|
||||
const SolverInitializerProto& initializer) {
|
||||
const SolverTypeProto solver_type, const ModelProto& model,
|
||||
const InitArgs& arguments) {
|
||||
RETURN_IF_ERROR(internal::ValidateInitArgs(arguments, solver_type));
|
||||
RETURN_IF_ERROR(ValidateModel(model));
|
||||
ASSIGN_OR_RETURN(
|
||||
auto underlying_solver,
|
||||
AllSolversRegistry::Instance()->Create(solver_type, model, initializer));
|
||||
AllSolversRegistry::Instance()->Create(solver_type, model, arguments));
|
||||
auto result = absl::WrapUnique(
|
||||
new Solver(std::move(underlying_solver), MakeSummary(model)));
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<SolveResultProto> Solver::Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const CallbackRegistrationProto& callback_registration,
|
||||
const Callback user_cb) {
|
||||
absl::StatusOr<SolveResultProto> Solver::Solve(const SolveArgs& arguments) {
|
||||
ASSIGN_OR_RETURN(const auto guard, ConcurrentCallsGuard::TryAcquire(mutex_));
|
||||
|
||||
// TODO(b/168037341): we should validate the result maths. Since the result
|
||||
// can be filtered, this should be included in the solver_interface
|
||||
// implementations.
|
||||
|
||||
RETURN_IF_ERROR(ValidateSolverParameters(parameters)) << "invalid parameters";
|
||||
RETURN_IF_ERROR(ValidateSolverParameters(arguments.parameters))
|
||||
<< "invalid parameters";
|
||||
RETURN_IF_ERROR(
|
||||
ValidateModelSolveParameters(model_parameters, model_summary_))
|
||||
ValidateModelSolveParameters(arguments.model_parameters, model_summary_))
|
||||
<< "invalid model_parameters";
|
||||
|
||||
SolverInterface::Callback cb = nullptr;
|
||||
if (user_cb != nullptr) {
|
||||
RETURN_IF_ERROR(
|
||||
ValidateCallbackRegistration(callback_registration, model_summary_));
|
||||
if (arguments.user_cb != nullptr) {
|
||||
RETURN_IF_ERROR(ValidateCallbackRegistration(
|
||||
arguments.callback_registration, model_summary_));
|
||||
cb = [&](const CallbackDataProto& callback_data)
|
||||
-> absl::StatusOr<CallbackResultProto> {
|
||||
RETURN_IF_ERROR(ValidateCallbackDataProto(
|
||||
callback_data, callback_registration, model_summary_));
|
||||
auto callback_result = user_cb(callback_data);
|
||||
RETURN_IF_ERROR(
|
||||
ValidateCallbackResultProto(callback_result, callback_data.event(),
|
||||
callback_registration, model_summary_));
|
||||
callback_data, arguments.callback_registration, model_summary_));
|
||||
auto callback_result = arguments.user_cb(callback_data);
|
||||
RETURN_IF_ERROR(ValidateCallbackResultProto(
|
||||
callback_result, callback_data.event(),
|
||||
arguments.callback_registration, model_summary_));
|
||||
return callback_result;
|
||||
};
|
||||
}
|
||||
|
||||
ASSIGN_OR_RETURN(const SolveResultProto result,
|
||||
underlying_solver_->Solve(parameters, model_parameters,
|
||||
callback_registration, cb));
|
||||
underlying_solver_->Solve(arguments.parameters,
|
||||
arguments.model_parameters,
|
||||
arguments.message_callback,
|
||||
arguments.callback_registration,
|
||||
cb, arguments.interrupter));
|
||||
|
||||
// We consider errors in `result` to be internal errors, but
|
||||
// `ValidateResult()` will return an InvalidArgumentError. So here we convert
|
||||
// the error.
|
||||
RETURN_IF_ERROR(ToInternalError(
|
||||
ValidateResult(result, model_parameters, model_summary_)));
|
||||
ValidateResult(result, arguments.model_parameters, model_summary_)));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<bool> Solver::Update(const ModelUpdateProto& model_update) {
|
||||
ASSIGN_OR_RETURN(const auto guard, ConcurrentCallsGuard::TryAcquire(mutex_));
|
||||
|
||||
RETURN_IF_ERROR(ValidateModelUpdateAndSummary(model_update, model_summary_));
|
||||
if (!underlying_solver_->CanUpdate(model_update)) {
|
||||
return false;
|
||||
@@ -163,5 +246,26 @@ absl::StatusOr<bool> Solver::Update(const ModelUpdateProto& model_update) {
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace internal {
|
||||
|
||||
absl::Status ValidateInitArgs(const Solver::InitArgs& init_args,
|
||||
const SolverTypeProto solver_type) {
|
||||
if (solver_type == SOLVER_TYPE_UNSPECIFIED) {
|
||||
return absl::InvalidArgumentError(
|
||||
"can't use SOLVER_TYPE_UNSPECIFIED as solver_type parameter");
|
||||
}
|
||||
|
||||
if (init_args.non_streamable != nullptr &&
|
||||
init_args.non_streamable->solver_type() != solver_type) {
|
||||
return absl::InvalidArgumentError(
|
||||
absl::StrCat("input non_streamable init arguments are for ",
|
||||
ProtoEnumToString(init_args.non_streamable->solver_type()),
|
||||
" but solver_type is ", ProtoEnumToString(solver_type)));
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/model_summary.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_parameters.pb.h"
|
||||
@@ -35,46 +38,87 @@ namespace math_opt {
|
||||
// Use the New() function to build a new solver instance; then call Solve() to
|
||||
// solve the model. You can then update the model using Update() and resolve.
|
||||
//
|
||||
// Thread-safety: methods Solve() and Update() must not be called concurrently;
|
||||
// they will immediately return with an error status if this happens. Some
|
||||
// solvers may add more restriction regarding threading. Please see
|
||||
// SOLVER_TYPE_XXX documentation for details.
|
||||
//
|
||||
// Usage:
|
||||
// const ModelProto model = ...;
|
||||
// const auto solver = Solver::New(SOLVER_TYPE_GSCIP,
|
||||
// model,
|
||||
// SolverInitializerProto{});
|
||||
// /*arguments=*/{});
|
||||
// CHECK_OK(solver.status());
|
||||
// const SolveParametersProto solve_params = ...;
|
||||
// Solver::SolveArgs solve_arguments;
|
||||
// ...
|
||||
//
|
||||
// // First solve of the initial Model.
|
||||
// const auto first_solution = (*solver)->Solve(solve_params);
|
||||
// const auto first_solution = (*solver)->Solve(solve_arguments);
|
||||
// CHECK_OK(first_solution.status());
|
||||
// // Use the first_solution here.
|
||||
//
|
||||
// // Update the Model with a ModelUpdate.
|
||||
// const ModelUpdate update = ...;
|
||||
// CHECK_OK((*solver)->Update(update));
|
||||
// const auto second_solution = (*solver)->Solve(solve_params);
|
||||
// const auto second_solution = (*solver)->Solve(solve_arguments);
|
||||
// CHECK_OK(second_solution.status());
|
||||
// // Use the second_solution of the updated problem here.
|
||||
//
|
||||
class Solver {
|
||||
public:
|
||||
// Callback function type.
|
||||
using InitArgs = SolverInterface::InitArgs;
|
||||
|
||||
// Callback function for messages callback sent by the solver.
|
||||
//
|
||||
// Each message represents a single output line from the solver, and each
|
||||
// message does not contain any '\n' character in it.
|
||||
//
|
||||
// Thread-safety: a callback may be called concurrently from multiple
|
||||
// threads. The users is expected to use proper synchronization primitives to
|
||||
// deal with that.
|
||||
using MessageCallback = SolverInterface::MessageCallback;
|
||||
|
||||
// Callback function type for MIP/LP callbacks.
|
||||
using Callback = std::function<CallbackResultProto(const CallbackDataProto&)>;
|
||||
|
||||
// Arguments used when calling Solve() to solve the problem.
|
||||
struct SolveArgs {
|
||||
SolveParametersProto parameters;
|
||||
ModelSolveParametersProto model_parameters;
|
||||
|
||||
// An optional callback for messages emitted by the solver.
|
||||
//
|
||||
// When set it enables the solver messages and ignores the `enable_output`
|
||||
// in solve parameters; messages are redirected to the callback and not
|
||||
// printed on stdout/stderr/logs anymore.
|
||||
MessageCallback message_callback = nullptr;
|
||||
|
||||
CallbackRegistrationProto callback_registration;
|
||||
Callback user_cb = nullptr;
|
||||
|
||||
// An optional interrupter that the solver can use to interrupt the solve
|
||||
// early.
|
||||
SolveInterrupter* interrupter = nullptr;
|
||||
};
|
||||
|
||||
// A shortcut for calling Solver::New() and then Solver::Solve().
|
||||
static absl::StatusOr<SolveResultProto> NonIncrementalSolve(
|
||||
const ModelProto& model, SolverTypeProto solver_type,
|
||||
const InitArgs& init_args, const SolveArgs& solve_args);
|
||||
|
||||
// Builds a solver of the given type with the provided model and
|
||||
// initialization parameters.
|
||||
static absl::StatusOr<std::unique_ptr<Solver>> New(
|
||||
SolverType solver_type, const ModelProto& model,
|
||||
const SolverInitializerProto& initializer);
|
||||
SolverTypeProto solver_type, const ModelProto& model,
|
||||
const InitArgs& arguments);
|
||||
|
||||
Solver(const Solver&) = delete;
|
||||
Solver& operator=(const Solver&) = delete;
|
||||
|
||||
~Solver();
|
||||
|
||||
// Solves the current model (included all updates).
|
||||
absl::StatusOr<SolveResultProto> Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters = {},
|
||||
const CallbackRegistrationProto& callback_registration = {},
|
||||
Callback user_cb = nullptr);
|
||||
absl::StatusOr<SolveResultProto> Solve(const SolveArgs& arguments);
|
||||
|
||||
// Updates the model to solve and returns true, or returns false if this
|
||||
// update is not supported by the underlying solver.
|
||||
@@ -87,10 +131,21 @@ class Solver {
|
||||
Solver(std::unique_ptr<SolverInterface> underlying_solver,
|
||||
ModelSummary model_summary);
|
||||
|
||||
// Mutex used to ensure that Solve() and Update() are not called concurrently.
|
||||
absl::Mutex mutex_;
|
||||
|
||||
const std::unique_ptr<SolverInterface> underlying_solver_;
|
||||
ModelSummary model_summary_;
|
||||
};
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Validates that the input streamable and non_streamable init arguments are
|
||||
// either not set or are the one of solver_type.
|
||||
absl::Status ValidateInitArgs(const Solver::InitArgs& init_args,
|
||||
SolverTypeProto solver_type);
|
||||
|
||||
} // namespace internal
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
|
||||
27
ortools/math_opt/core/solver_debug.cc
Normal file
27
ortools/math_opt/core/solver_debug.cc
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/core/solver_debug.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
namespace internal {
|
||||
|
||||
std::atomic<int64_t> debug_num_solver = 0;
|
||||
|
||||
} // namespace internal
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
35
ortools/math_opt/core/solver_debug.h
Normal file
35
ortools/math_opt/core/solver_debug.h
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CORE_SOLVER_DEBUG_H_
|
||||
#define OR_TOOLS_MATH_OPT_CORE_SOLVER_DEBUG_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
namespace internal {
|
||||
|
||||
// The number of Solver instances that currently exist.
|
||||
//
|
||||
// This variable is intended to be used by MathOpt unit tests in other languages
|
||||
// to test the proper garbage collection. It should never be used in any other
|
||||
// context.
|
||||
extern std::atomic<int64_t> debug_num_solver;
|
||||
|
||||
} // namespace internal
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CORE_SOLVER_DEBUG_H_
|
||||
@@ -39,7 +39,7 @@ AllSolversRegistry* AllSolversRegistry::Instance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
void AllSolversRegistry::Register(const SolverType solver_type,
|
||||
void AllSolversRegistry::Register(const SolverTypeProto solver_type,
|
||||
SolverInterface::Factory factory) {
|
||||
bool inserted;
|
||||
{
|
||||
@@ -52,8 +52,8 @@ void AllSolversRegistry::Register(const SolverType solver_type,
|
||||
}
|
||||
|
||||
absl::StatusOr<std::unique_ptr<SolverInterface>> AllSolversRegistry::Create(
|
||||
SolverType solver_type, const ModelProto& model,
|
||||
const SolverInitializerProto& initializer) const {
|
||||
SolverTypeProto solver_type, const ModelProto& model,
|
||||
const SolverInterface::InitArgs& init_args) const {
|
||||
const SolverInterface::Factory* factory = nullptr;
|
||||
{
|
||||
const absl::MutexLock lock(&mutex_);
|
||||
@@ -64,16 +64,16 @@ absl::StatusOr<std::unique_ptr<SolverInterface>> AllSolversRegistry::Create(
|
||||
absl::StrCat("Solver type: ", ProtoEnumToString(solver_type),
|
||||
" is not registered."));
|
||||
}
|
||||
return (*factory)(model, initializer);
|
||||
return (*factory)(model, init_args);
|
||||
}
|
||||
|
||||
bool AllSolversRegistry::IsRegistered(const SolverType solver_type) const {
|
||||
bool AllSolversRegistry::IsRegistered(const SolverTypeProto solver_type) const {
|
||||
const absl::MutexLock lock(&mutex_);
|
||||
return registered_solvers_.contains(solver_type);
|
||||
}
|
||||
|
||||
std::vector<SolverType> AllSolversRegistry::RegisteredSolvers() const {
|
||||
std::vector<SolverType> result;
|
||||
std::vector<SolverTypeProto> AllSolversRegistry::RegisteredSolvers() const {
|
||||
std::vector<SolverTypeProto> result;
|
||||
{
|
||||
const absl::MutexLock lock(&mutex_);
|
||||
for (const auto& kv_pair : registered_solvers_) {
|
||||
|
||||
@@ -19,11 +19,15 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/base/attributes.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/non_streamable_solver_init_arguments.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_parameters.pb.h"
|
||||
#include "ortools/math_opt/model_update.pb.h"
|
||||
@@ -32,12 +36,21 @@
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
namespace internal {
|
||||
|
||||
// The message of the InvalidArgumentError returned by solvers that are passed a
|
||||
// non null message callback when they don't support it.
|
||||
inline constexpr absl::string_view kMessageCallbackNotSupported =
|
||||
"This solver does not support message callbacks.";
|
||||
|
||||
} // namespace internal
|
||||
|
||||
// Interface implemented by actual solvers.
|
||||
//
|
||||
// This interface is not meant to be used directly. The actual API is the one of
|
||||
// the Solver class. The Solver class validates the models before calling this
|
||||
// interface.
|
||||
// interface. It also makes sure no concurrent calls happen on Solve(),
|
||||
// CanUpdate() and Update().
|
||||
//
|
||||
// Implementations of this interface should not have public constructors but
|
||||
// instead have a static `New` function with the signature of Factory function
|
||||
@@ -45,6 +58,22 @@ namespace math_opt {
|
||||
// MATH_OPT_REGISTER_SOLVER().
|
||||
class SolverInterface {
|
||||
public:
|
||||
// Initialization arguments.
|
||||
struct InitArgs {
|
||||
// All parameters that can be stored in a proto and exchange with other
|
||||
// processes.
|
||||
SolverInitializerProto streamable;
|
||||
|
||||
// All parameters that can't be exchanged with another process. The caller
|
||||
// keeps ownership of non_streamable.
|
||||
const NonStreamableSolverInitArguments* non_streamable = nullptr;
|
||||
};
|
||||
|
||||
// A callback function (if non null) for messages emitted by the solver.
|
||||
//
|
||||
// See Solver::MessageCallback documentation for details.
|
||||
using MessageCallback = std::function<void(const std::vector<std::string>&)>;
|
||||
|
||||
// A callback function (if non null) is a function that validates its input
|
||||
// and its output, and if fails, return a status. The invariant is that the
|
||||
// solver implementation can rely on receiving valid data. The implementation
|
||||
@@ -60,10 +89,12 @@ class SolverInterface {
|
||||
// and no public constructors.
|
||||
//
|
||||
// The implementation should assume the input ModelProto is valid and is free
|
||||
// to CHECK-fail if this is not the case.
|
||||
// to CHECK-fail if this is not the case. It should also assume that the input
|
||||
// init_args.streamable and init_args.non_streamable are also either not set
|
||||
// of set to the arguments of the correct solver.
|
||||
using Factory =
|
||||
std::function<absl::StatusOr<std::unique_ptr<SolverInterface>>(
|
||||
const ModelProto& model, const SolverInitializerProto& initializer)>;
|
||||
const ModelProto& model, const InitArgs& init_args)>;
|
||||
|
||||
SolverInterface() = default;
|
||||
SolverInterface(const SolverInterface&) = delete;
|
||||
@@ -77,10 +108,25 @@ class SolverInterface {
|
||||
// expression), the implementation should not keep a reference or copy of
|
||||
// them, as they may become invalid reference after the invocation if this
|
||||
// function.
|
||||
//
|
||||
// Parameters `message_cb`, `cb` and `interrupter` are optional. They are
|
||||
// nullptr when not set.
|
||||
//
|
||||
// When parameter `message_cb` is not null and the underlying solver does not
|
||||
// supports message callbacks, it must return an InvalidArgumentError with the
|
||||
// message internal::kMessageCallbackNotSupported.
|
||||
//
|
||||
// Solvers should return a InvalidArgumentError when called with events on
|
||||
// callback_registration that are not supported by the solver for the type of
|
||||
// model being solved (for example MIP events if the model is an LP, or events
|
||||
// that are not emitted by the solver). Solvers should use
|
||||
// CheckRegisteredCallbackEvents() to implement that.
|
||||
virtual absl::StatusOr<SolveResultProto> Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const CallbackRegistrationProto& callback_registration, Callback cb) = 0;
|
||||
MessageCallback message_cb,
|
||||
const CallbackRegistrationProto& callback_registration, Callback cb,
|
||||
SolveInterrupter* interrupter) = 0;
|
||||
|
||||
// Updates the model to solve.
|
||||
//
|
||||
@@ -107,19 +153,19 @@ class AllSolversRegistry {
|
||||
// MATH_OPT_REGISTER_SOLVER defined below.
|
||||
//
|
||||
// Required: factory must be threadsafe.
|
||||
void Register(SolverType solver_type, SolverInterface::Factory factory);
|
||||
void Register(SolverTypeProto solver_type, SolverInterface::Factory factory);
|
||||
|
||||
// Invokes the factory associated to the solver type with the provided
|
||||
// arguments.
|
||||
absl::StatusOr<std::unique_ptr<SolverInterface>> Create(
|
||||
SolverType solver_type, const ModelProto& model,
|
||||
const SolverInitializerProto& initializer) const;
|
||||
SolverTypeProto solver_type, const ModelProto& model,
|
||||
const SolverInterface::InitArgs& init_args) const;
|
||||
|
||||
// Whether a solver type is supported.
|
||||
bool IsRegistered(SolverType solver_type) const;
|
||||
bool IsRegistered(SolverTypeProto solver_type) const;
|
||||
|
||||
// List all supported solver types.
|
||||
std::vector<SolverType> RegisteredSolvers() const;
|
||||
std::vector<SolverTypeProto> RegisteredSolvers() const;
|
||||
|
||||
// Returns a human-readable list of supported solver types.
|
||||
std::string RegisteredSolversToString() const;
|
||||
@@ -128,7 +174,8 @@ class AllSolversRegistry {
|
||||
AllSolversRegistry() = default;
|
||||
|
||||
mutable absl::Mutex mutex_;
|
||||
absl::flat_hash_map<SolverType, SolverInterface::Factory> registered_solvers_;
|
||||
absl::flat_hash_map<SolverTypeProto, SolverInterface::Factory>
|
||||
registered_solvers_;
|
||||
};
|
||||
|
||||
// Use to ensure that a solver is registered exactly one time. Invoke in each cc
|
||||
@@ -139,7 +186,7 @@ class AllSolversRegistry {
|
||||
// Can only be used once per cc file.
|
||||
//
|
||||
// Arguments:
|
||||
// solver_type: A SolverType proto enum.
|
||||
// solver_type: A SolverTypeProto proto enum.
|
||||
// solver_factory: A SolverInterface::Factory for solver_type.
|
||||
#define MATH_OPT_REGISTER_SOLVER(solver_type, solver_factory) \
|
||||
namespace { \
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "testing/base/public/gmock.h"
|
||||
#include "testing/base/public/gunit.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
|
||||
|
||||
133
ortools/math_opt/core/sparse_submatrix.cc
Normal file
133
ortools/math_opt/core/sparse_submatrix.cc
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/core/sparse_submatrix.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/math_opt/core/sparse_vector.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
|
||||
namespace operations_research::math_opt {
|
||||
namespace {
|
||||
|
||||
// A semi-open range [start, end). If end is nullopt, all indices >= start are
|
||||
// included.
|
||||
struct IndexRange {
|
||||
int64_t start;
|
||||
std::optional<int64_t> end;
|
||||
|
||||
// Returns true if the input value is in the [start, end) range.
|
||||
bool Contains(const int64_t id) const {
|
||||
return id >= start && (!end.has_value() || id < *end);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
SparseSubmatrixRowsView SparseSubmatrixByRows(
|
||||
const SparseDoubleMatrixProto& matrix, const int64_t start_row_id,
|
||||
const std::optional<int64_t> end_row_id, const int64_t start_col_id,
|
||||
const std::optional<int64_t> end_col_id) {
|
||||
const int matrix_size = matrix.row_ids_size();
|
||||
CHECK_EQ(matrix_size, matrix.column_ids_size());
|
||||
CHECK_EQ(matrix_size, matrix.coefficients_size());
|
||||
const IndexRange row_range = {.start = start_row_id, .end = end_row_id};
|
||||
const IndexRange col_range = {.start = start_col_id, .end = end_col_id};
|
||||
|
||||
SparseSubmatrixRowsView filtered_rows;
|
||||
|
||||
// row_start, next_row_start and row_end are indices into the matrix data.
|
||||
for (int row_start = 0, next_row_start; row_start < matrix_size;
|
||||
// next_row_start is set from row_end once found at the start of the loop
|
||||
// below.
|
||||
row_start = next_row_start) {
|
||||
// Find the end of the current row such that all index in [start, end) are
|
||||
// for the same row.
|
||||
const int64_t row_id = matrix.row_ids(row_start);
|
||||
int row_end = row_start + 1;
|
||||
while (row_end < matrix_size && matrix.row_ids(row_end) == row_id) {
|
||||
++row_end;
|
||||
}
|
||||
|
||||
// Prepare the next iteration.
|
||||
next_row_start = row_end;
|
||||
|
||||
// Ignore rows not in the expected range.
|
||||
if (!row_range.Contains(row_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Finds the first column or the row in the col_range.
|
||||
int row_cols_start = row_start;
|
||||
while (row_cols_start < row_end &&
|
||||
!col_range.Contains(matrix.column_ids(row_cols_start))) {
|
||||
++row_cols_start;
|
||||
}
|
||||
|
||||
// Finds the first column greater of equal to row_cols_start that is not in
|
||||
// the col_range.
|
||||
int row_cols_end = row_cols_start;
|
||||
while (row_cols_end < row_end &&
|
||||
col_range.Contains(matrix.column_ids(row_cols_end))) {
|
||||
++row_cols_end;
|
||||
}
|
||||
const int row_cols_len = row_cols_end - row_cols_start;
|
||||
|
||||
if (row_cols_len != 0) {
|
||||
filtered_rows.emplace_back(
|
||||
row_id, MakeView(absl::MakeConstSpan(matrix.column_ids())
|
||||
.subspan(row_cols_start, row_cols_len),
|
||||
absl::MakeConstSpan(matrix.coefficients())
|
||||
.subspan(row_cols_start, row_cols_len)));
|
||||
}
|
||||
}
|
||||
|
||||
return filtered_rows;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int64_t, SparseVector<double>>> TransposeSparseSubmatrix(
|
||||
const SparseSubmatrixRowsView& submatrix_by_rows) {
|
||||
// Extract the columns by iterating on the filtered views of the rows (the
|
||||
// matrix is row major).
|
||||
absl::flat_hash_map<int64_t, SparseVector<double>> filtered_columns;
|
||||
for (const auto& [row_id, column_values] : submatrix_by_rows) {
|
||||
for (const auto [column_id, value] : column_values) {
|
||||
SparseVector<double>& row_values = filtered_columns[column_id];
|
||||
row_values.ids.push_back(row_id);
|
||||
row_values.values.push_back(value);
|
||||
}
|
||||
}
|
||||
|
||||
// The output should be sorted by column id.
|
||||
std::vector<std::pair<int64_t, SparseVector<double>>> sorted_filtered_columns(
|
||||
std::make_move_iterator(filtered_columns.begin()),
|
||||
std::make_move_iterator(filtered_columns.end()));
|
||||
std::sort(sorted_filtered_columns.begin(), sorted_filtered_columns.end(),
|
||||
[](const std::pair<int64_t, SparseVector<double>>& lhs,
|
||||
const std::pair<int64_t, SparseVector<double>>& rhs) {
|
||||
return lhs.first < rhs.first;
|
||||
});
|
||||
|
||||
return sorted_filtered_columns;
|
||||
}
|
||||
|
||||
} // namespace operations_research::math_opt
|
||||
88
ortools/math_opt/core/sparse_submatrix.h
Normal file
88
ortools/math_opt/core/sparse_submatrix.h
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Tools to extract some sub-components of sparse matrices.
|
||||
#ifndef OR_TOOLS_MATH_OPT_CORE_SPARSE_SUBMATRIX_H_
|
||||
#define OR_TOOLS_MATH_OPT_CORE_SPARSE_SUBMATRIX_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/math_opt/core/sparse_vector.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
|
||||
namespace operations_research::math_opt {
|
||||
|
||||
// A vector that contains one pair (row_id, columns_coefficients) per row,
|
||||
// sorted by row_id. The columns_coefficients are views.
|
||||
using SparseSubmatrixRowsView =
|
||||
std::vector<std::pair<int64_t, SparseVectorView<double>>>;
|
||||
|
||||
// Returns the coefficients of columns in the range [start_col_id, end_col_id)
|
||||
// for each row in the range [start_row_id, end_row_id).
|
||||
//
|
||||
// Returns a vector that contains one pair (row_id, columns_coefficients) per
|
||||
// row. It CHECKs that the input matrix is valid. The coefficients are returned
|
||||
// in a views that points to the input matrix's data. Therefore they should not
|
||||
// be used after the proto is modified/deleted.
|
||||
//
|
||||
// When end_(col|row)_id is nullopt, includes all indices greater or equal to
|
||||
// start_(col|row)_id.
|
||||
//
|
||||
// This functions runs in O(size of matrix).
|
||||
//
|
||||
// Use TransposeSparseSubmatrix() to transpose the submatrix and get the
|
||||
// columns instead of the rows.
|
||||
//
|
||||
// Usage example:
|
||||
//
|
||||
// // With this input sparse matrix:
|
||||
// // |0 1 2 3 4 5 6
|
||||
// // -+-------------
|
||||
// // 0|2 - - - 3 4 -
|
||||
// // 1|- - - - - - -
|
||||
// // 2|- 5 - 1 - - 3
|
||||
// // 3|9 - - 8 - - 7
|
||||
// const SparseDoubleMatrixProto matrix = ...;
|
||||
//
|
||||
// // Keeping coefficients of lines >= 1 and columns in [1, 6).
|
||||
// const auto rows = SparseSubmatrixByRows(
|
||||
// matrix,
|
||||
// /*start_row_id=*/1, /*end_row_id=*/std::nullopt,
|
||||
// /*start_col_id=*/1, /*end_col_id=*/6);
|
||||
//
|
||||
// // The returned rows and coefficients will be:
|
||||
// // {2, {{1, 5.0}, {3, 1.0}}}
|
||||
// // {3, { {3, 8.0}}}
|
||||
//
|
||||
SparseSubmatrixRowsView SparseSubmatrixByRows(
|
||||
const SparseDoubleMatrixProto& matrix, int64_t start_row_id,
|
||||
std::optional<int64_t> end_row_id, int64_t start_col_id,
|
||||
std::optional<int64_t> end_col_id);
|
||||
|
||||
// Returns a vector that contains one pair (row_id, rows_coefficients) per
|
||||
// column.
|
||||
//
|
||||
// The coefficients are returned as copies of the input views.
|
||||
//
|
||||
// This functions runs in:
|
||||
// O(num_non_zeros + num_non_empty_cols * lg(num_non_empty_cols)).
|
||||
std::vector<std::pair<int64_t, SparseVector<double>>> TransposeSparseSubmatrix(
|
||||
const SparseSubmatrixRowsView& submatrix_by_rows);
|
||||
|
||||
} // namespace operations_research::math_opt
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CORE_SPARSE_SUBMATRIX_H_
|
||||
36
ortools/math_opt/core/sparse_vector.h
Normal file
36
ortools/math_opt/core/sparse_vector.h
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CORE_SPARSE_VECTOR_H_
|
||||
#define OR_TOOLS_MATH_OPT_CORE_SPARSE_VECTOR_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace operations_research::math_opt {
|
||||
|
||||
// A sparse representation of a vector of values.
|
||||
//
|
||||
// This is equivalent to Sparse(Double|Bool|Int32)VectorProto but for C++.
|
||||
template <typename T>
|
||||
struct SparseVector {
|
||||
// Should be sorted (in increasing order) with all elements distinct.
|
||||
std::vector<int64_t> ids;
|
||||
|
||||
// Must have equal length to ids.
|
||||
std::vector<T> values;
|
||||
};
|
||||
|
||||
} // namespace operations_research::math_opt
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CORE_SPARSE_VECTOR_H_
|
||||
@@ -57,6 +57,8 @@
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/base/map_util.h"
|
||||
#include "ortools/math_opt/core/arrow_operator_proxy.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/core/sparse_vector.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
@@ -96,8 +98,10 @@ class SparseVectorView {
|
||||
using difference_type = int;
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
|
||||
value_type operator*() const;
|
||||
reference operator*() const;
|
||||
inline internal::ArrowOperatorProxy<reference> operator->() const;
|
||||
const_iterator& operator++();
|
||||
bool operator==(const const_iterator& other) const;
|
||||
bool operator!=(const const_iterator& other) const;
|
||||
|
||||
private:
|
||||
@@ -160,6 +164,13 @@ SparseVectorView<T> MakeView(const SparseVectorProto& sparse_vector) {
|
||||
return SparseVectorView<T>(sparse_vector.ids(), sparse_vector.values());
|
||||
}
|
||||
|
||||
// Returns a view for values in a SparseVector. For this case it is preferred
|
||||
// over the two-argument overloads. See other overloads for other values-types.
|
||||
template <typename T>
|
||||
SparseVectorView<T> MakeView(const SparseVector<T>& sparse_vector) {
|
||||
return SparseVectorView<T>(sparse_vector.ids, sparse_vector.values);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Inline implementations
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -174,11 +185,18 @@ SparseVectorView<T>::const_iterator::const_iterator(
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
typename SparseVectorView<T>::const_iterator::value_type
|
||||
typename SparseVectorView<T>::const_iterator::reference
|
||||
SparseVectorView<T>::const_iterator::operator*() const {
|
||||
return {view_->ids(index_), view_->values(index_)};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
internal::ArrowOperatorProxy<
|
||||
typename SparseVectorView<T>::const_iterator::reference>
|
||||
SparseVectorView<T>::const_iterator::operator->() const {
|
||||
return internal::ArrowOperatorProxy<reference>(**this);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
typename SparseVectorView<T>::const_iterator&
|
||||
SparseVectorView<T>::const_iterator::operator++() {
|
||||
@@ -188,10 +206,16 @@ SparseVectorView<T>::const_iterator::operator++() {
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool SparseVectorView<T>::const_iterator::operator!=(
|
||||
bool SparseVectorView<T>::const_iterator::operator==(
|
||||
const const_iterator& other) const {
|
||||
DCHECK_EQ(view_, other.view_);
|
||||
return index_ != other.index_;
|
||||
return index_ == other.index_;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool SparseVectorView<T>::const_iterator::operator!=(
|
||||
const const_iterator& other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
|
||||
@@ -4,29 +4,32 @@ package(default_visibility = ["//ortools/math_opt:__subpackages__"])
|
||||
|
||||
cc_library(
|
||||
name = "math_opt",
|
||||
srcs = ["math_opt.cc"],
|
||||
hdrs = ["math_opt.h"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":callback",
|
||||
":model",
|
||||
":solve",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "model",
|
||||
srcs = ["model.cc"],
|
||||
hdrs = ["model.h"],
|
||||
deps = [
|
||||
":key_types",
|
||||
":linear_constraint",
|
||||
":model_solve_parameters",
|
||||
":objective",
|
||||
":result",
|
||||
":update_tracker",
|
||||
":variable_and_expressions",
|
||||
"//ortools/base",
|
||||
"//ortools/base:int_type",
|
||||
"//ortools/base:status_macros",
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"//ortools/base:int_type",
|
||||
"//ortools/math_opt:model_cc_proto",
|
||||
"//ortools/math_opt:model_update_cc_proto",
|
||||
"//ortools/math_opt:parameters_cc_proto",
|
||||
"//ortools/math_opt:result_cc_proto",
|
||||
"//ortools/math_opt/core:solver",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/memory",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/strings",
|
||||
],
|
||||
@@ -36,11 +39,11 @@ cc_library(
|
||||
name = "id_map",
|
||||
hdrs = ["id_map.h"],
|
||||
deps = [
|
||||
":arrow_operator_proxy",
|
||||
":key_types",
|
||||
"//ortools/base",
|
||||
"//ortools/base:int_type",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"//ortools/math_opt/core:arrow_operator_proxy",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"@com_google_absl//absl/types:span",
|
||||
@@ -57,7 +60,7 @@ cc_library(
|
||||
"//ortools/base",
|
||||
"//ortools/base:int_type",
|
||||
"//ortools/base:map_util",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"@com_google_absl//absl/base:core_headers",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
],
|
||||
@@ -65,7 +68,6 @@ cc_library(
|
||||
|
||||
cc_library(
|
||||
name = "linear_constraint",
|
||||
srcs = ["linear_constraint.cc"],
|
||||
hdrs = ["linear_constraint.h"],
|
||||
deps = [
|
||||
":id_map",
|
||||
@@ -73,37 +75,52 @@ cc_library(
|
||||
":variable_and_expressions",
|
||||
"//ortools/base",
|
||||
"//ortools/base:int_type",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "objective",
|
||||
srcs = ["objective.cc"],
|
||||
hdrs = ["objective.h"],
|
||||
deps = [
|
||||
":key_types",
|
||||
":variable_and_expressions",
|
||||
"//ortools/base",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "result",
|
||||
srcs = ["result.cc"],
|
||||
hdrs = ["result.h"],
|
||||
name = "solution",
|
||||
srcs = ["solution.cc"],
|
||||
hdrs = ["solution.h"],
|
||||
deps = [
|
||||
":enums",
|
||||
":linear_constraint",
|
||||
":variable_and_expressions",
|
||||
"//ortools/base",
|
||||
"//ortools/base:protoutil",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"//ortools/base:int_type",
|
||||
"//ortools/math_opt:result_cc_proto",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"//ortools/math_opt/core:sparse_vector_view",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/types:optional",
|
||||
"@com_google_absl//absl/types:span",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "solve_result",
|
||||
srcs = ["solve_result.cc"],
|
||||
hdrs = ["solve_result.h"],
|
||||
deps = [
|
||||
":enums",
|
||||
":linear_constraint",
|
||||
":solution",
|
||||
":variable_and_expressions",
|
||||
"//ortools/base",
|
||||
"//ortools/base:status_macros",
|
||||
"//ortools/base:protoutil",
|
||||
"//ortools/math_opt:result_cc_proto",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"//ortools/port:proto_utils",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"@com_google_absl//absl/types:span",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -112,9 +129,9 @@ cc_library(
|
||||
hdrs = ["map_filter.h"],
|
||||
deps = [
|
||||
":id_set",
|
||||
"//ortools/base:int_type",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"@com_google_absl//absl/types:optional",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -123,6 +140,7 @@ cc_library(
|
||||
srcs = ["callback.cc"],
|
||||
hdrs = ["callback.h"],
|
||||
deps = [
|
||||
":enums",
|
||||
":key_types",
|
||||
":map_filter",
|
||||
":variable_and_expressions",
|
||||
@@ -131,29 +149,24 @@ cc_library(
|
||||
"//ortools/base:protoutil",
|
||||
"//ortools/base:status_macros",
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"//ortools/math_opt/core:sparse_vector_view",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/time",
|
||||
"@com_google_absl//absl/types:optional",
|
||||
"@com_google_absl//absl/types:span",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "arrow_operator_proxy",
|
||||
hdrs = ["arrow_operator_proxy.h"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "key_types",
|
||||
hdrs = ["key_types.h"],
|
||||
deps = [
|
||||
"//ortools/base",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"@com_google_absl//absl/strings",
|
||||
],
|
||||
)
|
||||
@@ -162,14 +175,11 @@ cc_library(
|
||||
name = "id_set",
|
||||
hdrs = ["id_set.h"],
|
||||
deps = [
|
||||
":arrow_operator_proxy",
|
||||
":key_types",
|
||||
":result",
|
||||
"//ortools/base",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt/core:arrow_operator_proxy",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"@com_google_protobuf//:protobuf",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -181,12 +191,102 @@ cc_library(
|
||||
":key_types",
|
||||
":linear_constraint",
|
||||
":map_filter",
|
||||
":result",
|
||||
":solution",
|
||||
":variable_and_expressions",
|
||||
"//ortools/math_opt/core:indexed_model",
|
||||
"//ortools/math_opt:model_parameters_cc_proto",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"@com_google_protobuf//:protobuf",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "update_tracker",
|
||||
srcs = ["update_tracker.cc"],
|
||||
hdrs = ["update_tracker.h"],
|
||||
deps = [
|
||||
"//ortools/base",
|
||||
"//ortools/math_opt:model_cc_proto",
|
||||
"//ortools/math_opt:model_update_cc_proto",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"@com_google_absl//absl/strings",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "solve",
|
||||
srcs = ["solve.cc"],
|
||||
hdrs = ["solve.h"],
|
||||
deps = [
|
||||
":callback",
|
||||
":key_types",
|
||||
":model",
|
||||
":model_solve_parameters",
|
||||
":parameters",
|
||||
":solve_result",
|
||||
":streamable_solver_init_arguments",
|
||||
"//ortools/base",
|
||||
"//ortools/base:status_macros",
|
||||
"//ortools/base:source_location",
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
"//ortools/math_opt:parameters_cc_proto",
|
||||
"//ortools/math_opt/core:model_storage",
|
||||
"//ortools/math_opt/core:non_streamable_solver_init_arguments",
|
||||
"//ortools/math_opt/core:solve_interrupter",
|
||||
"//ortools/math_opt/core:solver",
|
||||
"@com_google_absl//absl/base:core_headers",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"@com_google_absl//absl/memory",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
#"@com_google_absl//absl/types:source_location",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "streamable_solver_init_arguments",
|
||||
srcs = ["streamable_solver_init_arguments.cc"],
|
||||
hdrs = ["streamable_solver_init_arguments.h"],
|
||||
deps = [
|
||||
"//ortools/math_opt:parameters_cc_proto",
|
||||
"//ortools/math_opt/solvers:gurobi_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "parameters",
|
||||
srcs = ["parameters.cc"],
|
||||
hdrs = ["parameters.h"],
|
||||
deps = [
|
||||
":enums",
|
||||
"//ortools/base:linked_hash_map",
|
||||
"//ortools/base:status_macros",
|
||||
"//ortools/base:protoutil",
|
||||
"//ortools/glop:parameters_cc_proto",
|
||||
"//ortools/gscip:gscip_cc_proto",
|
||||
"//ortools/math_opt:parameters_cc_proto",
|
||||
"//ortools/math_opt/solvers:gurobi_cc_proto",
|
||||
#"//ortools/math_opt/solvers:osqp_settings_cc_proto",
|
||||
#"//ortools/pdlp:solvers_cc_proto",
|
||||
"//ortools/port:proto_utils",
|
||||
"//ortools/sat:sat_parameters_cc_proto",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/time",
|
||||
"@com_google_absl//absl/types:span",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "enums",
|
||||
hdrs = ["enums.h"],
|
||||
deps = [
|
||||
"//ortools/base",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/types:span",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -15,18 +15,18 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
#include "ortools/math_opt/cpp/map_filter.h"
|
||||
@@ -49,22 +49,23 @@ std::vector<std::pair<VariableId, double>> SortedVariableValues(
|
||||
}
|
||||
|
||||
// Container must be an iterable on some type T where
|
||||
// IndexedModel* T::model() const
|
||||
// const ModelStorage* T::storage() const
|
||||
// is defined.
|
||||
//
|
||||
// CHECKs that the non-null models the same, and returns the unique non-null
|
||||
// model if it exists, otherwise null.
|
||||
// CHECKs that the non-null model storages are the same, and returns the unique
|
||||
// non-null model storage if it exists, otherwise null.
|
||||
template <typename Container>
|
||||
IndexedModel* ConsistentModel(const Container& model_items,
|
||||
IndexedModel* const init_model = nullptr) {
|
||||
IndexedModel* result = init_model;
|
||||
const ModelStorage* ConsistentModelStorage(
|
||||
const Container& model_items,
|
||||
const ModelStorage* const init_model = nullptr) {
|
||||
const ModelStorage* result = init_model;
|
||||
for (const auto& item : model_items) {
|
||||
IndexedModel* const model = item.model();
|
||||
if (model != nullptr) {
|
||||
const ModelStorage* const storage = item.storage();
|
||||
if (storage != nullptr) {
|
||||
if (result == nullptr) {
|
||||
result = model;
|
||||
result = storage;
|
||||
} else {
|
||||
CHECK_EQ(model, result) << internal::kObjectsFromOtherIndexedModel;
|
||||
CHECK_EQ(storage, result) << internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,35 +74,65 @@ IndexedModel* ConsistentModel(const Container& model_items,
|
||||
|
||||
} // namespace
|
||||
|
||||
CallbackData::CallbackData(IndexedModel* model, const CallbackDataProto& proto)
|
||||
: event(proto.event()),
|
||||
messages(proto.messages().begin(), proto.messages().end()),
|
||||
std::optional<absl::string_view> Enum<CallbackEvent>::ToOptString(
|
||||
CallbackEvent value) {
|
||||
switch (value) {
|
||||
case CallbackEvent::kPresolve:
|
||||
return "presolve";
|
||||
case CallbackEvent::kSimplex:
|
||||
return "simplex";
|
||||
case CallbackEvent::kMip:
|
||||
return "mip";
|
||||
case CallbackEvent::kMipSolution:
|
||||
return "mip_solution";
|
||||
case CallbackEvent::kMipNode:
|
||||
return "mip_node";
|
||||
case CallbackEvent::kBarrier:
|
||||
return "barrier";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::Span<const CallbackEvent> Enum<CallbackEvent>::AllValues() {
|
||||
static constexpr CallbackEvent kCallbackEventValues[] = {
|
||||
CallbackEvent::kPresolve, CallbackEvent::kSimplex,
|
||||
CallbackEvent::kMip, CallbackEvent::kMipSolution,
|
||||
CallbackEvent::kMipNode, CallbackEvent::kBarrier,
|
||||
};
|
||||
return absl::MakeConstSpan(kCallbackEventValues);
|
||||
}
|
||||
|
||||
CallbackData::CallbackData(const ModelStorage* storage,
|
||||
const CallbackDataProto& proto)
|
||||
// iOS 11 does not support .value() hence we use operator* here and CHECK
|
||||
// below that we have a value.
|
||||
: event(*EnumFromProto(proto.event())),
|
||||
presolve_stats(proto.presolve_stats()),
|
||||
simplex_stats(proto.simplex_stats()),
|
||||
barrier_stats(proto.barrier_stats()),
|
||||
mip_stats(proto.mip_stats()) {
|
||||
if (proto.has_primal_solution()) {
|
||||
CHECK(EnumFromProto(proto.event()).has_value());
|
||||
if (proto.has_primal_solution_vector()) {
|
||||
solution = VariableMap<double>(
|
||||
model, MakeView(proto.primal_solution().variable_values())
|
||||
.as_map<VariableId>());
|
||||
storage, MakeView(proto.primal_solution_vector()).as_map<VariableId>());
|
||||
}
|
||||
auto maybe_time = util_time::DecodeGoogleApiProto(proto.runtime());
|
||||
CHECK_OK(maybe_time.status());
|
||||
runtime = *maybe_time;
|
||||
}
|
||||
|
||||
IndexedModel* CallbackRegistration::model() const {
|
||||
return internal::ConsistentModel(
|
||||
{mip_node_filter.model(), mip_solution_filter.model()});
|
||||
const ModelStorage* CallbackRegistration::storage() const {
|
||||
return internal::ConsistentModelStorage(
|
||||
{mip_node_filter.storage(), mip_solution_filter.storage()});
|
||||
}
|
||||
|
||||
CallbackRegistrationProto CallbackRegistration::Proto() const {
|
||||
// Ensure that the underlying IndexedModel is consistent (or CHECK fail).
|
||||
model();
|
||||
// Ensure that the underlying ModelStorage is consistent (or CHECK fail).
|
||||
storage();
|
||||
|
||||
CallbackRegistrationProto result;
|
||||
for (const CallbackEventProto event : events) {
|
||||
result.add_request_registration(event);
|
||||
for (const CallbackEvent event : events) {
|
||||
result.add_request_registration(EnumToProto(event));
|
||||
}
|
||||
std::sort(result.mutable_request_registration()->begin(),
|
||||
result.mutable_request_registration()->end());
|
||||
@@ -112,22 +143,22 @@ CallbackRegistrationProto CallbackRegistration::Proto() const {
|
||||
return result;
|
||||
}
|
||||
|
||||
IndexedModel* CallbackResult::model() const {
|
||||
IndexedModel* result = ConsistentModel(new_constraints);
|
||||
return ConsistentModel(suggested_solutions, result);
|
||||
const ModelStorage* CallbackResult::storage() const {
|
||||
const ModelStorage* result = ConsistentModelStorage(new_constraints);
|
||||
return ConsistentModelStorage(suggested_solutions, result);
|
||||
}
|
||||
|
||||
CallbackResultProto CallbackResult::Proto() const {
|
||||
// Ensure that the underlying IndexedModel is consistent (or CHECK fail).
|
||||
model();
|
||||
// Ensure that the underlying ModelStorage is consistent (or CHECK fail).
|
||||
storage();
|
||||
|
||||
CallbackResultProto result;
|
||||
result.set_terminate(terminate);
|
||||
for (const VariableMap<double>& solution : suggested_solutions) {
|
||||
PrimalSolutionProto* solution_proto = result.add_suggested_solution();
|
||||
SparseDoubleVectorProto* solution_vector = result.add_suggested_solutions();
|
||||
for (const auto& [typed_id, value] : SortedVariableValues(solution)) {
|
||||
solution_proto->mutable_variable_values()->add_ids(typed_id.value());
|
||||
solution_proto->mutable_variable_values()->add_values(value);
|
||||
solution_vector->add_ids(typed_id.value());
|
||||
solution_vector->add_values(value);
|
||||
}
|
||||
}
|
||||
for (const GeneratedLinearConstraint& constraint : new_constraints) {
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Data types for using callbacks with MathOpt.
|
||||
// Data types for using callbacks with Solve() and IncrementalSolver.
|
||||
//
|
||||
// Callbacks allow to user to observe the progress of a solver and modify its
|
||||
// behavior mid solve. This is supported by allowing the user to a function of
|
||||
// type MathOpt::Callback as an optional argument to MathOpt::Solve(). This
|
||||
// function is called periodically throughout the solve process. This file
|
||||
// defines the data types needed to use this callback.
|
||||
// type Callback as an optional argument to Solve() and
|
||||
// IncrementalSolver::Solve(). This function is called periodically throughout
|
||||
// the solve process. This file defines the data types needed to use this
|
||||
// callback.
|
||||
//
|
||||
// The example below registers a callback that listens for feasible solutions
|
||||
// the solvers finds along the way and accumulates them in a list for analysis
|
||||
@@ -26,14 +27,15 @@
|
||||
// using ::operations_research::math_opt::CallbackData;
|
||||
// using ::operations_research::math_opt::CallbackRegistration;
|
||||
// using ::operations_research::math_opt::CallbackResult;
|
||||
// using ::operations_research::math_opt::MathOpt;
|
||||
// using ::operations_research::math_opt::Result;
|
||||
// using ::operations_research::math_opt::Model;
|
||||
// using ::operations_research::math_opt::SolveResult;
|
||||
// using ::operations_research::math_opt::Solve;
|
||||
// using ::operations_research::math_opt::Variable;
|
||||
// using ::operations_research::math_opt::VariableMap;
|
||||
//
|
||||
// MathOpt model(operations_research::math_opt::SOLVER_TYPE_GUROBI);
|
||||
// Model model;
|
||||
// Variable x = model.AddBinaryVariable();
|
||||
// model.objective().Maximize(x);
|
||||
// model.Maximize(x);
|
||||
// CallbackRegistration cb_reg;
|
||||
// cb_reg.events = {
|
||||
// operations_research::math_opt::CALLBACK_EVENT_MIP_SOLUTION};
|
||||
@@ -44,85 +46,99 @@
|
||||
// solutions.push_back(*cb_data.solution);
|
||||
// return CallbackResult();
|
||||
// };
|
||||
// absl::StatusOr<Result> result = opt.Solve({}, {}, cb_reb, cb);
|
||||
// absl::StatusOr<SolveResult> result = Solve(
|
||||
// model, operations_research::math_opt::SOLVER_TYPE_GUROBI,
|
||||
// /*parameters=*/{}, /*model_parameters=*/{}, cb_reb, cb);
|
||||
//
|
||||
// At the termination of the example, solutions will have {{x, 1.0}}, and
|
||||
// possibly {{x, 0.0}} as well.
|
||||
//
|
||||
// If the callback argument to MathOpt::Solve() is not null, it will be invoked
|
||||
// on the events specified by the callback_registration argument (and when the
|
||||
// If the callback argument to Solve() is not null, it will be invoked on the
|
||||
// events specified by the callback_registration argument (and when the
|
||||
// callback is null, callback_registration must not request any events or will
|
||||
// CHECK fail). Some solvers do not support callbacks or certain events, in this
|
||||
// case the callback is ignored. TODO(b/180617976): change this behavior.
|
||||
//
|
||||
// Some solvers may call callback from multiple threads (SCIP will, Gurobi
|
||||
// will not). You should either solve with one thread (see
|
||||
// solver_parameters.common_parameters.threads), write a threadsafe callback,
|
||||
// or consult the documentation of your underlying solver.
|
||||
// Some solvers may call callback from multiple threads (SCIP will, Gurobi will
|
||||
// not). You should either solve with one thread (see
|
||||
// solver_parameters.threads), write a threadsafe callback, or consult
|
||||
// the documentation of your underlying solver.
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_CALLBACK_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_CALLBACK_H_
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "absl/types/optional.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/enums.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/map_filter.h"
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// The input to the MathOpt::Callback function.
|
||||
//
|
||||
// The information available depends on the current event.
|
||||
struct CallbackData {
|
||||
// Users will typically not need this function.
|
||||
// Will CHECK fail if proto is not valid.
|
||||
CallbackData(IndexedModel* model, const CallbackDataProto& proto);
|
||||
CallbackData() = default;
|
||||
struct CallbackData;
|
||||
struct CallbackResult;
|
||||
|
||||
// The current state of the underlying solver.
|
||||
CallbackEventProto event = CALLBACK_EVENT_UNSPECIFIED;
|
||||
using Callback = std::function<CallbackResult(const CallbackData&)>;
|
||||
|
||||
// If event == CALLBACK_EVENT_MIP_NODE, the primal_solution contains the
|
||||
// primal solution to the current LP-node relaxation. In some cases, no
|
||||
// solution will be available (e.g. because LP was infeasible or the solve
|
||||
// was imprecise).
|
||||
// If event == CALLBACK_EVENT_MIP_SOLUTION, the primal_solution contains the
|
||||
// newly found primal (integer) feasible solution. The solution is always
|
||||
// present.
|
||||
// Otherwise, the primal_solution is not available.
|
||||
absl::optional<VariableMap<double>> solution;
|
||||
// The supported events for LP/MIP callbacks.
|
||||
enum class CallbackEvent {
|
||||
// The solver is currently running presolve.
|
||||
//
|
||||
// This event is supported for MIP & LP models by SolverType::kGurobi. Other
|
||||
// solvers don't support this event.
|
||||
kPresolve = CALLBACK_EVENT_PRESOLVE,
|
||||
|
||||
// If event == CALLBACK_EVENT_MESSAGE, contains the messages from the solver.
|
||||
// Each message represents a single output line from the solver, and each
|
||||
// message does not contain any '\n' characters.
|
||||
// Otherwise, messages is empty.
|
||||
std::vector<std::string> messages;
|
||||
// The solver is currently running the simplex method.
|
||||
//
|
||||
// This event is supported for MIP & LP models by SolverType::kGurobi. Other
|
||||
// solvers don't support this event.
|
||||
kSimplex = CALLBACK_EVENT_SIMPLEX,
|
||||
|
||||
// Time since `Solve()` was called. Available for all events except
|
||||
// CALLBACK_EVENT_POLLING.
|
||||
absl::Duration runtime;
|
||||
// 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.
|
||||
//
|
||||
// This event is supported for MIP models only by SolverType::kGurobi. Other
|
||||
// solvers don't support this event.
|
||||
kMip = CALLBACK_EVENT_MIP,
|
||||
|
||||
// Only available for event == CALLBACK_EVENT_PRESOLVE.
|
||||
CallbackDataProto::PresolveStats presolve_stats;
|
||||
// Called every time a new MIP incumbent is found.
|
||||
//
|
||||
// This event is fully supported for MIP models by SolverType::kGurobi. CP-SAT
|
||||
// has partial support: you can view the solutions and request termination,
|
||||
// but you cannot add lazy constraints. Other solvers don't support this
|
||||
// event.
|
||||
kMipSolution = CALLBACK_EVENT_MIP_SOLUTION,
|
||||
|
||||
// Only available for event == CALLBACK_EVENT_SIMPLEX.
|
||||
CallbackDataProto::SimplexStats simplex_stats;
|
||||
// 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.
|
||||
//
|
||||
// Disabling cuts using CommonSolveParameters may interfere with this event
|
||||
// being called and/or adding cuts at this event, the behavior is solver
|
||||
// specific.
|
||||
//
|
||||
// This event is supported for MIP models only by SolverType::kGurobi. Other
|
||||
// solvers don't support this event.
|
||||
kMipNode = CALLBACK_EVENT_MIP_NODE,
|
||||
|
||||
// Only available for event == CALLBACK_EVENT_BARRIER.
|
||||
CallbackDataProto::BarrierStats barrier_stats;
|
||||
|
||||
// Only available for event of CALLBACK_EVENT_MIP, CALLBACK_EVENT_MIP_NODE, or
|
||||
// CALLBACK_EVENT_MIP_SOLUTION.
|
||||
CallbackDataProto::MipStats mip_stats;
|
||||
// Called in each iterate of an interior point/barrier method.
|
||||
//
|
||||
// This event is supported for LP models only by SolverType::kGurobi. Other
|
||||
// solvers don't support this event.
|
||||
kBarrier = CALLBACK_EVENT_BARRIER,
|
||||
};
|
||||
|
||||
MATH_OPT_DEFINE_ENUM(CallbackEvent, CALLBACK_EVENT_UNSPECIFIED);
|
||||
|
||||
// Provided with a callback at the start of a Solve() to inform the solver:
|
||||
// * what information the callback needs,
|
||||
// * how the callback might alter the solve process.
|
||||
@@ -132,31 +148,77 @@ struct CallbackRegistration {
|
||||
|
||||
// Returns the model referenced variables, or null if no variables are
|
||||
// referenced. Will CHECK fail if variables are not from the same model.
|
||||
IndexedModel* model() const;
|
||||
const ModelStorage* storage() const;
|
||||
|
||||
// The events the solver should invoke the callback at.
|
||||
absl::flat_hash_set<CallbackEventProto> events;
|
||||
//
|
||||
// A solver will return an InvalidArgument status when called with registered
|
||||
// events that are not supported for the selected solver and the type of
|
||||
// model. For example registring for CallbackEvent::kMip with a model that
|
||||
// only contains continuous variables will fail for most solvers (see the
|
||||
// documentation of each event to see which solvers support them and in which
|
||||
// case).
|
||||
absl::flat_hash_set<CallbackEvent> events;
|
||||
|
||||
// Restricts the variable returned in CallbackData.solution for event
|
||||
// CALLBACK_EVENT_MIP_SOLUTION. This can improve performance.
|
||||
// CallbackEvent::kMipSolution. This can improve performance.
|
||||
MapFilter<Variable> mip_solution_filter;
|
||||
|
||||
// Restricts the variable returned in CallbackData.solution for event
|
||||
// CALLBACK_EVENT_MIP_NODE. This can improve performance.
|
||||
// CallbackEvent::kMipNode. This can improve performance.
|
||||
MapFilter<Variable> mip_node_filter;
|
||||
|
||||
// If the callback will ever add "user cuts" at event CALLBACK_EVENT_MIP_NODE
|
||||
// If the callback will ever add "user cuts" at event CallbackEvent::kMipNode
|
||||
// during the solve process (a linear constraint that excludes the current LP
|
||||
// solution but does not cut off any integer points).
|
||||
bool add_cuts = false;
|
||||
|
||||
// If the callback will ever add "lazy constraints" at event
|
||||
// CALLBACK_EVENT_MIP_NODE or CALLBACK_EVENT_MIP_SOLUTION during the solve
|
||||
// CallbackEvent::kMipNode or CallbackEvent::kMipSolution during the solve
|
||||
// process (a linear constraint that excludes integer points).
|
||||
bool add_lazy_constraints = false;
|
||||
};
|
||||
|
||||
// The value returned by the MathOpt::Callback function.
|
||||
// The input to the Callback function.
|
||||
//
|
||||
// The information available depends on the current event.
|
||||
struct CallbackData {
|
||||
// Users will typically not need this function.
|
||||
// Will CHECK fail if proto is not valid.
|
||||
CallbackData(const ModelStorage* storage, const CallbackDataProto& proto);
|
||||
|
||||
// The current state of the underlying solver.
|
||||
CallbackEvent event;
|
||||
|
||||
// If event == CallbackEvent::kMipNode, the primal_solution contains the
|
||||
// primal solution to the current LP-node relaxation. In some cases, no
|
||||
// solution will be available (e.g. because LP was infeasible or the solve
|
||||
// was imprecise).
|
||||
// If event == CallbackEvent::kMipSolution, the primal_solution contains the
|
||||
// newly found primal (integer) feasible solution. The solution is always
|
||||
// present.
|
||||
// Otherwise, the primal_solution is not available.
|
||||
std::optional<VariableMap<double>> solution;
|
||||
|
||||
// Time since `Solve()` was called. Available for all events except
|
||||
// CallbackEvent::kPolling.
|
||||
absl::Duration runtime;
|
||||
|
||||
// Only available for event == CallbackEvent::kPresolve.
|
||||
CallbackDataProto::PresolveStats presolve_stats;
|
||||
|
||||
// Only available for event == CallbackEvent::kSimplex.
|
||||
CallbackDataProto::SimplexStats simplex_stats;
|
||||
|
||||
// Only available for event == CallbackEvent::kBarrier.
|
||||
CallbackDataProto::BarrierStats barrier_stats;
|
||||
|
||||
// Only available for event of CallbackEvent::kMip, CallbackEvent::kMipNode,
|
||||
// or CallbackEvent::kMipSolution.
|
||||
CallbackDataProto::MipStats mip_stats;
|
||||
};
|
||||
|
||||
// The value returned by the Callback function.
|
||||
struct CallbackResult {
|
||||
// Prefer AddUserCut and AddLazyConstraint below instead of using this
|
||||
// directly.
|
||||
@@ -164,18 +226,20 @@ struct CallbackResult {
|
||||
BoundedLinearExpression linear_constraint;
|
||||
bool is_lazy = false;
|
||||
|
||||
IndexedModel* model() const { return linear_constraint.expression.model(); }
|
||||
const ModelStorage* storage() const {
|
||||
return linear_constraint.expression.storage();
|
||||
}
|
||||
};
|
||||
|
||||
// Adds a "user cut," a linear constraint that excludes the current LP
|
||||
// solution but does not cut off any integer points. Use only for
|
||||
// CALLBACK_EVENT_MIP_NODE.
|
||||
// CallbackEvent::kMipNode.
|
||||
void AddUserCut(BoundedLinearExpression linear_constraint) {
|
||||
new_constraints.push_back({std::move(linear_constraint), false});
|
||||
}
|
||||
|
||||
// Adds a "lazy constraint," a linear constraint that excludes integer points.
|
||||
// Use only for CALLBACK_EVENT_MIP_NODE and CALLBACK_EVENT_MIP_SOLUTION.
|
||||
// Use only for CallbackEvent::kMipNode and CallbackEvent::kMipSolution.
|
||||
void AddLazyConstraint(BoundedLinearExpression linear_constraint) {
|
||||
new_constraints.push_back({std::move(linear_constraint), true});
|
||||
}
|
||||
@@ -187,7 +251,7 @@ struct CallbackResult {
|
||||
// referenced. Will CHECK fail if variables are not from the same model.
|
||||
//
|
||||
// Runs in O(num constraints + num suggested solutions).
|
||||
IndexedModel* model() const;
|
||||
const ModelStorage* storage() const;
|
||||
|
||||
// Stop the solve process and return early. Can be called from any event.
|
||||
bool terminate = false;
|
||||
|
||||
345
ortools/math_opt/cpp/enums.h
Normal file
345
ortools/math_opt/cpp/enums.h
Normal file
@@ -0,0 +1,345 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// The MathOpt C++ API defines enums that are used in parameters and results and
|
||||
// that corresponds to Proto generated enums.
|
||||
//
|
||||
// The tools in this header make sure the C++ enums provide the following
|
||||
// features:
|
||||
// * enumerating all enum values
|
||||
// * bidirectional string conversion
|
||||
// * operator<< stream support
|
||||
// * bidirectional proto generated enum conversion
|
||||
//
|
||||
// Example declaration:
|
||||
//
|
||||
// my_file.proto:
|
||||
// enum MyEnumProto {
|
||||
// MY_ENUM_UNSPECIFIED = 0;
|
||||
// MY_ENUM_FIRST_VALUE = 1;
|
||||
// MY_ENUM_SECOND_VALUE = 2;
|
||||
// }
|
||||
//
|
||||
// my_file.h:
|
||||
// enum class MyEnum {
|
||||
// kFirstValue = MY_ENUM_FIRST_VALUE,
|
||||
// kSecondValue = MY_ENUM_SECOND_VALUE,
|
||||
// };
|
||||
//
|
||||
// MATH_OPT_DEFINE_ENUM(MyEnum, MY_ENUM_UNSPECIFIED);
|
||||
//
|
||||
// my_file.cc:
|
||||
// std::optional<absl::string_view>
|
||||
// Enum<MyEnum>::ToOptString(MyEnum value) {
|
||||
// switch (value) {
|
||||
// case MyEnum::kFirstValue:
|
||||
// return "first_value";
|
||||
// case MyEnum::kSecondValue:
|
||||
// return "second_value";
|
||||
// }
|
||||
// return std::nullopt;
|
||||
// }
|
||||
//
|
||||
// absl::Span<const MyEnum> Enum<MyEnum>::AllValues() {
|
||||
// static constexpr MyEnum kMyEnumValues[] = {MyEnum::kFirstValue,
|
||||
// MyEnum::kSecondValue};
|
||||
// return absl::MakeConstSpan(kMyEnumValues);
|
||||
// }
|
||||
//
|
||||
// my_file_test.cc:
|
||||
// #include "ortools/math_opt/cpp/enums_testing.h"
|
||||
// ...
|
||||
// INSTANTIATE_TYPED_TEST_SUITE_P(MyEnum, EnumTest, MyEnum);
|
||||
//
|
||||
// Once this is done, the following functions are available:
|
||||
// * absl::Span<MyEnum> Enum<MyEnum>::AllValues()
|
||||
// * optional<MyEnum> EnumFromString<MyEnum>(string_view)
|
||||
// * string_view EnumToString(MyEnum)
|
||||
// * optional<string_view> EnumToOptString(MyEnum)
|
||||
// * optional<MyEnum> EnumFromProto(MyEnumProto)
|
||||
// * MyEnumProto EnumToProto(optional<MyEnum>)
|
||||
// * MyEnumProto EnumToProto(MyEnum)
|
||||
// * operator<<(MyEnum)
|
||||
// * operator<<(std::optional<MyEnum>)
|
||||
//
|
||||
// See examples of usage in the Enum struct documentation below.
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_ENUMS_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_ENUMS_H_
|
||||
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/types/span.h"
|
||||
|
||||
namespace operations_research::math_opt {
|
||||
|
||||
// This template is specialized for each enum in the C++ API.
|
||||
//
|
||||
// It provides a standard way to query properties of those enums and it is used
|
||||
// by some global functions below to implement conversion from/to string or
|
||||
// proto enum.
|
||||
//
|
||||
// Usage example:
|
||||
//
|
||||
// // Iterating on all enum values.
|
||||
// for (const auto solver_type : Enum<SolverType>::AllValues()) {
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// // Parsing a flag as an enum.
|
||||
// const std::optional<SolverType> solver_type =
|
||||
// EnumFromString(absl::GetFlag(FLAGS_solver_type));
|
||||
// if (!solver_type) {
|
||||
// return util::InvalidArgumentErrorBuilder()
|
||||
// _ << "failed to parse --solver_type value: "
|
||||
// << absl::GetFlag(FLAGS_solver_type);
|
||||
// }
|
||||
//
|
||||
// // Conversion to string.
|
||||
// const SolverType solver_type = ...;
|
||||
// LOG(INFO) << "solver: " << solver_type;
|
||||
// absl::StrCat(EnumToString(solver_type), "_test");
|
||||
// absl::StrCat(EnumToOptString(solver_type).value(), "_test");
|
||||
//
|
||||
// // Conversion to Proto.
|
||||
// const std::optional<SolverType> opt_solver_type = ...;
|
||||
// const SolverTypeProto solver_type_proto = EnumToProto(opt_solver_type);
|
||||
//
|
||||
// // Conversion from Proto.
|
||||
// const SolverTypeProto solver_type_proto = ...;
|
||||
// const std::optional<SolverType> opt_solver_type =
|
||||
// EnumFromProto(solver_type_proto);
|
||||
//
|
||||
// Implementation note: don't specialize directly and instead use the
|
||||
// MATH_OPT_DEFINE_ENUM macro.
|
||||
template <typename E>
|
||||
struct Enum {
|
||||
// Must be true in all implementation. This is used with std::enable_if to
|
||||
// condition the implementation of some overloads.
|
||||
static constexpr bool kIsImplemented = false;
|
||||
|
||||
// The type of the Proto equivalent to this enum.
|
||||
//
|
||||
// (Here we use int as a placeholder so that the code compiles.)
|
||||
using Proto = int;
|
||||
|
||||
// The value Proto enum that represents the unspecified case.
|
||||
static constexpr Proto kProtoUnspecifiedValue = {};
|
||||
|
||||
// Returns a unique string that represent the enum. Returns nullopt if the
|
||||
// input value is not a valid value of the enum.
|
||||
//
|
||||
// The returned string should not include the enum name and should be in
|
||||
// snake_case (e.g. is the enum is kLimitReached, this should return
|
||||
// "limit_reached").
|
||||
//
|
||||
// Please prefer using the global functions EnumToString() (or
|
||||
// EnumToOptString() if support for invalid values is needed) instead to
|
||||
// benefit from automatic template type deduction.
|
||||
static std::optional<absl::string_view> ToOptString(E value);
|
||||
|
||||
// Returns all possible values of the enum.
|
||||
static absl::Span<const E> AllValues();
|
||||
};
|
||||
|
||||
using ProtoEnumIsValid = bool (*)(int);
|
||||
|
||||
// This template is specialized for each enum in the Proto API. It
|
||||
// defines the correspondence with the C++ enum.
|
||||
//
|
||||
// Implementation note: don't specialize directly and instead use the
|
||||
// MATH_OPT_DEFINE_ENUM macro.
|
||||
template <typename P>
|
||||
struct EnumProto {
|
||||
// The type of the C++ enum equivalent to the P proto enum.
|
||||
//
|
||||
// (Here we use void as a placeholder so that the code compiles.)
|
||||
using Cpp = void;
|
||||
|
||||
// The smallest valid enum value.
|
||||
static constexpr P kMin = {};
|
||||
|
||||
// The largest valid enum value.
|
||||
static constexpr P kMax = {};
|
||||
|
||||
// Proto function returning the true if the input integer matches a valid
|
||||
// value (some values may be missing in range [kMin, kMax]).
|
||||
static constexpr ProtoEnumIsValid kIsValid = nullptr;
|
||||
};
|
||||
|
||||
// Returns the Proto enum that matches the input C++ proto, returns
|
||||
// Enum<E>::kProtoUnspecifiedValue if the input is std::nullopt.
|
||||
template <typename E>
|
||||
typename Enum<E>::Proto EnumToProto(const std::optional<E> value);
|
||||
|
||||
// Returns the Proto enum that matches the input C++ proto.
|
||||
//
|
||||
// Implementation note: this overload is necessary for EnumToProto(Xxx::kXxx)
|
||||
// since C++ won't deduce E in std::optional<E> with the other overload.
|
||||
template <typename E>
|
||||
typename Enum<E>::Proto EnumToProto(const E value);
|
||||
|
||||
// Returns the C++ enum that matches the input Proto enum, returns
|
||||
// std::nullopt if the input is kProtoUnspecifiedValue.
|
||||
template <typename P>
|
||||
std::optional<typename EnumProto<P>::Cpp> EnumFromProto(const P proto_value);
|
||||
|
||||
// Returns a unique string that represent the enum.
|
||||
//
|
||||
// It CHECKs that the input is a valid enum value. For most users this should
|
||||
// always be the case since MathOpt don't generates invalid data.
|
||||
//
|
||||
// Prefer using operator<< when possible though. As a side benefice it does not
|
||||
// CHECK but instead prints the integer value of the invalid input.
|
||||
template <typename E>
|
||||
absl::string_view EnumToString(const E value);
|
||||
|
||||
// Returns a unique string that represent the enum. Returns nullopt if the input
|
||||
// value is not a valid value of the enum.
|
||||
template <typename E>
|
||||
std::optional<absl::string_view> EnumToOptString(const E value);
|
||||
|
||||
// Returns the enum value that corresponds to the input string or nullopt if no
|
||||
// enum matches.
|
||||
//
|
||||
// The expected strings are the one returned by EnumToString().
|
||||
//
|
||||
// This is O(n) in complexity so use with care.
|
||||
template <typename E>
|
||||
std::optional<E> EnumFromString(const absl::string_view str);
|
||||
|
||||
// Overload of operator<< for enum types that implements Enum<E>.
|
||||
//
|
||||
// It calls EnumToOptString(), printing the returned value if not nullopt. When
|
||||
// nullopt it prints the enum numeric value instead.
|
||||
template <typename E, typename>
|
||||
std::ostream& operator<<(std::ostream& out, const E value);
|
||||
|
||||
// Overload of operator<< for std::optional<E> when Enum<E> is implemented.
|
||||
//
|
||||
// When the value is nullopt, it prints "<unspecified>", else it prints the enum
|
||||
// value.
|
||||
template <typename E, typename>
|
||||
std::ostream& operator<<(std::ostream& out, const std::optional<E> value);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Template functions implementations after this point.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
template <typename E>
|
||||
typename Enum<E>::Proto EnumToProto(const std::optional<E> value) {
|
||||
return value ? static_cast<typename Enum<E>::Proto>(*value)
|
||||
: Enum<E>::kProtoUnspecifiedValue;
|
||||
}
|
||||
|
||||
template <typename E>
|
||||
typename Enum<E>::Proto EnumToProto(const E value) {
|
||||
return EnumToProto(std::make_optional(value));
|
||||
}
|
||||
|
||||
template <typename P>
|
||||
std::optional<typename EnumProto<P>::Cpp> EnumFromProto(const P proto_value) {
|
||||
if (proto_value == Enum<typename EnumProto<P>::Cpp>::kProtoUnspecifiedValue) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<typename EnumProto<P>::Cpp>(proto_value);
|
||||
}
|
||||
|
||||
template <typename E>
|
||||
absl::string_view EnumToString(const E value) {
|
||||
std::optional<absl::string_view> opt_str = Enum<E>::ToOptString(value);
|
||||
CHECK(opt_str.has_value())
|
||||
<< "invalid value: " << static_cast<std::underlying_type_t<E>>(value);
|
||||
return *opt_str;
|
||||
}
|
||||
|
||||
template <typename E>
|
||||
std::optional<absl::string_view> EnumToOptString(const E value) {
|
||||
return Enum<E>::ToOptString(value);
|
||||
}
|
||||
|
||||
template <typename E>
|
||||
std::optional<E> EnumFromString(const absl::string_view str) {
|
||||
for (const E value : Enum<E>::AllValues()) {
|
||||
if (EnumToOptString(value) == str) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <typename E,
|
||||
// We must use enable_if here to prevent this overload to be selected
|
||||
// for other types than ones that implement Enum<E>.
|
||||
typename = std::enable_if_t<Enum<E>::kIsImplemented>>
|
||||
std::ostream& operator<<(std::ostream& out, const E value) {
|
||||
const std::optional<absl::string_view> opt_str = EnumToOptString(value);
|
||||
if (opt_str.has_value()) {
|
||||
out << *opt_str;
|
||||
} else {
|
||||
out << "<invalid enum (" << static_cast<std::underlying_type_t<E>>(value)
|
||||
<< ")>";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename E,
|
||||
// We must use enable_if here to prevent this overload to be selected
|
||||
// for other types than ones that implement Enum<E>.
|
||||
typename = std::enable_if_t<Enum<E>::kIsImplemented>>
|
||||
std::ostream& operator<<(std::ostream& out, const std::optional<E> opt_value) {
|
||||
if (opt_value.has_value()) {
|
||||
out << *opt_value;
|
||||
} else {
|
||||
out << "<unspecified>";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Macros that defines the templates specializations for Enum and EnumProto.
|
||||
//
|
||||
// The CppEnum parameter is the name of the C++ enum class which values are the
|
||||
// Proto enum values. The C++ enum must contain a value for each value of the
|
||||
// Proto enum but the UNSPECIFIED one. The proto_unspecified_value is the
|
||||
// UNSPECIFIED one.
|
||||
//
|
||||
// It leaves two functions to be implemented in the .cc file:
|
||||
//
|
||||
// absl::string_view Enum<CppEnum>::ToOptString(CppEnum value) {
|
||||
// absl::Span<const CppEnum> Enum<CppEnum>::AllValues();
|
||||
//
|
||||
// See the comment at the top of this file for an example. See the comment on
|
||||
// Enum struct for the functions that can then be used on enums.
|
||||
#define MATH_OPT_DEFINE_ENUM(CppEnum, proto_unspecified_value) \
|
||||
template <> \
|
||||
struct Enum<CppEnum> { \
|
||||
static constexpr bool kIsImplemented = true; \
|
||||
using Proto = CppEnum##Proto; \
|
||||
static constexpr Proto kProtoUnspecifiedValue = proto_unspecified_value; \
|
||||
static std::optional<absl::string_view> ToOptString(CppEnum value); \
|
||||
static absl::Span<const CppEnum> AllValues(); \
|
||||
}; \
|
||||
\
|
||||
template <> \
|
||||
struct EnumProto<CppEnum##Proto> { \
|
||||
using Cpp = CppEnum; \
|
||||
static constexpr CppEnum##Proto kMin = CppEnum##Proto##_MIN; \
|
||||
static constexpr CppEnum##Proto kMax = CppEnum##Proto##_MAX; \
|
||||
static constexpr ProtoEnumIsValid kIsValid = CppEnum##Proto##_IsValid; \
|
||||
} /* missing semicolon to force adding it at the invocation site */
|
||||
|
||||
} // namespace operations_research::math_opt
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_ENUMS_H_
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -26,8 +25,8 @@
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/cpp/arrow_operator_proxy.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/core/arrow_operator_proxy.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
|
||||
namespace operations_research {
|
||||
@@ -139,7 +138,7 @@ class IdMap {
|
||||
inline IdMap(std::initializer_list<value_type> ilist);
|
||||
|
||||
// Typically for internal use only.
|
||||
inline IdMap(IndexedModel* model, StorageType values);
|
||||
inline IdMap(const ModelStorage* storage, StorageType values);
|
||||
|
||||
inline const_iterator cbegin() const;
|
||||
inline const_iterator begin() const;
|
||||
@@ -226,10 +225,10 @@ class IdMap {
|
||||
inline std::vector<V> SortedValues() const;
|
||||
|
||||
const StorageType& raw_map() const { return map_; }
|
||||
IndexedModel* model() const { return model_; }
|
||||
const ModelStorage* storage() const { return storage_; }
|
||||
|
||||
friend bool operator==(const IdMap& lhs, const IdMap& rhs) {
|
||||
return lhs.model_ == rhs.model_ && lhs.map_ == rhs.map_;
|
||||
return lhs.storage_ == rhs.storage_ && lhs.map_ == rhs.map_;
|
||||
}
|
||||
friend bool operator!=(const IdMap& lhs, const IdMap& rhs) {
|
||||
return !(lhs == rhs);
|
||||
@@ -237,21 +236,21 @@ class IdMap {
|
||||
|
||||
private:
|
||||
inline std::vector<IdType> SortedIds() const;
|
||||
// CHECKs that model_ and k.model() matches when this map is not empty
|
||||
// (i.e. its model_ is not null). When it is empty, simply check that
|
||||
// k.model() is not null.
|
||||
// CHECKs that storage_ and k.storage() matches when this map is not empty
|
||||
// (i.e. its storage_ is not null). When it is empty, simply check that
|
||||
// k.storage() is not null.
|
||||
inline void CheckModel(const K& k) const;
|
||||
// Sets model_ to k.model() if this map is empty (i.e. its model_ is
|
||||
// null). Else CHECK that it has the same model. It also CHECK that k.model()
|
||||
// is not null.
|
||||
// Sets storage_ to k.storage() if this map is empty (i.e. its storage_ is
|
||||
// null). Else CHECK that it has the same model. It also CHECK that
|
||||
// k.storage() is not null.
|
||||
inline void CheckOrSetModel(const K& k);
|
||||
// Sets model_ to other.model_ if this map is empty (i.e. its model_ is
|
||||
// Sets storage_ to other.storage_ if this map is empty (i.e. its storage_ is
|
||||
// null). Else if the other map is not empty, CHECK that it has the same
|
||||
// model.
|
||||
inline void CheckOrSetModel(const IdMap& other);
|
||||
|
||||
// Invariant: model == nullptr if and only if map_.empty().
|
||||
IndexedModel* model_ = nullptr;
|
||||
// Invariant: storage == nullptr if and only if map_.empty().
|
||||
const ModelStorage* storage_ = nullptr;
|
||||
StorageType map_;
|
||||
};
|
||||
|
||||
@@ -274,7 +273,7 @@ void swap(IdMap<K, V>& a, IdMap<K, V>& b) {
|
||||
|
||||
template <typename K, typename V>
|
||||
typename IdMap<K, V>::reference IdMap<K, V>::iterator::operator*() const {
|
||||
return reference(K(map_->model_, storage_iterator_->first),
|
||||
return reference(K(map_->storage_, storage_iterator_->first),
|
||||
storage_iterator_->second);
|
||||
}
|
||||
|
||||
@@ -314,7 +313,7 @@ IdMap<K, V>::const_iterator::const_iterator(const iterator& non_const_iterator)
|
||||
template <typename K, typename V>
|
||||
typename IdMap<K, V>::const_iterator::reference
|
||||
IdMap<K, V>::const_iterator::operator*() const {
|
||||
return reference(K(map_->model_, storage_iterator_->first),
|
||||
return reference(K(map_->storage_, storage_iterator_->first),
|
||||
storage_iterator_->second);
|
||||
}
|
||||
|
||||
@@ -349,10 +348,10 @@ IdMap<K, V>::const_iterator::const_iterator(
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
template <typename K, typename V>
|
||||
IdMap<K, V>::IdMap(IndexedModel* model, StorageType values)
|
||||
: model_(model), map_(std::move(values)) {
|
||||
IdMap<K, V>::IdMap(const ModelStorage* storage, StorageType values)
|
||||
: storage_(storage), map_(std::move(values)) {
|
||||
if (!map_.empty()) {
|
||||
CHECK(model_ != nullptr);
|
||||
CHECK(storage_ != nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +398,7 @@ typename IdMap<K, V>::iterator IdMap<K, V>::end() {
|
||||
|
||||
template <typename K, typename V>
|
||||
void IdMap<K, V>::clear() {
|
||||
model_ = nullptr;
|
||||
storage_ = nullptr;
|
||||
map_.clear();
|
||||
}
|
||||
|
||||
@@ -447,7 +446,7 @@ int IdMap<K, V>::erase(const K& k) {
|
||||
CheckModel(k);
|
||||
const int ret = map_.erase(k.typed_id());
|
||||
if (map_.empty()) {
|
||||
model_ = nullptr;
|
||||
storage_ = nullptr;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -456,7 +455,7 @@ template <typename K, typename V>
|
||||
void IdMap<K, V>::erase(const const_iterator pos) {
|
||||
map_.erase(pos.storage_iterator_);
|
||||
if (map_.empty()) {
|
||||
model_ = nullptr;
|
||||
storage_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,7 +464,7 @@ typename IdMap<K, V>::iterator IdMap<K, V>::erase(const const_iterator first,
|
||||
const const_iterator last) {
|
||||
auto ret = map_.erase(first.storage_iterator_, last.storage_iterator_);
|
||||
if (map_.empty()) {
|
||||
model_ = nullptr;
|
||||
storage_ = nullptr;
|
||||
}
|
||||
return iterator(this, std::move(ret));
|
||||
}
|
||||
@@ -473,7 +472,7 @@ typename IdMap<K, V>::iterator IdMap<K, V>::erase(const const_iterator first,
|
||||
template <typename K, typename V>
|
||||
void IdMap<K, V>::swap(IdMap& other) {
|
||||
using std::swap;
|
||||
swap(model_, other.model_);
|
||||
swap(storage_, other.storage_);
|
||||
swap(map_, other.map_);
|
||||
}
|
||||
|
||||
@@ -581,7 +580,7 @@ std::vector<K> IdMap<K, V>::SortedKeys() const {
|
||||
std::vector<K> result;
|
||||
result.reserve(map_.size());
|
||||
for (const IdType id : SortedIds()) {
|
||||
result.push_back(K(model_, id));
|
||||
result.push_back(K(storage_, id));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -609,29 +608,30 @@ std::vector<typename K::IdType> IdMap<K, V>::SortedIds() const {
|
||||
|
||||
template <typename K, typename V>
|
||||
void IdMap<K, V>::CheckModel(const K& k) const {
|
||||
CHECK(k.model() != nullptr) << internal::kKeyHasNullIndexedModel;
|
||||
CHECK(model_ == nullptr || model_ == k.model())
|
||||
<< internal::kObjectsFromOtherIndexedModel;
|
||||
CHECK(k.storage() != nullptr) << internal::kKeyHasNullModelStorage;
|
||||
CHECK(storage_ == nullptr || storage_ == k.storage())
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
|
||||
template <typename K, typename V>
|
||||
void IdMap<K, V>::CheckOrSetModel(const K& k) {
|
||||
CHECK(k.model() != nullptr) << internal::kKeyHasNullIndexedModel;
|
||||
if (model_ == nullptr) {
|
||||
model_ = k.model();
|
||||
CHECK(k.storage() != nullptr) << internal::kKeyHasNullModelStorage;
|
||||
if (storage_ == nullptr) {
|
||||
storage_ = k.storage();
|
||||
} else {
|
||||
CHECK_EQ(model_, k.model()) << internal::kObjectsFromOtherIndexedModel;
|
||||
CHECK_EQ(storage_, k.storage()) << internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename K, typename V>
|
||||
void IdMap<K, V>::CheckOrSetModel(const IdMap& other) {
|
||||
if (model_ == nullptr) {
|
||||
model_ = other.model_;
|
||||
} else if (other.model_ != nullptr) {
|
||||
CHECK_EQ(model_, other.model_) << internal::kObjectsFromOtherIndexedModel;
|
||||
if (storage_ == nullptr) {
|
||||
storage_ = other.storage_;
|
||||
} else if (other.storage_ != nullptr) {
|
||||
CHECK_EQ(storage_, other.storage_)
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
} else {
|
||||
// By construction when other is not empty, it has a non null `model_`.
|
||||
// By construction when other is not empty, it has a non null `storage_`.
|
||||
DCHECK(other.empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/cpp/arrow_operator_proxy.h"
|
||||
#include "ortools/math_opt/core/arrow_operator_proxy.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
|
||||
namespace operations_research {
|
||||
@@ -104,7 +104,7 @@ class IdSet {
|
||||
inline IdSet(std::initializer_list<value_type> ilist);
|
||||
|
||||
// Typically for internal use only.
|
||||
inline IdSet(IndexedModel* model, StorageType values);
|
||||
inline IdSet(const ModelStorage* storage, StorageType values);
|
||||
|
||||
inline const_iterator cbegin() const;
|
||||
inline const_iterator begin() const;
|
||||
@@ -140,27 +140,27 @@ class IdSet {
|
||||
const K& k) const;
|
||||
|
||||
const StorageType& raw_set() const { return set_; }
|
||||
IndexedModel* model() const { return model_; }
|
||||
const ModelStorage* storage() const { return storage_; }
|
||||
|
||||
friend bool operator==(const IdSet& lhs, const IdSet& rhs) {
|
||||
return lhs.model_ == rhs.model_ && lhs.set_ == rhs.set_;
|
||||
return lhs.storage_ == rhs.storage_ && lhs.set_ == rhs.set_;
|
||||
}
|
||||
friend bool operator!=(const IdSet& lhs, const IdSet& rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
private:
|
||||
// CHECKs that model_ and k.model() matches when this set is not empty
|
||||
// (i.e. its model_ is not null). When it is empty, simply check that
|
||||
// k.model() is not null.
|
||||
// CHECKs that storage_ and k.storage() matches when this set is not empty
|
||||
// (i.e. its storage_ is not null). When it is empty, simply check that
|
||||
// k.storage() is not null.
|
||||
inline void CheckModel(const K& k) const;
|
||||
// Sets model_ to k.model() if this set is empty (i.e. its model_ is
|
||||
// null). Else CHECK that it has the same model. It also CHECK that k.model()
|
||||
// is not null.
|
||||
// Sets storage_ to k.storage() if this set is empty (i.e. its storage_ is
|
||||
// null). Else CHECK that it has the same storage. It also CHECK that
|
||||
// k.storage() is not null.
|
||||
inline void CheckOrSetModel(const K& k);
|
||||
|
||||
// Invariant: model == nullptr if and only if set_.empty().
|
||||
IndexedModel* model_ = nullptr;
|
||||
// Invariant: storage == nullptr if and only if set_.empty().
|
||||
const ModelStorage* storage_ = nullptr;
|
||||
StorageType set_;
|
||||
};
|
||||
|
||||
@@ -184,7 +184,7 @@ void swap(IdSet<K>& a, IdSet<K>& b) {
|
||||
template <typename K>
|
||||
typename IdSet<K>::const_iterator::reference
|
||||
IdSet<K>::const_iterator::operator*() const {
|
||||
return K(set_->model_, *storage_iterator_);
|
||||
return K(set_->storage_, *storage_iterator_);
|
||||
}
|
||||
|
||||
template <typename K>
|
||||
@@ -216,10 +216,10 @@ IdSet<K>::const_iterator::const_iterator(
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
template <typename K>
|
||||
IdSet<K>::IdSet(IndexedModel* model, StorageType values)
|
||||
: model_(model), set_(std::move(values)) {
|
||||
IdSet<K>::IdSet(const ModelStorage* storage, StorageType values)
|
||||
: storage_(storage), set_(std::move(values)) {
|
||||
if (!set_.empty()) {
|
||||
CHECK(model_ != nullptr);
|
||||
CHECK(storage_ != nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ typename IdSet<K>::const_iterator IdSet<K>::end() const {
|
||||
|
||||
template <typename K>
|
||||
void IdSet<K>::clear() {
|
||||
model_ = nullptr;
|
||||
storage_ = nullptr;
|
||||
set_.clear();
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ int IdSet<K>::erase(const K& k) {
|
||||
CheckModel(k);
|
||||
const int ret = set_.erase(k.typed_id());
|
||||
if (set_.empty()) {
|
||||
model_ = nullptr;
|
||||
storage_ = nullptr;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -302,7 +302,7 @@ template <typename K>
|
||||
void IdSet<K>::erase(const const_iterator pos) {
|
||||
set_.erase(pos.storage_iterator_);
|
||||
if (set_.empty()) {
|
||||
model_ = nullptr;
|
||||
storage_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ typename IdSet<K>::const_iterator IdSet<K>::erase(const const_iterator first,
|
||||
const const_iterator last) {
|
||||
auto ret = set_.erase(first.storage_iterator_, last.storage_iterator_);
|
||||
if (set_.empty()) {
|
||||
model_ = nullptr;
|
||||
storage_ = nullptr;
|
||||
}
|
||||
return const_iterator(this, std::move(ret));
|
||||
}
|
||||
@@ -319,7 +319,7 @@ typename IdSet<K>::const_iterator IdSet<K>::erase(const const_iterator first,
|
||||
template <typename K>
|
||||
void IdSet<K>::swap(IdSet& other) {
|
||||
using std::swap;
|
||||
swap(model_, other.model_);
|
||||
swap(storage_, other.storage_);
|
||||
swap(set_, other.set_);
|
||||
}
|
||||
|
||||
@@ -353,18 +353,18 @@ IdSet<K>::equal_range(const K& k) const {
|
||||
|
||||
template <typename K>
|
||||
void IdSet<K>::CheckModel(const K& k) const {
|
||||
CHECK(k.model() != nullptr) << internal::kKeyHasNullIndexedModel;
|
||||
CHECK(model_ == nullptr || model_ == k.model())
|
||||
<< internal::kObjectsFromOtherIndexedModel;
|
||||
CHECK(k.storage() != nullptr) << internal::kKeyHasNullModelStorage;
|
||||
CHECK(storage_ == nullptr || storage_ == k.storage())
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
|
||||
template <typename K>
|
||||
void IdSet<K>::CheckOrSetModel(const K& k) {
|
||||
CHECK(k.model() != nullptr) << internal::kKeyHasNullIndexedModel;
|
||||
if (model_ == nullptr) {
|
||||
model_ = k.model();
|
||||
CHECK(k.storage() != nullptr) << internal::kKeyHasNullModelStorage;
|
||||
if (storage_ == nullptr) {
|
||||
storage_ = k.storage();
|
||||
} else {
|
||||
CHECK_EQ(model_, k.model()) << internal::kObjectsFromOtherIndexedModel;
|
||||
CHECK_EQ(storage_, k.storage()) << internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
// collections and should not be needed by users.
|
||||
//
|
||||
// Key types are types that are used as identifiers in the C++ interface where
|
||||
// the IndexedModel is using typed integers. They are pairs of (model,
|
||||
// typed_index) where `model` is a pointer on an IndexedModel and `typed_index`
|
||||
// is the typed integer type used in IndexedModel.
|
||||
// the ModelStorage is using typed integers. They are pairs of (storage,
|
||||
// typed_index) where `storage` is a pointer on an ModelStorage and
|
||||
// `typed_index` is the typed integer type used in ModelStorage.
|
||||
//
|
||||
// A key type K must match the following requirements:
|
||||
// - K::IdType is a typed integer used for indices.
|
||||
// - K has a constructor K(IndexedModel*, K::IdType).
|
||||
// - K::IdType is a value type used for indices.
|
||||
// - K has a constructor K(const ModelStorage*, K::IdType).
|
||||
// - K is a value-semantic type.
|
||||
// - K has a function with signature `K::IdType K::typed_id() const`.
|
||||
// - K has a function with signature `IndexedModel* K::model() const`. It
|
||||
// must return a non-null pointer.
|
||||
// - K has a function with signature `const ModelStorage* K::storage() const`.
|
||||
// It must return a non-null pointer.
|
||||
// - K::IdType is a valid key for absl::flat_hash_map or absl::flat_hash_set
|
||||
// (supports hash and ==).
|
||||
//
|
||||
@@ -41,32 +41,32 @@
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
namespace internal {
|
||||
|
||||
// The CHECK message to use when a KeyType::model() is nullptr.
|
||||
inline constexpr absl::string_view kKeyHasNullIndexedModel =
|
||||
"The input key has null model().";
|
||||
// The CHECK message to use when a KeyType::storage() is nullptr.
|
||||
inline constexpr absl::string_view kKeyHasNullModelStorage =
|
||||
"The input key has null .storage().";
|
||||
|
||||
// The CHECK message to use when two KeyType with different models() are used in
|
||||
// the same collection.
|
||||
inline constexpr absl::string_view kObjectsFromOtherIndexedModel =
|
||||
// The CHECK message to use when two KeyType with different storage() are used
|
||||
// in the same collection.
|
||||
inline constexpr absl::string_view kObjectsFromOtherModelStorage =
|
||||
"The input objects belongs to another model.";
|
||||
|
||||
// CHECKs that the non-null models the same, and returns the unique non-null
|
||||
// model if it exists, otherwise null.
|
||||
inline IndexedModel* ConsistentModel(
|
||||
std::initializer_list<IndexedModel*> models) {
|
||||
IndexedModel* result = nullptr;
|
||||
for (IndexedModel* const model : models) {
|
||||
if (model != nullptr) {
|
||||
// model storage if it exists, otherwise null.
|
||||
inline const ModelStorage* ConsistentModelStorage(
|
||||
std::initializer_list<const ModelStorage*> storages) {
|
||||
const ModelStorage* result = nullptr;
|
||||
for (const ModelStorage* const storage : storages) {
|
||||
if (storage != nullptr) {
|
||||
if (result == nullptr) {
|
||||
result = model;
|
||||
result = storage;
|
||||
} else {
|
||||
CHECK_EQ(model, result) << internal::kObjectsFromOtherIndexedModel;
|
||||
CHECK_EQ(storage, result) << internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/linear_constraint.h"
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
std::vector<Variable> LinearConstraint::RowNonzeros() const {
|
||||
std::vector<Variable> result;
|
||||
for (const VariableId variable :
|
||||
model_->variables_in_linear_constraint(id_)) {
|
||||
result.push_back(Variable(model_, variable));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
BoundedLinearExpression LinearConstraint::AsBoundedLinearExpression() const {
|
||||
LinearExpression terms;
|
||||
for (const VariableId var : model_->variables_in_linear_constraint(id_)) {
|
||||
terms +=
|
||||
Variable(model_, var) * model_->linear_constraint_coefficient(id_, var);
|
||||
}
|
||||
return lower_bound() <= std::move(terms) <= upper_bound();
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
@@ -11,18 +11,17 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// An object oriented wrapper for linear constraints in IndexedModel.
|
||||
// An object oriented wrapper for linear constraints in ModelStorage.
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_LINEAR_CONSTRAINT_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_LINEAR_CONSTRAINT_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/id_map.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h"
|
||||
@@ -30,41 +29,29 @@
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// A value type that references a linear constraint from IndexedModel. Usually
|
||||
// A value type that references a linear constraint from ModelStorage. Usually
|
||||
// this type is passed by copy.
|
||||
class LinearConstraint {
|
||||
public:
|
||||
// The typed integer used for ids.
|
||||
using IdType = LinearConstraintId;
|
||||
|
||||
inline LinearConstraint(IndexedModel* model, LinearConstraintId id);
|
||||
inline LinearConstraint(const ModelStorage* storage, LinearConstraintId id);
|
||||
|
||||
inline int64_t id() const;
|
||||
|
||||
inline LinearConstraintId typed_id() const;
|
||||
inline IndexedModel* model() const;
|
||||
inline const ModelStorage* storage() const;
|
||||
|
||||
inline double lower_bound() const;
|
||||
inline double upper_bound() const;
|
||||
inline const std::string& name() const;
|
||||
|
||||
inline void set_lower_bound(double lower_bound) const;
|
||||
inline void set_upper_bound(double upper_bound) const;
|
||||
|
||||
// Setting a value to 0.0 will delete the {constraint, variable} pair from the
|
||||
// underlying sparse matrix representation (and has no effect if the pair is
|
||||
// not present).
|
||||
inline void set_coefficient(Variable variable, double value) const;
|
||||
|
||||
inline bool is_coefficient_nonzero(Variable variable) const;
|
||||
|
||||
// Returns 0.0 if the variable is not used in the constraint.
|
||||
inline double coefficient(Variable variable) const;
|
||||
|
||||
std::vector<Variable> RowNonzeros() const;
|
||||
|
||||
BoundedLinearExpression AsBoundedLinearExpression() const;
|
||||
|
||||
friend inline bool operator==(const LinearConstraint& lhs,
|
||||
const LinearConstraint& rhs);
|
||||
friend inline bool operator!=(const LinearConstraint& lhs,
|
||||
@@ -75,7 +62,7 @@ class LinearConstraint {
|
||||
const LinearConstraint& linear_constraint);
|
||||
|
||||
private:
|
||||
IndexedModel* model_;
|
||||
const ModelStorage* storage_;
|
||||
LinearConstraintId id_;
|
||||
};
|
||||
|
||||
@@ -95,41 +82,35 @@ int64_t LinearConstraint::id() const { return id_.value(); }
|
||||
|
||||
LinearConstraintId LinearConstraint::typed_id() const { return id_; }
|
||||
|
||||
IndexedModel* LinearConstraint::model() const { return model_; }
|
||||
const ModelStorage* LinearConstraint::storage() const { return storage_; }
|
||||
|
||||
double LinearConstraint::lower_bound() const {
|
||||
return model_->linear_constraint_lower_bound(id_);
|
||||
}
|
||||
double LinearConstraint::upper_bound() const {
|
||||
return model_->linear_constraint_upper_bound(id_);
|
||||
}
|
||||
const std::string& LinearConstraint::name() const {
|
||||
return model_->linear_constraint_name(id_);
|
||||
return storage_->linear_constraint_lower_bound(id_);
|
||||
}
|
||||
|
||||
void LinearConstraint::set_lower_bound(const double lower_bound) const {
|
||||
model_->set_linear_constraint_lower_bound(id_, lower_bound);
|
||||
double LinearConstraint::upper_bound() const {
|
||||
return storage_->linear_constraint_upper_bound(id_);
|
||||
}
|
||||
void LinearConstraint::set_upper_bound(const double upper_bound) const {
|
||||
model_->set_linear_constraint_upper_bound(id_, upper_bound);
|
||||
}
|
||||
void LinearConstraint::set_coefficient(const Variable variable,
|
||||
const double value) const {
|
||||
CHECK_EQ(variable.model(), model_) << internal::kObjectsFromOtherIndexedModel;
|
||||
model_->set_linear_constraint_coefficient(id_, variable.typed_id(), value);
|
||||
|
||||
const std::string& LinearConstraint::name() const {
|
||||
return storage_->linear_constraint_name(id_);
|
||||
}
|
||||
|
||||
bool LinearConstraint::is_coefficient_nonzero(const Variable variable) const {
|
||||
CHECK_EQ(variable.model(), model_) << internal::kObjectsFromOtherIndexedModel;
|
||||
return model_->is_linear_constraint_coefficient_nonzero(id_,
|
||||
variable.typed_id());
|
||||
CHECK_EQ(variable.storage(), storage_)
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
return storage_->is_linear_constraint_coefficient_nonzero(
|
||||
id_, variable.typed_id());
|
||||
}
|
||||
|
||||
double LinearConstraint::coefficient(const Variable variable) const {
|
||||
CHECK_EQ(variable.model(), model_) << internal::kObjectsFromOtherIndexedModel;
|
||||
return model_->linear_constraint_coefficient(id_, variable.typed_id());
|
||||
CHECK_EQ(variable.storage(), storage_)
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
return storage_->linear_constraint_coefficient(id_, variable.typed_id());
|
||||
}
|
||||
|
||||
bool operator==(const LinearConstraint& lhs, const LinearConstraint& rhs) {
|
||||
return lhs.id_ == rhs.id_ && lhs.model_ == rhs.model_;
|
||||
return lhs.id_ == rhs.id_ && lhs.storage_ == rhs.storage_;
|
||||
}
|
||||
|
||||
bool operator!=(const LinearConstraint& lhs, const LinearConstraint& rhs) {
|
||||
@@ -139,7 +120,7 @@ bool operator!=(const LinearConstraint& lhs, const LinearConstraint& rhs) {
|
||||
template <typename H>
|
||||
H AbslHashValue(H h, const LinearConstraint& linear_constraint) {
|
||||
return H::combine(std::move(h), linear_constraint.id_.value(),
|
||||
linear_constraint.model_);
|
||||
linear_constraint.storage_);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostr,
|
||||
@@ -148,9 +129,9 @@ std::ostream& operator<<(std::ostream& ostr,
|
||||
return ostr;
|
||||
}
|
||||
|
||||
LinearConstraint::LinearConstraint(IndexedModel* const model,
|
||||
LinearConstraint::LinearConstraint(const ModelStorage* const storage,
|
||||
const LinearConstraintId id)
|
||||
: model_(model), id_(id) {}
|
||||
: storage_(storage), id_(id) {}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/id_set.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace math_opt {
|
||||
|
||||
// A filter that only keeps some specific key-value pairs of a map.
|
||||
//
|
||||
// It is used to limit the quantity of data returned in a Result or in a
|
||||
// It is used to limit the quantity of data returned in a SolveResult or in a
|
||||
// CallbackResult when the models are huge and the user is only interested in
|
||||
// the values of a subset of the keys.
|
||||
//
|
||||
@@ -69,7 +69,7 @@ struct MapFilter {
|
||||
// // Unset the filter.
|
||||
// filter.filtered_keys.reset();
|
||||
// // alternatively:
|
||||
// filter.filtered_keys = absl::nullopt;
|
||||
// filter.filtered_keys = std::nullopt;
|
||||
//
|
||||
// // Set the filter with an empty list of keys (filtering out all pairs).
|
||||
// //
|
||||
@@ -87,11 +87,11 @@ struct MapFilter {
|
||||
// filter.emplace(decision_vars.begin(), decision_vars.end());
|
||||
//
|
||||
// Prefer using MakeSkipAllFilter() or MakeKeepKeysFilter() when appropriate.
|
||||
absl::optional<IdSet<KeyType>> filtered_keys;
|
||||
std::optional<IdSet<KeyType>> filtered_keys;
|
||||
|
||||
// Returns the model of filtered keys. It returns a non-null value if and only
|
||||
// if the filtered_keys is set and non-empty.
|
||||
inline IndexedModel* model() const;
|
||||
inline const ModelStorage* storage() const;
|
||||
|
||||
// Returns the proto corresponding to this filter.
|
||||
SparseVectorFilterProto Proto() const;
|
||||
@@ -99,7 +99,7 @@ struct MapFilter {
|
||||
|
||||
// Returns a filter that skips all key-value pairs.
|
||||
//
|
||||
// This is typically used to disable the dual data in Result when these are
|
||||
// This is typically used to disable the dual data in SolveResult when these are
|
||||
// ignored by the user.
|
||||
//
|
||||
// Example:
|
||||
@@ -157,8 +157,8 @@ MapFilter<KeyType> MakeKeepKeysFilter(std::initializer_list<KeyType> keys) {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
template <typename KeyType>
|
||||
IndexedModel* MapFilter<KeyType>::model() const {
|
||||
return filtered_keys ? filtered_keys->model() : nullptr;
|
||||
const ModelStorage* MapFilter<KeyType>::storage() const {
|
||||
return filtered_keys ? filtered_keys->storage() : nullptr;
|
||||
}
|
||||
|
||||
template <typename KeyType>
|
||||
|
||||
768
ortools/math_opt/cpp/matchers.cc
Normal file
768
ortools/math_opt/cpp/matchers.cc
Normal file
@@ -0,0 +1,768 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/matchers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/port/proto_utils.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::testing::AllOf;
|
||||
using ::testing::AllOfArray;
|
||||
using ::testing::AnyOf;
|
||||
using ::testing::AnyOfArray;
|
||||
using ::testing::Contains;
|
||||
using ::testing::DoubleNear;
|
||||
using ::testing::Eq;
|
||||
using ::testing::ExplainMatchResult;
|
||||
using ::testing::Field;
|
||||
using ::testing::IsEmpty;
|
||||
using ::testing::Matcher;
|
||||
using ::testing::MatcherInterface;
|
||||
using ::testing::MatchResultListener;
|
||||
using ::testing::Optional;
|
||||
using ::testing::PrintToString;
|
||||
} // namespace
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Printing
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
struct Printer {
|
||||
explicit Printer(const T& t) : value(t) {}
|
||||
|
||||
const T& value;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os, const Printer& printer) {
|
||||
os << PrintToString(printer.value);
|
||||
return os;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
Printer<T> Print(const T& t) {
|
||||
return Printer<T>(t);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PrintTo(const Termination& termination, std::ostream* os) {
|
||||
*os << "{reason: " << termination.reason;
|
||||
if (termination.limit.has_value()) {
|
||||
*os << ", limit: " << *termination.limit;
|
||||
}
|
||||
*os << ", detail: " << Print(termination.detail) << "}";
|
||||
}
|
||||
|
||||
void PrintTo(const PrimalSolution& primal_solution, std::ostream* const os) {
|
||||
*os << "{variable_values: " << Print(primal_solution.variable_values)
|
||||
<< ", objective_value: " << Print(primal_solution.objective_value)
|
||||
<< ", feasibility_status: " << Print(primal_solution.feasibility_status)
|
||||
<< "}";
|
||||
}
|
||||
|
||||
void PrintTo(const DualSolution& dual_solution, std::ostream* const os) {
|
||||
*os << "{dual_values: " << Print(dual_solution.dual_values)
|
||||
<< ", reduced_costs: " << Print(dual_solution.reduced_costs)
|
||||
<< ", objective_value: " << Print(dual_solution.objective_value)
|
||||
<< ", feasibility_status: " << Print(dual_solution.feasibility_status)
|
||||
<< "}";
|
||||
}
|
||||
|
||||
void PrintTo(const PrimalRay& primal_ray, std::ostream* const os) {
|
||||
*os << "{variable_values: " << Print(primal_ray.variable_values) << "}";
|
||||
}
|
||||
|
||||
void PrintTo(const DualRay& dual_ray, std::ostream* const os) {
|
||||
*os << "{dual_values: " << Print(dual_ray.dual_values)
|
||||
<< ", reduced_costs: " << Print(dual_ray.reduced_costs) << "}";
|
||||
}
|
||||
|
||||
void PrintTo(const Basis& basis, std::ostream* const os) {
|
||||
*os << "{variable_status: " << Print(basis.variable_status)
|
||||
<< ", constraint_status: " << Print(basis.constraint_status)
|
||||
<< ", basic_dual_feasibility: " << Print(basis.basic_dual_feasibility)
|
||||
<< "}";
|
||||
}
|
||||
|
||||
void PrintTo(const Solution& solution, std::ostream* const os) {
|
||||
*os << "{primal_solution: " << Print(solution.primal_solution)
|
||||
<< ", dual_solution: " << Print(solution.dual_solution)
|
||||
<< ", basis: " << Print(solution.basis) << "}";
|
||||
}
|
||||
|
||||
void PrintTo(const SolveResult& result, std::ostream* const os) {
|
||||
*os << "{termination: " << Print(result.termination)
|
||||
<< ", warnings: " << Print(result.warnings)
|
||||
<< ", solve_stats: " << Print(result.solve_stats)
|
||||
<< ", solutions: " << Print(result.solutions)
|
||||
<< ", primal_rays: " << Print(result.primal_rays)
|
||||
<< ", dual_rays: " << Print(result.dual_rays) << "}";
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// IdMap Matchers
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename K>
|
||||
class IdMapMatcher : public MatcherInterface<IdMap<K, double>> {
|
||||
public:
|
||||
IdMapMatcher(IdMap<K, double> expected, const bool all_keys,
|
||||
const double tolerance)
|
||||
: expected_(std::move(expected)),
|
||||
all_keys_(all_keys),
|
||||
tolerance_(tolerance) {
|
||||
for (const auto [k, v] : expected_) {
|
||||
CHECK(!std::isnan(v)) << "Illegal NaN for key: " << k;
|
||||
}
|
||||
}
|
||||
|
||||
bool MatchAndExplain(IdMap<K, double> actual,
|
||||
MatchResultListener* const os) const override {
|
||||
for (const auto& [key, value] : expected_) {
|
||||
if (!actual.contains(key)) {
|
||||
*os << "expected key " << key << " not found";
|
||||
return false;
|
||||
}
|
||||
if (!(std::abs(value - actual.at(key)) <= tolerance_)) {
|
||||
*os << "value for key " << key
|
||||
<< " not within tolerance, expected: " << value
|
||||
<< " but found: " << actual.at(key);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Post condition: expected_ is a subset of actual.
|
||||
if (all_keys_ && expected_.size() != actual.size()) {
|
||||
for (const auto& [key, value] : actual) {
|
||||
if (!expected_.contains(key)) {
|
||||
*os << "found unexpected key " << key << " in actual";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// expected_ subset of actual && expected_.size() != actual.size() implies
|
||||
// that there is a member A of actual not in expected. When the loop above
|
||||
// hits A, it will return, thus this line is unreachable.
|
||||
LOG(FATAL) << "unreachable";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void DescribeTo(std::ostream* const os) const override {
|
||||
if (all_keys_) {
|
||||
*os << "has identical keys to ";
|
||||
} else {
|
||||
*os << "keys are contained in ";
|
||||
}
|
||||
PrintTo(expected_, os);
|
||||
*os << " and values within " << tolerance_;
|
||||
}
|
||||
|
||||
void DescribeNegationTo(std::ostream* const os) const override {
|
||||
if (all_keys_) {
|
||||
*os << "either keys differ from ";
|
||||
} else {
|
||||
*os << "either has a key not in ";
|
||||
}
|
||||
PrintTo(expected_, os);
|
||||
*os << " or a value differs by more than " << tolerance_;
|
||||
}
|
||||
|
||||
private:
|
||||
const IdMap<K, double> expected_;
|
||||
const bool all_keys_;
|
||||
const double tolerance_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Matcher<VariableMap<double>> IsNearlySubsetOf(VariableMap<double> expected,
|
||||
double tolerance) {
|
||||
return Matcher<VariableMap<double>>(new IdMapMatcher<Variable>(
|
||||
std::move(expected), /*all_keys=*/false, tolerance));
|
||||
}
|
||||
|
||||
Matcher<VariableMap<double>> IsNear(VariableMap<double> expected,
|
||||
const double tolerance) {
|
||||
return Matcher<VariableMap<double>>(new IdMapMatcher<Variable>(
|
||||
std::move(expected), /*all_keys=*/true, tolerance));
|
||||
}
|
||||
|
||||
Matcher<LinearConstraintMap<double>> IsNearlySubsetOf(
|
||||
LinearConstraintMap<double> expected, double tolerance) {
|
||||
return Matcher<LinearConstraintMap<double>>(
|
||||
new IdMapMatcher<LinearConstraint>(std::move(expected),
|
||||
/*all_keys=*/false, tolerance));
|
||||
}
|
||||
|
||||
Matcher<LinearConstraintMap<double>> IsNear(
|
||||
LinearConstraintMap<double> expected, const double tolerance) {
|
||||
return Matcher<LinearConstraintMap<double>>(
|
||||
new IdMapMatcher<LinearConstraint>(std::move(expected), /*all_keys=*/true,
|
||||
tolerance));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Matcher helpers
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename RayType>
|
||||
class RayMatcher : public MatcherInterface<RayType> {
|
||||
public:
|
||||
RayMatcher(RayType expected, const double tolerance)
|
||||
: expected_(std::move(expected)), tolerance_(tolerance) {}
|
||||
void DescribeTo(std::ostream* os) const final {
|
||||
*os << "after L_inf normalization, is within tolerance: " << tolerance_
|
||||
<< " of expected: ";
|
||||
PrintTo(expected_, os);
|
||||
}
|
||||
void DescribeNegationTo(std::ostream* const os) const final {
|
||||
*os << "after L_inf normalization, is not within tolerance: " << tolerance_
|
||||
<< " of expected: ";
|
||||
PrintTo(expected_, os);
|
||||
}
|
||||
|
||||
protected:
|
||||
const RayType expected_;
|
||||
const double tolerance_;
|
||||
};
|
||||
|
||||
// Alias to use the std::optional templated adaptor.
|
||||
Matcher<double> IsNear(double expected, const double tolerance) {
|
||||
return DoubleNear(expected, tolerance);
|
||||
}
|
||||
|
||||
template <typename Type>
|
||||
Matcher<std::optional<Type>> IsNear(std::optional<Type> expected,
|
||||
const double tolerance) {
|
||||
if (expected.has_value()) {
|
||||
return Optional(IsNear(*expected, tolerance));
|
||||
}
|
||||
return testing::Eq(std::nullopt);
|
||||
}
|
||||
|
||||
// Custom std::optional for basis.
|
||||
Matcher<std::optional<Basis>> BasisIs(const std::optional<Basis>& expected) {
|
||||
if (expected.has_value()) {
|
||||
return Optional(BasisIs(*expected));
|
||||
}
|
||||
return testing::Eq(std::nullopt);
|
||||
}
|
||||
|
||||
testing::Matcher<std::vector<Solution>> IsNear(
|
||||
const std::vector<Solution>& expected_solutions,
|
||||
const SolutionMatcherOptions options) {
|
||||
if (expected_solutions.empty()) {
|
||||
return IsEmpty();
|
||||
}
|
||||
std::vector<Matcher<Solution>> matchers;
|
||||
for (const Solution& sol : expected_solutions) {
|
||||
matchers.push_back(IsNear(sol, options));
|
||||
}
|
||||
return ::testing::ElementsAreArray(matchers);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Matchers for Solutions
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Matcher<PrimalSolution> IsNear(PrimalSolution expected,
|
||||
const double tolerance) {
|
||||
return AllOf(Field("variable_values", &PrimalSolution::variable_values,
|
||||
IsNear(expected.variable_values, tolerance)),
|
||||
Field("objective_value", &PrimalSolution::objective_value,
|
||||
IsNear(expected.objective_value, tolerance)),
|
||||
Field("feasibility_status", &PrimalSolution::feasibility_status,
|
||||
expected.feasibility_status));
|
||||
}
|
||||
|
||||
Matcher<DualSolution> IsNear(DualSolution expected, const double tolerance) {
|
||||
return AllOf(Field("dual_values", &DualSolution::dual_values,
|
||||
IsNear(expected.dual_values, tolerance)),
|
||||
Field("reduced_costs", &DualSolution::reduced_costs,
|
||||
IsNear(expected.reduced_costs, tolerance)),
|
||||
Field("objective_value", &DualSolution::objective_value,
|
||||
IsNear(expected.objective_value, tolerance)),
|
||||
Field("feasibility_status", &DualSolution::feasibility_status,
|
||||
expected.feasibility_status));
|
||||
}
|
||||
|
||||
Matcher<Basis> BasisIs(const Basis& expected) {
|
||||
return AllOf(Field("variable_status", &Basis::variable_status,
|
||||
expected.variable_status),
|
||||
Field("constraint_status", &Basis::constraint_status,
|
||||
expected.constraint_status),
|
||||
Field("basic_dual_feasibility", &Basis::basic_dual_feasibility,
|
||||
expected.basic_dual_feasibility));
|
||||
}
|
||||
|
||||
Matcher<Solution> IsNear(Solution expected,
|
||||
const SolutionMatcherOptions options) {
|
||||
std::vector<Matcher<Solution>> to_check;
|
||||
if (options.check_primal) {
|
||||
to_check.push_back(
|
||||
Field("primal_solution", &Solution::primal_solution,
|
||||
IsNear(expected.primal_solution, options.tolerance)));
|
||||
}
|
||||
if (options.check_dual) {
|
||||
to_check.push_back(
|
||||
Field("dual_solution", &Solution::dual_solution,
|
||||
IsNear(expected.dual_solution, options.tolerance)));
|
||||
}
|
||||
if (options.check_basis) {
|
||||
to_check.push_back(
|
||||
Field("basis", &Solution::basis, BasisIs(expected.basis)));
|
||||
}
|
||||
return AllOfArray(to_check);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Primal Ray Matcher
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename K>
|
||||
double InfinityNorm(const IdMap<K, double>& vector) {
|
||||
double infinity_norm = 0.0;
|
||||
for (auto [id, value] : vector) {
|
||||
infinity_norm = std::max(infinity_norm, std::abs(value));
|
||||
}
|
||||
return infinity_norm;
|
||||
}
|
||||
|
||||
// Returns a normalized primal ray.
|
||||
//
|
||||
// The normalization is done using infinity norm:
|
||||
//
|
||||
// ray / ||ray||_inf
|
||||
//
|
||||
// If the input ray norm is zero, the ray is returned unchanged.
|
||||
PrimalRay NormalizePrimalRay(PrimalRay ray) {
|
||||
const double norm = InfinityNorm(ray.variable_values);
|
||||
if (norm != 0.0) {
|
||||
for (auto entry : ray.variable_values) {
|
||||
entry.second /= norm;
|
||||
}
|
||||
}
|
||||
return ray;
|
||||
}
|
||||
|
||||
class PrimalRayMatcher : public RayMatcher<PrimalRay> {
|
||||
public:
|
||||
PrimalRayMatcher(PrimalRay expected, const double tolerance)
|
||||
: RayMatcher(std::move(expected), tolerance) {}
|
||||
|
||||
bool MatchAndExplain(PrimalRay actual,
|
||||
MatchResultListener* const os) const override {
|
||||
auto normalized_actual = NormalizePrimalRay(actual);
|
||||
auto normalized_expected = NormalizePrimalRay(expected_);
|
||||
if (os->IsInterested()) {
|
||||
*os << "actual normalized: " << PrintToString(normalized_actual)
|
||||
<< ", expected normalized: " << PrintToString(normalized_expected);
|
||||
}
|
||||
return ExplainMatchResult(
|
||||
IsNear(normalized_expected.variable_values, tolerance_),
|
||||
normalized_actual.variable_values, os);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Matcher<PrimalRay> IsNear(PrimalRay expected, const double tolerance) {
|
||||
return Matcher<PrimalRay>(
|
||||
new PrimalRayMatcher(std::move(expected), tolerance));
|
||||
}
|
||||
|
||||
Matcher<PrimalRay> PrimalRayIsNear(VariableMap<double> expected_var_values,
|
||||
const double tolerance) {
|
||||
PrimalRay expected;
|
||||
expected.variable_values = std::move(expected_var_values);
|
||||
return IsNear(expected, tolerance);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Dual Ray Matcher
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
namespace {
|
||||
|
||||
// Returns a normalized dual ray.
|
||||
//
|
||||
// The normalization is done using infinity norm:
|
||||
//
|
||||
// ray / ||ray||_inf
|
||||
//
|
||||
// If the input ray norm is zero, the ray is returned unchanged.
|
||||
DualRay NormalizeDualRay(DualRay ray) {
|
||||
const double norm =
|
||||
std::max(InfinityNorm(ray.dual_values), InfinityNorm(ray.reduced_costs));
|
||||
if (norm != 0.0) {
|
||||
for (auto entry : ray.dual_values) {
|
||||
entry.second /= norm;
|
||||
}
|
||||
for (auto entry : ray.reduced_costs) {
|
||||
entry.second /= norm;
|
||||
}
|
||||
}
|
||||
return ray;
|
||||
}
|
||||
|
||||
class DualRayMatcher : public RayMatcher<DualRay> {
|
||||
public:
|
||||
DualRayMatcher(DualRay expected, const double tolerance)
|
||||
: RayMatcher(std::move(expected), tolerance) {}
|
||||
|
||||
bool MatchAndExplain(DualRay actual, MatchResultListener* os) const override {
|
||||
auto normalized_actual = NormalizeDualRay(actual);
|
||||
auto normalized_expected = NormalizeDualRay(expected_);
|
||||
if (os->IsInterested()) {
|
||||
*os << "actual normalized: " << PrintToString(normalized_actual)
|
||||
<< ", expected normalized: " << PrintToString(normalized_expected);
|
||||
}
|
||||
return ExplainMatchResult(
|
||||
IsNear(normalized_expected.dual_values, tolerance_),
|
||||
normalized_actual.dual_values, os) &&
|
||||
ExplainMatchResult(
|
||||
IsNear(normalized_expected.reduced_costs, tolerance_),
|
||||
normalized_actual.reduced_costs, os);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Matcher<DualRay> IsNear(DualRay expected, const double tolerance) {
|
||||
return Matcher<DualRay>(new DualRayMatcher(std::move(expected), tolerance));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// SolveResult termination reason matchers
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Matcher<SolveResult> TerminatesWithOneOf(
|
||||
const std::vector<TerminationReason>& allowed, const bool check_warnings) {
|
||||
std::vector<Matcher<SolveResult>> matchers;
|
||||
matchers.push_back(
|
||||
Field("termination", &SolveResult::termination,
|
||||
Field("reason", &Termination::reason, AnyOfArray(allowed))));
|
||||
if (check_warnings) {
|
||||
matchers.push_back(Field("warnings", &SolveResult::warnings, IsEmpty()));
|
||||
}
|
||||
return ::testing::AllOfArray(matchers);
|
||||
}
|
||||
|
||||
Matcher<SolveResult> TerminatesWith(const TerminationReason expected,
|
||||
const bool check_warnings) {
|
||||
std::vector<Matcher<SolveResult>> matchers;
|
||||
matchers.push_back(Field("termination", &SolveResult::termination,
|
||||
Field("reason", &Termination::reason, expected)));
|
||||
if (check_warnings) {
|
||||
matchers.push_back(Field("warnings", &SolveResult::warnings, IsEmpty()));
|
||||
}
|
||||
return ::testing::AllOfArray(matchers);
|
||||
}
|
||||
|
||||
testing::Matcher<SolveResult> TerminatesWithLimit(Limit expected,
|
||||
bool allow_limit_undetermined,
|
||||
bool check_warnings) {
|
||||
std::vector<Matcher<SolveResult>> matchers;
|
||||
matchers.push_back(Field(
|
||||
"termination", &SolveResult::termination,
|
||||
Field("reason", &Termination::reason, TerminationReason::kLimitReached)));
|
||||
if (allow_limit_undetermined) {
|
||||
matchers.push_back(Field("termination", &SolveResult::termination,
|
||||
Field("limit", &Termination::limit,
|
||||
AnyOf(Limit::kUndetermined, expected))));
|
||||
} else {
|
||||
matchers.push_back(Field("termination", &SolveResult::termination,
|
||||
Field("limit", &Termination::limit, expected)));
|
||||
}
|
||||
if (check_warnings) {
|
||||
matchers.push_back(Field("warnings", &SolveResult::warnings, IsEmpty()));
|
||||
}
|
||||
return ::testing::AllOfArray(matchers);
|
||||
}
|
||||
|
||||
template <typename MatcherType>
|
||||
std::string MatcherToStringImpl(const MatcherType& matcher, const bool negate) {
|
||||
std::ostringstream os;
|
||||
if (negate) {
|
||||
matcher.DescribeNegationTo(&os);
|
||||
} else {
|
||||
matcher.DescribeTo(&os);
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::string MatcherToString(const Matcher<T>& matcher, bool negate) {
|
||||
return MatcherToStringImpl(matcher, negate);
|
||||
}
|
||||
|
||||
// Polymorphic matchers do not always define DescribeTo, see
|
||||
// The <T> type may not be a matcher, but it will implement DescribeTo.
|
||||
template <typename T>
|
||||
std::string MatcherToString(const ::testing::PolymorphicMatcher<T>& matcher,
|
||||
bool negate) {
|
||||
return MatcherToStringImpl(matcher.impl(), negate);
|
||||
}
|
||||
|
||||
MATCHER_P(FirstElementIs, first_element_matcher,
|
||||
(negation
|
||||
? absl::StrCat("is empty or first element ",
|
||||
MatcherToString(first_element_matcher, true))
|
||||
: absl::StrCat("has at least one element and first element ",
|
||||
MatcherToString(first_element_matcher, false)))) {
|
||||
return ExplainMatchResult(UnorderedElementsAre(first_element_matcher),
|
||||
absl::MakeSpan(arg).subspan(0, 1), result_listener);
|
||||
}
|
||||
|
||||
Matcher<SolveResult> IsOptimal(const std::optional<double> expected_objective,
|
||||
const bool check_warnings,
|
||||
const double tolerance) {
|
||||
std::vector<Matcher<SolveResult>> matchers;
|
||||
matchers.push_back(Field(
|
||||
"termination", &SolveResult::termination,
|
||||
Field("reason", &Termination::reason, TerminationReason::kOptimal)));
|
||||
if (check_warnings) {
|
||||
matchers.push_back(Field("warnings", &SolveResult::warnings, IsEmpty()));
|
||||
}
|
||||
if (expected_objective.has_value()) {
|
||||
matchers.push_back(Field(
|
||||
"solutions", &SolveResult::solutions,
|
||||
FirstElementIs(Field(
|
||||
"primal_solution", &Solution::primal_solution,
|
||||
Optional(Field("objective_value", &PrimalSolution::objective_value,
|
||||
IsNear(*expected_objective, tolerance)))))));
|
||||
}
|
||||
return ::testing::AllOfArray(matchers);
|
||||
}
|
||||
|
||||
Matcher<SolveResult> IsOptimalWithSolution(
|
||||
const double expected_objective,
|
||||
const VariableMap<double> expected_variable_values,
|
||||
const bool check_warnings, const double tolerance) {
|
||||
return AllOf(
|
||||
IsOptimal(std::make_optional(expected_objective), check_warnings,
|
||||
tolerance),
|
||||
HasSolution(
|
||||
PrimalSolution{.variable_values = expected_variable_values,
|
||||
.objective_value = expected_objective,
|
||||
.feasibility_status = SolutionStatus::kFeasible},
|
||||
tolerance));
|
||||
}
|
||||
|
||||
Matcher<SolveResult> IsOptimalWithDualSolution(
|
||||
const double expected_objective,
|
||||
const LinearConstraintMap<double> expected_dual_values,
|
||||
const VariableMap<double> expected_reduced_costs, const bool check_warnings,
|
||||
const double tolerance) {
|
||||
return AllOf(
|
||||
IsOptimal(std::make_optional(expected_objective), check_warnings,
|
||||
tolerance),
|
||||
HasDualSolution(
|
||||
DualSolution{
|
||||
.dual_values = expected_dual_values,
|
||||
.reduced_costs = expected_reduced_costs,
|
||||
.objective_value = std::make_optional(expected_objective),
|
||||
.feasibility_status = SolutionStatus::kFeasible},
|
||||
tolerance));
|
||||
}
|
||||
|
||||
Matcher<SolveResult> HasSolution(PrimalSolution expected,
|
||||
const double tolerance) {
|
||||
return ::testing::Field(
|
||||
"solutions", &SolveResult::solutions,
|
||||
Contains(Field("primal_solution", &Solution::primal_solution,
|
||||
Optional(IsNear(std::move(expected), tolerance)))));
|
||||
}
|
||||
|
||||
Matcher<SolveResult> HasDualSolution(DualSolution expected,
|
||||
const double tolerance) {
|
||||
return ::testing::Field(
|
||||
"solutions", &SolveResult::solutions,
|
||||
Contains(Field("dual_solution", &Solution::dual_solution,
|
||||
Optional(IsNear(std::move(expected), tolerance)))));
|
||||
}
|
||||
|
||||
Matcher<SolveResult> HasPrimalRay(PrimalRay expected, const double tolerance) {
|
||||
return ::testing::Field("primal_rays", &SolveResult::primal_rays,
|
||||
Contains(IsNear(std::move(expected), tolerance)));
|
||||
}
|
||||
|
||||
Matcher<SolveResult> HasPrimalRay(VariableMap<double> expected_vars,
|
||||
const double tolerance) {
|
||||
PrimalRay ray;
|
||||
ray.variable_values = std::move(expected_vars);
|
||||
return HasPrimalRay(std::move(ray), tolerance);
|
||||
}
|
||||
|
||||
Matcher<SolveResult> HasDualRay(DualRay expected, const double tolerance) {
|
||||
return ::testing::Field("dual_rays", &SolveResult::dual_rays,
|
||||
Contains(IsNear(std::move(expected), tolerance)));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool MightTerminateWithRays(const TerminationReason reason) {
|
||||
switch (reason) {
|
||||
case TerminationReason::kInfeasibleOrUnbounded:
|
||||
case TerminationReason::kUnbounded:
|
||||
case TerminationReason::kInfeasible:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<TerminationReason> CompatibleReasons(
|
||||
const TerminationReason expected, const bool inf_or_unb_soft_match) {
|
||||
if (!inf_or_unb_soft_match) {
|
||||
return {expected};
|
||||
}
|
||||
switch (expected) {
|
||||
case TerminationReason::kUnbounded:
|
||||
return {TerminationReason::kUnbounded,
|
||||
TerminationReason::kInfeasibleOrUnbounded};
|
||||
case TerminationReason::kInfeasible:
|
||||
return {TerminationReason::kInfeasible,
|
||||
TerminationReason::kInfeasibleOrUnbounded};
|
||||
case TerminationReason::kInfeasibleOrUnbounded:
|
||||
return {TerminationReason::kUnbounded, TerminationReason::kInfeasible,
|
||||
TerminationReason::kInfeasibleOrUnbounded};
|
||||
default:
|
||||
return {expected};
|
||||
}
|
||||
}
|
||||
|
||||
Matcher<std::vector<Solution>> CheckSolutions(
|
||||
const std::vector<Solution>& expected_solutions,
|
||||
const SolveResultMatcherOptions& options) {
|
||||
if (options.first_solution_only && !expected_solutions.empty()) {
|
||||
return FirstElementIs(
|
||||
IsNear(expected_solutions[0],
|
||||
SolutionMatcherOptions{.tolerance = options.tolerance,
|
||||
.check_primal = true,
|
||||
.check_dual = options.check_dual,
|
||||
.check_basis = options.check_basis}));
|
||||
}
|
||||
return IsNear(expected_solutions,
|
||||
SolutionMatcherOptions{.tolerance = options.tolerance,
|
||||
.check_primal = true,
|
||||
.check_dual = options.check_dual,
|
||||
.check_basis = options.check_basis});
|
||||
}
|
||||
|
||||
template <typename RayType>
|
||||
Matcher<std::vector<RayType>> AnyRayNear(
|
||||
const std::vector<RayType>& expected_rays, const double tolerance) {
|
||||
std::vector<Matcher<RayType>> matchers;
|
||||
for (const RayType& ray : expected_rays) {
|
||||
matchers.push_back(IsNear(ray, tolerance));
|
||||
}
|
||||
return ::testing::Contains(::testing::AnyOfArray(matchers));
|
||||
}
|
||||
|
||||
template <typename RayType>
|
||||
Matcher<std::vector<RayType>> AllRaysNear(
|
||||
const std::vector<RayType>& expected_rays, const double tolerance) {
|
||||
std::vector<Matcher<RayType>> matchers;
|
||||
for (const RayType& ray : expected_rays) {
|
||||
matchers.push_back(IsNear(ray, tolerance));
|
||||
}
|
||||
return ::testing::UnorderedElementsAreArray(matchers);
|
||||
}
|
||||
|
||||
template <typename RayType>
|
||||
Matcher<std::vector<RayType>> CheckRays(
|
||||
const std::vector<RayType>& expected_rays, const double tolerance,
|
||||
bool check_all) {
|
||||
if (expected_rays.empty()) {
|
||||
return ::testing::IsEmpty();
|
||||
}
|
||||
if (check_all) {
|
||||
return AllRaysNear(expected_rays, tolerance);
|
||||
}
|
||||
return AnyRayNear(expected_rays, tolerance);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Matcher<SolveResult> IsConsistentWith(
|
||||
const SolveResult& expected, const SolveResultMatcherOptions& options) {
|
||||
std::vector<Matcher<SolveResult>> to_check;
|
||||
to_check.push_back(
|
||||
TerminatesWithOneOf(CompatibleReasons(expected.termination.reason,
|
||||
options.inf_or_unb_soft_match),
|
||||
/*check_warnings=*/false));
|
||||
if (options.check_warnings) {
|
||||
to_check.push_back(
|
||||
Field("warnings", &SolveResult::warnings,
|
||||
::testing::UnorderedElementsAreArray(expected.warnings)));
|
||||
}
|
||||
const bool skip_solution =
|
||||
MightTerminateWithRays(expected.termination.reason) &&
|
||||
!options.check_solutions_if_inf_or_unbounded;
|
||||
if (!skip_solution) {
|
||||
to_check.push_back(Field("solutions", &SolveResult::solutions,
|
||||
CheckSolutions(expected.solutions, options)));
|
||||
}
|
||||
if (options.check_rays) {
|
||||
to_check.push_back(Field("primal_rays", &SolveResult::primal_rays,
|
||||
CheckRays(expected.primal_rays, options.tolerance,
|
||||
!options.first_solution_only)));
|
||||
to_check.push_back(Field("dual_rays", &SolveResult::dual_rays,
|
||||
CheckRays(expected.dual_rays, options.tolerance,
|
||||
!options.first_solution_only)));
|
||||
}
|
||||
|
||||
return AllOfArray(to_check);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Rarely used
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Matcher<IncrementalSolver::UpdateResult> DidUpdate() {
|
||||
return ::testing::Field("did_update",
|
||||
&IncrementalSolver::UpdateResult::did_update,
|
||||
::testing::IsTrue());
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
402
ortools/math_opt/cpp/matchers.h
Normal file
402
ortools/math_opt/cpp/matchers.h
Normal file
@@ -0,0 +1,402 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Matchers for MathOpt types, specifically SolveResult and nested fields.
|
||||
//
|
||||
// The matchers defined here are useful for writing unit tests checking that the
|
||||
// result of Solve(), absl::StatusOr<SolveResult>, meets expectations. We give
|
||||
// some examples below. All code is assumed with the following setup:
|
||||
//
|
||||
// namespace operations_research::math_opt {
|
||||
// using ::testing::status::IsOkAndHolds;
|
||||
//
|
||||
// Model model;
|
||||
// const Variable x = model.AddContinuousVariable(0.0, 1.0);
|
||||
// const Variable y = model.AddContinuousVariable(0.0, 1.0);
|
||||
// const LinearConstraint c = model.AddLinearConstraint(x + y <= 1);
|
||||
// model.Maximize(2*x + y);
|
||||
//
|
||||
// Example 1.a: result is OK, optimal, and objective value approximately 42.
|
||||
// EXPECT_THAT(Solve(model, SOLVER_TYPE_GLOP), IsOkAndHolds(IsOptimal(42)));
|
||||
//
|
||||
// Example 1.b: equivalent to 1.a.
|
||||
// ASSERT_OK_AND_ASSIGN(const SolveResult result,
|
||||
// Solve(model, SOLVER_TYPE_GLOP));
|
||||
// EXPECT_THAT(result, IsOptimal(42));
|
||||
//
|
||||
// Example 2: result is OK, optimal, and best solution is x=1, y=0.
|
||||
// ASSERT_OK_AND_ASSIGN(const SolveResult result,
|
||||
// Solve(model, SOLVER_TYPE_GLOP));
|
||||
// ASSERT_THAT(result, IsOptimal());
|
||||
// EXPECT_THAT(result.variable_value(), IsNear({{x, 1}, {y, 0}});
|
||||
// Note: the second ASSERT ensures that if the solution is not optimal, then
|
||||
// result.variable_value() will not run (the function will crash if the solver
|
||||
// didn't find a solution). Further, MathOpt guarantees there is a solution
|
||||
// when the termination reason is optimal.
|
||||
//
|
||||
// Example 3: result is OK, check the solution without specifying termination.
|
||||
// ASSERT_OK_AND_ASSIGN(const SolveResult result,
|
||||
// Solve(model, SOLVER_TYPE_GLOP));
|
||||
// EXPECT_THAT(result, HasBestSolution({{x, 1}, {y, 0}}));
|
||||
//
|
||||
// Example 4: multiple possible termination reason, primal ray optional:
|
||||
// ASSERT_OK_AND_ASSIGN(const SolveResult result,
|
||||
// Solve(model, SOLVER_TYPE_GLOP));
|
||||
// ASSERT_THAT(result, TerminatesWithOneOf(
|
||||
// TerminationReason::kUnbounded,
|
||||
// TerminationReason::kInfeasibleOrUnbounded));
|
||||
// if(!result.primal_rays.empty()) {
|
||||
// EXPECT_THAT(result.primal_rays[0], IsNear({{x, 1,}, {y, 0}}));
|
||||
// }
|
||||
//
|
||||
//
|
||||
// Tips on writing good tests:
|
||||
// * Use ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(...)) to ensure
|
||||
// the test terminates immediately if Solve() does not return OK.
|
||||
// * If you ASSERT_THAT(result, IsOptimal()), you can assume that you have a
|
||||
// feasible primal solution afterwards. Otherwise, make no assumptions on
|
||||
// the contents of result (e.g. do not assume result contains a primal ray
|
||||
// just because the termination reason was UNBOUNDED).
|
||||
// * For problems that are infeasible, the termination reasons INFEASIBLE and
|
||||
// DUAL_INFEASIBLE are both possible. Likewise, for unbounded problems, you
|
||||
// can get both UNBOUNDED and DUAL_INFEASIBLE. See TerminatesWithOneOf()
|
||||
// below to make assertions in this case. Note also that some solvers have
|
||||
// solver specific parameters to ensure that DUAL_INFEASIBLE will not be
|
||||
// returned (e.g. for Gurobi, use DualReductions or InfUnbdInfo).
|
||||
// * The objective value and variable values should always be compared up to
|
||||
// a tolerance, even if your decision variables are integer. The matchers
|
||||
// defined have a configurable tolerance with default value 1e-5.
|
||||
// * Primal and dual rays are unique only up to a constant scaling. The
|
||||
// matchers provided rescale both expected and actual before comparing.
|
||||
// * Take care on problems with multiple optimal solutions. Do not rely on a
|
||||
// particular solution being returned in your test, as the test will break
|
||||
// when we upgrade the solver.
|
||||
//
|
||||
// This file also defines functions to let gunit print various MathOpt types.
|
||||
//
|
||||
// To see the error messages these matchers generate, run
|
||||
// blaze test experimental/users/rander/math_opt:matchers_error_messages
|
||||
// which is a fork of matchers_test.cc where the assertions are all negated
|
||||
// (note that every test should fail).
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_
|
||||
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "ortools/math_opt/cpp/linear_constraint.h"
|
||||
#include "ortools/math_opt/cpp/solve.h"
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
constexpr double kMatcherDefaultTolerance = 1e-5;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Matchers for IdMap<Variable,double> and IdMap<LinearConstraint, double>
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Checks that the maps have identical keys and values within tolerance.
|
||||
testing::Matcher<VariableMap<double>> IsNear(
|
||||
VariableMap<double> expected, double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Checks that the keys of actual are a subset of the keys of expected, and that
|
||||
// for all shared keys, the values are within tolerance.
|
||||
testing::Matcher<VariableMap<double>> IsNearlySubsetOf(
|
||||
VariableMap<double> expected, double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Checks that the maps have identical keys and values within tolerance.
|
||||
testing::Matcher<LinearConstraintMap<double>> IsNear(
|
||||
LinearConstraintMap<double> expected,
|
||||
double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Checks that the keys of actual are a subset of the keys of expected, and that
|
||||
// for all shared keys, the values are within tolerance.
|
||||
testing::Matcher<LinearConstraintMap<double>> IsNearlySubsetOf(
|
||||
LinearConstraintMap<double> expected,
|
||||
double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Matchers for solutions
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Options for IsNear(Solution).
|
||||
struct SolutionMatcherOptions {
|
||||
double tolerance = kMatcherDefaultTolerance;
|
||||
bool check_primal = true;
|
||||
bool check_dual = true;
|
||||
bool check_basis = true;
|
||||
};
|
||||
|
||||
testing::Matcher<Solution> IsNear(Solution expected,
|
||||
SolutionMatcherOptions options = {});
|
||||
|
||||
// Checks variables match and variable/objective values are within tolerance and
|
||||
// feasibility statuses are identical.
|
||||
testing::Matcher<PrimalSolution> IsNear(
|
||||
PrimalSolution expected, double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Checks dual variables, reduced costs and objective are within tolerance and
|
||||
// feasibility statuses are identical.
|
||||
testing::Matcher<DualSolution> IsNear(
|
||||
DualSolution expected, double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
testing::Matcher<Basis> BasisIs(const Basis& expected);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Matchers for a Rays
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Checks variables match and that after rescaling, variable values are within
|
||||
// tolerance.
|
||||
testing::Matcher<PrimalRay> IsNear(PrimalRay expected,
|
||||
double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Checks variables match and that after rescaling, variable values are within
|
||||
// tolerance.
|
||||
testing::Matcher<PrimalRay> PrimalRayIsNear(
|
||||
VariableMap<double> expected_var_values,
|
||||
double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Checks that dual variables and reduced costs are defined for the same
|
||||
// set of Variables/LinearConstraints, and that their rescaled values are within
|
||||
// tolerance.
|
||||
testing::Matcher<DualRay> IsNear(DualRay expected,
|
||||
double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Matchers for a SolveResult
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Checks the following:
|
||||
// * The termination reason is optimal.
|
||||
// * If expected_objective contains a value, there is at least one feasible
|
||||
// solution and that solution has an objective value within tolerance of
|
||||
// expected_objective.
|
||||
// * If check_warnings, the result has no warnings.
|
||||
testing::Matcher<SolveResult> IsOptimal(
|
||||
std::optional<double> expected_objective = std::nullopt,
|
||||
bool check_warnings = true, double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
testing::Matcher<SolveResult> IsOptimalWithSolution(
|
||||
double expected_objective, VariableMap<double> expected_variable_values,
|
||||
bool check_warnings = true, double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
testing::Matcher<SolveResult> IsOptimalWithDualSolution(
|
||||
double expected_objective, LinearConstraintMap<double> expected_dual_values,
|
||||
VariableMap<double> expected_reduced_costs, bool check_warnings = true,
|
||||
double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Checks the following:
|
||||
// * The result has the expected termination reason.
|
||||
// * If check_warnings, the result has no warnings.
|
||||
testing::Matcher<SolveResult> TerminatesWith(TerminationReason expected,
|
||||
bool check_warnings = true);
|
||||
|
||||
// Checks the following:
|
||||
// * The result has one of the allowed termination reasons.
|
||||
// * If check_warnings, the result has no warnings.
|
||||
testing::Matcher<SolveResult> TerminatesWithOneOf(
|
||||
const std::vector<TerminationReason>& allowed, bool check_warnings = true);
|
||||
|
||||
// Checks the following:
|
||||
// * The result has termination reason kLimitReached.
|
||||
// * The limit is expected, or is kUndetermined if allow_limit_undetermined.
|
||||
// * If check_warnings, the result has no warnings.
|
||||
testing::Matcher<SolveResult> TerminatesWithLimit(
|
||||
Limit expected, bool allow_limit_undetermined = false,
|
||||
bool check_warnings = true);
|
||||
|
||||
// SolveResult has a primal solution matching expected within tolerance.
|
||||
testing::Matcher<SolveResult> HasSolution(
|
||||
PrimalSolution expected, double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// SolveResult has a dual solution matching expected within
|
||||
// tolerance.
|
||||
testing::Matcher<SolveResult> HasDualSolution(
|
||||
DualSolution expected, double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Actual SolveResult contains a primal ray that matches expected within
|
||||
// tolerance.
|
||||
testing::Matcher<SolveResult> HasPrimalRay(
|
||||
PrimalRay expected, double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Actual SolveResult contains a primal ray with variable values equivalent to
|
||||
// (under L_inf scaling) expected_vars up to tolerance.
|
||||
testing::Matcher<SolveResult> HasPrimalRay(
|
||||
VariableMap<double> expected_vars,
|
||||
double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Actual SolveResult contains a dual ray that matches expected within
|
||||
// tolerance.
|
||||
testing::Matcher<SolveResult> HasDualRay(
|
||||
DualRay expected, double tolerance = kMatcherDefaultTolerance);
|
||||
|
||||
// Configures SolveResult matcher IsConsistentWith() below.
|
||||
struct SolveResultMatcherOptions {
|
||||
bool check_warnings = true;
|
||||
double tolerance = 1e-5;
|
||||
bool first_solution_only = true;
|
||||
bool check_dual = true;
|
||||
bool check_rays = true;
|
||||
|
||||
// If the expected result has termination reason kInfeasible, kUnbounded, or
|
||||
// kDualInfeasasible, the primal solution, dual solution, and basis are
|
||||
// ignored unless check_solutions_if_inf_or_unbounded is true.
|
||||
//
|
||||
// TODO(b/201099290): this is perhaps not a good default. Gurobi as
|
||||
// implemented is returning primal solutions for both unbounded and
|
||||
// infeasible problems. We need to add unit tests that inspect this value
|
||||
// and turn them on one solver at a time with a new parameter on
|
||||
// SimpleLpTestParameters.
|
||||
bool check_solutions_if_inf_or_unbounded = false;
|
||||
bool check_basis = false;
|
||||
|
||||
// In linear programming, the following outcomes are all possible
|
||||
//
|
||||
// Primal LP | Dual LP | Possible MathOpt Termination Reasons
|
||||
// -----------------------------------------------------------------
|
||||
// 1. Infeasible | Unbounded | kInfeasible
|
||||
// 2. Optimal | Optimal | kOptimal
|
||||
// 3. Unbounded | Infeasible | kUnbounded, kInfeasibleOrUnbounded
|
||||
// 4. Infeasible | Infeasible | kInfeasible, kInfeasibleOrUnbounded
|
||||
//
|
||||
// (Above "Optimal" means that an optimal solution exists. This is a statement
|
||||
// about the existence of optimal solutions and certificates of
|
||||
// infeasibility/unboundedness, not about the outcome of applying any
|
||||
// particular algorithm.)
|
||||
//
|
||||
// When writing your unit test, you can typically tell which case of 1-4 you
|
||||
// are in, but in cases 3-4 you do not know which termination reason will be
|
||||
// returned. In some situations, it may not be clear if you are in case 1 or
|
||||
// case 4 as well.
|
||||
//
|
||||
// When inf_or_unb_soft_match=false, the matcher must exactly specify the
|
||||
// status returned by the solver. For cases 3-4, this is implementation
|
||||
// dependent and we do not recommend this. When
|
||||
// inf_or_unb_soft_match=true:
|
||||
// * kInfeasible can also match kInfeasibleOrUnbounded
|
||||
// * kUnbounded can also match kInfeasibleOrUnbounded
|
||||
// * kInfeasibleOrUnbounded can also match kInfeasible and kUnbounded.
|
||||
// For case 2, inf_or_unb_soft_match has no effect.
|
||||
//
|
||||
// To build the strongest possible matcher (accepting the minimal set of
|
||||
// termination reasons):
|
||||
// * If you know you are in case 1, se inf_or_unb_soft_match=false
|
||||
// (soft_match=true over-matches)
|
||||
// * For case 3, use inf_or_unb_soft_match=false and
|
||||
// termination_reason=kUnbounded (kInfeasibleOrUnbounded over-matches).
|
||||
// * For case 4 (or if you are unsure of case 1 vs case 4), use
|
||||
// inf_or_unb_soft_match=true and
|
||||
// termination_reason=kInfeasible (kInfeasibleOrUnbounded over-matches).
|
||||
// * If you cannot tell if you are in case 3 or case 4, use
|
||||
// inf_or_unb_soft_match=true and termination reason
|
||||
// kInfeasibleOrUnbounded.
|
||||
//
|
||||
// If the above is too complicated, always setting
|
||||
// inf_or_unb_soft_match=true and using any of the expected MathOpt
|
||||
// termination reasons from the above table will give a matcher that is
|
||||
// slightly too lenient.
|
||||
bool inf_or_unb_soft_match = true;
|
||||
};
|
||||
|
||||
// Tests that two SolveResults are equivalent. Basic use:
|
||||
//
|
||||
// SolveResult expected;
|
||||
// // Fill in expected...
|
||||
// ASSERT_OK_AND_ASSIGN(SolveResult actual, Solve(model, solver_type));
|
||||
// EXPECT_THAT(actual, IsConsistentWith(expected));
|
||||
//
|
||||
// Equivalence is defined as follows:
|
||||
// * The warnings are the same (in any order).
|
||||
// - Disabled if options.check_warnings=false.
|
||||
// * The termination reasons are the same.
|
||||
// - For infeasible and unbounded problems, see
|
||||
// options.inf_or_unb_soft_match.
|
||||
// * The solve stats are ignored.
|
||||
// * For both primal and dual solutions, either expected and actual are
|
||||
// both empty, or their first entries satisfy IsNear() at options.tolerance.
|
||||
// - Not checked if options.check_solutions_if_inf_or_unbounded and the
|
||||
// problem is infeasible or unbounded (default).
|
||||
// - If options.first_solution_only is false, check the entire list of
|
||||
// solutions matches in the same order.
|
||||
// - Dual solution is not checked if options.check_dual=false
|
||||
// * For both the primal and dual rays, either expected and actual are both
|
||||
// empty, or any ray in expected IsNear() any ray in actual (which is up
|
||||
// to a rescaling) at options.tolerance.
|
||||
// - Not checked if options.check_rays=false
|
||||
// - If options.first_solution_only is false, check the entire list of
|
||||
// solutions matches in the same order.
|
||||
// * The basis is not checked by default. If enabled, checked with BasisIs().
|
||||
// - Enable with options.check_basis
|
||||
//
|
||||
// This function is symmetric in that:
|
||||
// EXPECT_THAT(actual, IsConsistentWith(expected));
|
||||
// EXPECT_THAT(expected, IsConsistentWith(actual));
|
||||
// agree on matching, they only differ in strings produced. Per gmock
|
||||
// conventions, prefer the former.
|
||||
//
|
||||
// For problems with either primal or dual infeasibility, see
|
||||
// SolveResultMatcherOptions::inf_or_unb_soft_match for guidance on how to
|
||||
// best set the termination reason and inf_or_unb_soft_match.
|
||||
testing::Matcher<SolveResult> IsConsistentWith(
|
||||
const SolveResult& expected, const SolveResultMatcherOptions& options = {});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Rarely used
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Actual UpdateResult.did_update is true.
|
||||
testing::Matcher<IncrementalSolver::UpdateResult> DidUpdate();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Implementation details
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO(b/200835670): use the << operator on Termination instead once it
|
||||
// supports quoting/escaping on termination.detail.
|
||||
void PrintTo(const Termination& termination, std::ostream* os);
|
||||
void PrintTo(const PrimalSolution& primal_solution, std::ostream* os);
|
||||
void PrintTo(const DualSolution& dual_solution, std::ostream* os);
|
||||
void PrintTo(const PrimalRay& primal_ray, std::ostream* os);
|
||||
void PrintTo(const DualRay& dual_ray, std::ostream* os);
|
||||
void PrintTo(const Basis& basis, std::ostream* os);
|
||||
void PrintTo(const SolveResult& result, std::ostream* os);
|
||||
|
||||
// We do not want to rely on ::testing::internal::ContainerPrinter because we
|
||||
// want to sort the keys.
|
||||
template <typename K, typename V>
|
||||
void PrintTo(const IdMap<K, V>& id_map, std::ostream* const os) {
|
||||
constexpr int kMaxPrint = 10;
|
||||
int num_added = 0;
|
||||
*os << "{";
|
||||
for (const K k : id_map.SortedKeys()) {
|
||||
if (num_added > 0) {
|
||||
*os << ", ";
|
||||
}
|
||||
if (num_added >= kMaxPrint) {
|
||||
*os << "...(size=" << id_map.size() << ")";
|
||||
break;
|
||||
}
|
||||
*os << "{" << k << ", " << ::testing::PrintToString(id_map.at(k)) << "}";
|
||||
++num_added;
|
||||
}
|
||||
*os << "}";
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_MATCHERS_H_
|
||||
@@ -1,166 +0,0 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/math_opt.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/core/solver.h"
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
#include "ortools/math_opt/model_update.pb.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
absl::StatusOr<Result> MathOpt::Solve(
|
||||
const SolveParametersProto& solver_parameters,
|
||||
const ModelSolveParameters& model_parameters,
|
||||
const CallbackRegistration& callback_registration, Callback callback) {
|
||||
CheckModel(model_parameters.model());
|
||||
CheckModel(callback_registration.model());
|
||||
if (callback == nullptr) {
|
||||
CHECK(callback_registration.events.empty())
|
||||
<< "No callback was provided to run, but callback events were "
|
||||
"registered.";
|
||||
}
|
||||
|
||||
bool attempted_incremental_solve = false;
|
||||
if (solver_ != nullptr) {
|
||||
const absl::optional<ModelUpdateProto> model_update =
|
||||
update_tracker_->ExportModelUpdate();
|
||||
bool did_update = false;
|
||||
if (model_update == absl::nullopt) {
|
||||
did_update = true;
|
||||
} else {
|
||||
ASSIGN_OR_RETURN(did_update, solver_->Update(*model_update));
|
||||
update_tracker_->Checkpoint();
|
||||
}
|
||||
if (did_update) {
|
||||
attempted_incremental_solve = true;
|
||||
} else {
|
||||
solver_ = nullptr;
|
||||
// Note that we could keep the same tracker but it is simpler to have both
|
||||
// solver_ and update_tracker_ synchronized. This removes the need for an
|
||||
// extra branch below where we would have solver_ == nullptr but
|
||||
// update_tracker_ != nullptr.
|
||||
//
|
||||
// This code will be removed when b/185769575 is addressed since we won't
|
||||
// have a use-case where solver_ == nullptr anymore (the class that will
|
||||
// represent an incremental solve will always have a solver by
|
||||
// construction).
|
||||
update_tracker_ = nullptr;
|
||||
}
|
||||
}
|
||||
if (solver_ == nullptr) {
|
||||
update_tracker_ = model_->NewUpdateTracker();
|
||||
ASSIGN_OR_RETURN(solver_, Solver::New(solver_type_, model_->ExportModel(),
|
||||
solver_initializer_));
|
||||
}
|
||||
|
||||
Solver::Callback cb = nullptr;
|
||||
if (callback != nullptr) {
|
||||
cb = [&](const CallbackDataProto& callback_data_proto) {
|
||||
const CallbackData data(model_.get(), callback_data_proto);
|
||||
const CallbackResult result = callback(data);
|
||||
CheckModel(result.model());
|
||||
return result.Proto();
|
||||
};
|
||||
}
|
||||
ASSIGN_OR_RETURN(const SolveResultProto solve_result,
|
||||
solver_->Solve(solver_parameters, model_parameters.Proto(),
|
||||
callback_registration.Proto(), cb));
|
||||
Result result(model_.get(), solve_result);
|
||||
result.attempted_incremental_solve = attempted_incremental_solve;
|
||||
return result;
|
||||
}
|
||||
|
||||
LinearConstraint MathOpt::AddLinearConstraint(
|
||||
const BoundedLinearExpression& bounded_expr, absl::string_view name) {
|
||||
CheckModel(bounded_expr.expression.model());
|
||||
|
||||
const LinearConstraintId constraint = model_->AddLinearConstraint(
|
||||
bounded_expr.lower_bound_minus_offset(),
|
||||
bounded_expr.upper_bound_minus_offset(), name);
|
||||
for (auto [variable, coef] : bounded_expr.expression.raw_terms()) {
|
||||
model_->set_linear_constraint_coefficient(constraint, variable, coef);
|
||||
}
|
||||
return LinearConstraint(model_.get(), constraint);
|
||||
}
|
||||
|
||||
std::vector<Variable> MathOpt::Variables() {
|
||||
std::vector<Variable> result;
|
||||
result.reserve(model_->num_variables());
|
||||
for (const VariableId var_id : model_->variables()) {
|
||||
result.push_back(Variable(model_.get(), var_id));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Variable> MathOpt::SortedVariables() {
|
||||
std::vector<Variable> result = Variables();
|
||||
std::sort(result.begin(), result.end(),
|
||||
[](const Variable& l, const Variable& r) {
|
||||
return l.typed_id() < r.typed_id();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<LinearConstraint> MathOpt::ColumnNonzeros(const Variable variable) {
|
||||
std::vector<LinearConstraint> result;
|
||||
for (const LinearConstraintId constraint :
|
||||
model_->linear_constraints_with_variable(variable.typed_id())) {
|
||||
result.push_back(LinearConstraint(model_.get(), constraint));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<LinearConstraint> MathOpt::LinearConstraints() {
|
||||
std::vector<LinearConstraint> result;
|
||||
result.reserve(model_->num_linear_constraints());
|
||||
for (const LinearConstraintId lin_con_id : model_->linear_constraints()) {
|
||||
result.push_back(LinearConstraint(model_.get(), lin_con_id));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<LinearConstraint> MathOpt::SortedLinearConstraints() {
|
||||
std::vector<LinearConstraint> result = LinearConstraints();
|
||||
std::sort(result.begin(), result.end(),
|
||||
[](const LinearConstraint& l, const LinearConstraint& r) {
|
||||
return l.typed_id() < r.typed_id();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
ModelProto MathOpt::ExportModel() const { return model_->ExportModel(); }
|
||||
|
||||
void MathOpt::CheckModel(IndexedModel* model) {
|
||||
if (model != nullptr) {
|
||||
CHECK_EQ(model, model_.get()) << internal::kObjectsFromOtherIndexedModel;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
@@ -11,370 +11,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// A C++ API for building optimization problems.
|
||||
//
|
||||
// Warning: Variable, LinearConstraint, and Objective are value types, see
|
||||
// "Memory Model" below.
|
||||
//
|
||||
// A simple example:
|
||||
//
|
||||
// Model the problem:
|
||||
// max 2.0 * x + y
|
||||
// s.t. x + y <= 1.5
|
||||
// x in {0.0, 1.0}
|
||||
// y in [0.0, 2.5]
|
||||
//
|
||||
// using ::operations_research::math_opt::LinearConstraint;
|
||||
// using ::operations_research::math_opt::Objective;
|
||||
// using ::operations_research::math_opt::MathOpt;
|
||||
// using ::operations_research::math_opt::Result;
|
||||
// using ::operations_research::math_opt::SolveParameters;
|
||||
// using ::operations_research::math_opt::SolveResultProto;
|
||||
// using ::operations_research::math_opt::Variable;
|
||||
//
|
||||
// Version 1:
|
||||
//
|
||||
// MathOpt optimizer(operations_research::math_opt::SOLVER_TYPE_GSCIP,
|
||||
// "my_model");
|
||||
// const Variable x = optimizer.AddBinaryVariable("x");
|
||||
// const Variable y = optimizer.AddContinuousVariable(0.0, 2.5, "y");
|
||||
// const LinearConstraint c = optimizer.AddLinearConstraint(
|
||||
// -std::numeric_limits<double>::infinity(), 1.5, "c");
|
||||
// c.set_coefficient(x, 1.0);
|
||||
// c.set_coefficient(y, 1.0);
|
||||
// const Objective obj = optimizer.objective();
|
||||
// obj.set_linear_coefficient(x, 2.0);
|
||||
// obj.set_linear_coefficient(y, 1.0);
|
||||
// obj.set_maximize();
|
||||
// const Result result = optimizer.Solve(SolveParametersProto()).value();
|
||||
// for (const auto& warning : result.warnings) {
|
||||
// std::cerr << "Solver warning: " << warning << std::endl;
|
||||
// }
|
||||
// CHECK_EQ(result.termination_reason, SolveResultProto::OPTIMAL)
|
||||
// << result.termination_detail;
|
||||
// // The following code will print:
|
||||
// // objective value: 2.5
|
||||
// // value for variable x: 1
|
||||
// std::cout << "objective value: " << result.objective_value()
|
||||
// << "\nvalue for variable x: " << result.variable_values().at(x)
|
||||
// << std::endl;
|
||||
//
|
||||
// Version 2 (with linear expressions):
|
||||
//
|
||||
// MathOpt optimizer(operations_research::math_opt::SOLVER_TYPE_GSCIP,
|
||||
// "my_model");
|
||||
// const Variable x = optimizer.AddBinaryVariable("x");
|
||||
// const Variable y = optimizer.AddContinuousVariable(0.0, 2.5, "y");
|
||||
// // We can directly use linear combinations of variables ...
|
||||
// optimizer.AddLinearConstraint(x + y <= 1.5, "c");
|
||||
// // ... or build them incrementally.
|
||||
// LinearExpression objective_expression;
|
||||
// objective_expression += 2*x;
|
||||
// objective_expression += y;
|
||||
// optimizer.objective().Maximize(objective_expression);
|
||||
// const Result result = optimizer.Solve(SolveParametersProto()).value();
|
||||
// for (const auto& warning : result.warnings) {
|
||||
// std::cerr << "Solver warning: " << warning << std::endl;
|
||||
// }
|
||||
// CHECK_EQ(result.termination_reason, SolveResultProto::OPTIMAL)
|
||||
// << result.termination_detail;
|
||||
// // The following code will print:
|
||||
// // objective value: 2.5
|
||||
// // value for variable x: 1
|
||||
// std::cout << "objective value: " << result.objective_value()
|
||||
// << "\nvalue for variable x: " << result.variable_values().at(x)
|
||||
// << std::endl;
|
||||
//
|
||||
// Memory model:
|
||||
//
|
||||
// Variable, LinearConstraint, and Objective are all value types that
|
||||
// represent references to the underlying MathOpt object. They don't hold any of
|
||||
// the actual model data, they can be copied, and they should be passed by
|
||||
// value. They can be regenerated arbitrarily from MathOpt. MathOpt holds all
|
||||
// the data.
|
||||
//
|
||||
// Performance:
|
||||
//
|
||||
// This class is a thin wrapper around IndexedModel (for incrementally building
|
||||
// the model and reading it back, and producing the Model proto) and Solver (for
|
||||
// consuming the Model proto to solve the optimization problem). Operations for
|
||||
// building/reading/modifying the problem typically run in O(read/write size)
|
||||
// and rely on hashing, see the indexed model documentation for details. At
|
||||
// solve time (if you are solving locally) beware that there will be (at least)
|
||||
// three copies of the model in memory, IndexedModel, the Model proto, and the
|
||||
// underlying solver's copy(/ies). Note that the Model proto is reclaimed before
|
||||
// the underlying solver begins solving.
|
||||
|
||||
// Global include for math_opt C++ API that includes anything necessary to
|
||||
// create a math problem and solve it in-process.
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_MATH_OPT_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_MATH_OPT_H_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/core/solver.h"
|
||||
#include "ortools/math_opt/cpp/callback.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/linear_constraint.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/model_solve_parameters.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/objective.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/result.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/model.pb.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/parameters.pb.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/result.pb.h" // IWYU pragma: export
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// Models and solves mathematical optimization problems.
|
||||
class MathOpt {
|
||||
public:
|
||||
using Callback = std::function<CallbackResult(CallbackData)>;
|
||||
|
||||
MathOpt(const MathOpt&) = delete;
|
||||
MathOpt& operator=(const MathOpt&) = delete;
|
||||
|
||||
// Creates an empty minimization problem.
|
||||
inline explicit MathOpt(
|
||||
SolverType solver_type, absl::string_view name = "",
|
||||
SolverInitializerProto solver_initializer = SolverInitializerProto());
|
||||
|
||||
inline const std::string& name() const;
|
||||
|
||||
// Adds a variable to the model and returns a reference to it.
|
||||
inline Variable AddVariable(double lower_bound, double upper_bound,
|
||||
bool is_integer, absl::string_view name = "");
|
||||
|
||||
// Adds a continuous unbounded variable to the model.
|
||||
inline Variable AddVariable(absl::string_view name = "");
|
||||
|
||||
// Adds an variable to the model with domain {0, 1}.
|
||||
inline Variable AddBinaryVariable(absl::string_view name = "");
|
||||
|
||||
// Adds a variable to the model with domain [lower_bound, upper_bound].
|
||||
inline Variable AddContinuousVariable(double lower_bound, double upper_bound,
|
||||
absl::string_view name = "");
|
||||
|
||||
// Adds a variable to the model that can take integer values between
|
||||
// lower_bound and upper_bound (inclusive).
|
||||
inline Variable AddIntegerVariable(double lower_bound, double upper_bound,
|
||||
absl::string_view name = "");
|
||||
|
||||
// Removes a variable from the model.
|
||||
//
|
||||
// It is an error to use any reference to this variable after this operation.
|
||||
// Runs in O(#constraints containing the variable).
|
||||
inline void DeleteVariable(Variable variable);
|
||||
|
||||
// The number of variables in the model.
|
||||
//
|
||||
// Equal to the number of variables created minus the number of variables
|
||||
// deleted.
|
||||
inline int num_variables() const;
|
||||
|
||||
// The returned id of the next call to AddVariable.
|
||||
//
|
||||
// Equal to the number of variables created.
|
||||
inline int next_variable_id() const;
|
||||
|
||||
// Returns true if this id has been created and not yet deleted.
|
||||
inline bool has_variable(int id) const;
|
||||
|
||||
// Returns all the existing (created and not deleted) variables in the model
|
||||
// in an arbitrary order.
|
||||
std::vector<Variable> Variables();
|
||||
|
||||
// Returns all the existing (created and not deleted) variables in the model,
|
||||
// sorted by id.
|
||||
std::vector<Variable> SortedVariables();
|
||||
|
||||
std::vector<LinearConstraint> ColumnNonzeros(Variable variable);
|
||||
|
||||
// Adds a linear constraint to the model with bounds [-inf, +inf].
|
||||
inline LinearConstraint AddLinearConstraint(absl::string_view name = "");
|
||||
|
||||
// Adds a linear constraint with bounds [lower_bound, upper_bound].
|
||||
inline LinearConstraint AddLinearConstraint(double lower_bound,
|
||||
double upper_bound,
|
||||
absl::string_view name = "");
|
||||
|
||||
// Adds a linear constraint from the given bounded linear expression.
|
||||
//
|
||||
// Usage:
|
||||
// MathOpt model = ...;
|
||||
// const Variable x = ...;
|
||||
// const Variable y = ...;
|
||||
// model.AddLinearConstraint(3 <= 2 * x + y + 1 <= 5, "c");
|
||||
// // The new constraint formula is:
|
||||
// // 3 - 1 <= 2 * x + y <= 5 - 1
|
||||
// // Which is:
|
||||
// // 2 <= 2 * x + y <= 4
|
||||
// // since the offset has been removed from bounds.
|
||||
//
|
||||
// model.AddLinearConstraint(2 * x + y == x + 5 * z + 3);
|
||||
// model.AddLinearConstraint(x >= 5);
|
||||
LinearConstraint AddLinearConstraint(
|
||||
const BoundedLinearExpression& bounded_expr, absl::string_view name = "");
|
||||
|
||||
// Removes a linear constraint from the model.
|
||||
//
|
||||
// It is an error to use any reference to this linear constraint after this
|
||||
// operation. Runs in O(#variables in the linear constraint).
|
||||
inline void DeleteLinearConstraint(LinearConstraint constraint);
|
||||
|
||||
// The number of linear constraints in the model.
|
||||
//
|
||||
// Equal to the number of linear constraints created minus the number of
|
||||
// linear constraints deleted.
|
||||
inline int num_linear_constraints() const;
|
||||
|
||||
// The returned id of the next call to AddLinearConstraint.
|
||||
//
|
||||
// Equal to the number of linear constraints created.
|
||||
inline int next_linear_constraint_id() const;
|
||||
|
||||
// Returns true if this id has been created and not yet deleted.
|
||||
inline bool has_linear_constraint(int id) const;
|
||||
|
||||
// Returns all the existing (created and not deleted) linear constraints in
|
||||
// the model in an arbitrary order.
|
||||
std::vector<LinearConstraint> LinearConstraints();
|
||||
|
||||
// Returns all the existing (created and not deleted) linear constraints in
|
||||
// the model sorted by id.
|
||||
std::vector<LinearConstraint> SortedLinearConstraints();
|
||||
|
||||
inline Objective objective();
|
||||
|
||||
// Solves the current optimization problem.
|
||||
//
|
||||
// A Status error will be returned if there is an unexpected failure in an
|
||||
// underlying solver or for some internal MathOpt errors. Otherwise, check
|
||||
// Result::termination_reason to see if an optimal solution was found.
|
||||
//
|
||||
// Memory model: the returned Result owns its own memory (for solutions, solve
|
||||
// stats, etc.), EXPECT for a pointer back to this->model_. As a result:
|
||||
// * Keep this alive to access Result
|
||||
// * Avoid unnecessarily copying Result,
|
||||
// * The result is generally accessible after mutating this, but some care
|
||||
// is needed if Variables or LinearConstraints are added or deleted.
|
||||
//
|
||||
// Asserts (using CHECK) that the inputs model_parameters and
|
||||
// callback_registration only contain variables and constraints from this
|
||||
// model.
|
||||
//
|
||||
// See callback.h for documentation on callback and callback_registration.
|
||||
absl::StatusOr<Result> Solve(
|
||||
const SolveParametersProto& solver_parameters,
|
||||
const ModelSolveParameters& model_parameters = {},
|
||||
const CallbackRegistration& callback_registration = {},
|
||||
Callback callback = nullptr);
|
||||
|
||||
ModelProto ExportModel() const;
|
||||
|
||||
// TODO(user): expose a way to efficiently iterate through the nonzeros of
|
||||
// the linear constraint matrix.
|
||||
private:
|
||||
// Asserts (with CHECK) that the input pointer is either nullptr or that it
|
||||
// points to the same model as model_.
|
||||
void CheckModel(IndexedModel* model);
|
||||
const SolverType solver_type_;
|
||||
const SolverInitializerProto solver_initializer_;
|
||||
const std::unique_ptr<IndexedModel> model_;
|
||||
std::unique_ptr<Solver> solver_;
|
||||
std::unique_ptr<IndexedModel::UpdateTracker> update_tracker_;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Inline function implementations
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
MathOpt::MathOpt(const SolverType solver_type, const absl::string_view name,
|
||||
SolverInitializerProto solver_initializer)
|
||||
: solver_type_(solver_type),
|
||||
solver_initializer_(std::move(solver_initializer)),
|
||||
model_(absl::make_unique<IndexedModel>(name)) {}
|
||||
|
||||
const std::string& MathOpt::name() const { return model_->name(); }
|
||||
|
||||
Variable MathOpt::AddVariable(const absl::string_view name) {
|
||||
return Variable(model_.get(), model_->AddVariable(name));
|
||||
}
|
||||
Variable MathOpt::AddVariable(const double lower_bound,
|
||||
const double upper_bound, const bool is_integer,
|
||||
const absl::string_view name) {
|
||||
return Variable(model_.get(), model_->AddVariable(lower_bound, upper_bound,
|
||||
is_integer, name));
|
||||
}
|
||||
|
||||
Variable MathOpt::AddBinaryVariable(const absl::string_view name) {
|
||||
return AddVariable(0.0, 1.0, true, name);
|
||||
}
|
||||
|
||||
Variable MathOpt::AddContinuousVariable(const double lower_bound,
|
||||
const double upper_bound,
|
||||
const absl::string_view name) {
|
||||
return AddVariable(lower_bound, upper_bound, false, name);
|
||||
}
|
||||
|
||||
Variable MathOpt::AddIntegerVariable(const double lower_bound,
|
||||
const double upper_bound,
|
||||
const absl::string_view name) {
|
||||
return AddVariable(lower_bound, upper_bound, true, name);
|
||||
}
|
||||
|
||||
void MathOpt::DeleteVariable(const Variable variable) {
|
||||
CHECK_EQ(model_.get(), variable.model());
|
||||
model_->DeleteVariable(variable.typed_id());
|
||||
}
|
||||
|
||||
int MathOpt::num_variables() const { return model_->num_variables(); }
|
||||
|
||||
int MathOpt::next_variable_id() const {
|
||||
return model_->next_variable_id().value();
|
||||
}
|
||||
|
||||
bool MathOpt::has_variable(const int id) const {
|
||||
return model_->has_variable(VariableId(id));
|
||||
}
|
||||
|
||||
LinearConstraint MathOpt::AddLinearConstraint(const absl::string_view name) {
|
||||
return LinearConstraint(model_.get(), model_->AddLinearConstraint(name));
|
||||
}
|
||||
LinearConstraint MathOpt::AddLinearConstraint(const double lower_bound,
|
||||
const double upper_bound,
|
||||
const absl::string_view name) {
|
||||
return LinearConstraint(model_.get(), model_->AddLinearConstraint(
|
||||
lower_bound, upper_bound, name));
|
||||
}
|
||||
|
||||
void MathOpt::DeleteLinearConstraint(const LinearConstraint constraint) {
|
||||
CHECK_EQ(model_.get(), constraint.model());
|
||||
model_->DeleteLinearConstraint(constraint.typed_id());
|
||||
}
|
||||
|
||||
int MathOpt::num_linear_constraints() const {
|
||||
return model_->num_linear_constraints();
|
||||
}
|
||||
|
||||
int MathOpt::next_linear_constraint_id() const {
|
||||
return model_->next_linear_constraint_id().value();
|
||||
}
|
||||
|
||||
bool MathOpt::has_linear_constraint(const int id) const {
|
||||
return model_->has_linear_constraint(LinearConstraintId(id));
|
||||
}
|
||||
|
||||
Objective MathOpt::objective() { return Objective(model_.get()); }
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
#include "ortools/math_opt/cpp/model.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/solve.h" // IWYU pragma: export
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_MATH_OPT_H_
|
||||
|
||||
221
ortools/math_opt/cpp/model.cc
Normal file
221
ortools/math_opt/cpp/model.cc
Normal file
@@ -0,0 +1,221 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/model.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
absl::StatusOr<std::unique_ptr<Model>> Model::FromModelProto(
|
||||
const ModelProto& model_proto) {
|
||||
ASSIGN_OR_RETURN(std::unique_ptr<ModelStorage> storage,
|
||||
ModelStorage::FromModelProto(model_proto));
|
||||
return std::make_unique<Model>(std::move(storage));
|
||||
}
|
||||
|
||||
Model::Model(const absl::string_view name)
|
||||
: storage_(std::make_shared<ModelStorage>(name)) {}
|
||||
|
||||
Model::Model(std::unique_ptr<ModelStorage> storage)
|
||||
: storage_(std::move(storage)) {}
|
||||
|
||||
std::unique_ptr<Model> Model::Clone() const {
|
||||
return std::make_unique<Model>(storage_->Clone());
|
||||
}
|
||||
|
||||
LinearConstraint Model::AddLinearConstraint(
|
||||
const BoundedLinearExpression& bounded_expr, absl::string_view name) {
|
||||
CheckOptionalModel(bounded_expr.expression.storage());
|
||||
|
||||
const LinearConstraintId constraint = storage()->AddLinearConstraint(
|
||||
bounded_expr.lower_bound_minus_offset(),
|
||||
bounded_expr.upper_bound_minus_offset(), name);
|
||||
for (auto [variable, coef] : bounded_expr.expression.raw_terms()) {
|
||||
storage()->set_linear_constraint_coefficient(constraint, variable, coef);
|
||||
}
|
||||
return LinearConstraint(storage(), constraint);
|
||||
}
|
||||
|
||||
std::vector<Variable> Model::Variables() const {
|
||||
std::vector<Variable> result;
|
||||
result.reserve(storage()->num_variables());
|
||||
for (const VariableId var_id : storage()->variables()) {
|
||||
result.push_back(Variable(storage(), var_id));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Variable> Model::SortedVariables() const {
|
||||
std::vector<Variable> result = Variables();
|
||||
std::sort(result.begin(), result.end(),
|
||||
[](const Variable& l, const Variable& r) {
|
||||
return l.typed_id() < r.typed_id();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<LinearConstraint> Model::ColumnNonzeros(const Variable variable) {
|
||||
CheckModel(variable.storage());
|
||||
std::vector<LinearConstraint> result;
|
||||
for (const LinearConstraintId constraint :
|
||||
storage()->linear_constraints_with_variable(variable.typed_id())) {
|
||||
result.push_back(LinearConstraint(storage(), constraint));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Variable> Model::RowNonzeros(const LinearConstraint constraint) {
|
||||
CheckModel(constraint.storage());
|
||||
std::vector<Variable> result;
|
||||
for (const VariableId variable :
|
||||
storage()->variables_in_linear_constraint(constraint.typed_id())) {
|
||||
result.push_back(Variable(storage(), variable));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
BoundedLinearExpression Model::AsBoundedLinearExpression(
|
||||
const LinearConstraint constraint) {
|
||||
CheckModel(constraint.storage());
|
||||
LinearExpression terms;
|
||||
for (const VariableId var :
|
||||
storage()->variables_in_linear_constraint(constraint.typed_id())) {
|
||||
terms +=
|
||||
Variable(storage(), var) *
|
||||
storage()->linear_constraint_coefficient(constraint.typed_id(), var);
|
||||
}
|
||||
return storage()->linear_constraint_lower_bound(constraint.typed_id()) <=
|
||||
std::move(terms) <=
|
||||
storage()->linear_constraint_upper_bound(constraint.typed_id());
|
||||
}
|
||||
|
||||
std::vector<LinearConstraint> Model::LinearConstraints() const {
|
||||
std::vector<LinearConstraint> result;
|
||||
result.reserve(storage()->num_linear_constraints());
|
||||
for (const LinearConstraintId lin_con_id : storage()->linear_constraints()) {
|
||||
result.push_back(LinearConstraint(storage(), lin_con_id));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<LinearConstraint> Model::SortedLinearConstraints() const {
|
||||
std::vector<LinearConstraint> result = LinearConstraints();
|
||||
std::sort(result.begin(), result.end(),
|
||||
[](const LinearConstraint& l, const LinearConstraint& r) {
|
||||
return l.typed_id() < r.typed_id();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void Model::SetObjective(const LinearExpression& objective,
|
||||
const bool is_maximize) {
|
||||
CheckOptionalModel(objective.storage());
|
||||
storage()->clear_objective();
|
||||
storage()->set_is_maximize(is_maximize);
|
||||
storage()->set_objective_offset(objective.offset());
|
||||
for (auto [var, coef] : objective.raw_terms()) {
|
||||
storage()->set_linear_objective_coefficient(var, coef);
|
||||
}
|
||||
}
|
||||
|
||||
void Model::SetObjective(const QuadraticExpression& objective,
|
||||
const bool is_maximize) {
|
||||
CheckOptionalModel(objective.storage());
|
||||
storage()->clear_objective();
|
||||
storage()->set_is_maximize(is_maximize);
|
||||
storage()->set_objective_offset(objective.offset());
|
||||
for (auto [var, coef] : objective.raw_linear_terms()) {
|
||||
storage()->set_linear_objective_coefficient(var, coef);
|
||||
}
|
||||
for (auto [vars, coef] : objective.raw_quadratic_terms()) {
|
||||
storage()->set_quadratic_objective_coefficient(vars.first, vars.second,
|
||||
coef);
|
||||
}
|
||||
}
|
||||
|
||||
void Model::AddToObjective(const LinearExpression& objective_terms) {
|
||||
CheckOptionalModel(objective_terms.storage());
|
||||
storage()->set_objective_offset(objective_terms.offset() +
|
||||
storage()->objective_offset());
|
||||
for (auto [var, coef] : objective_terms.raw_terms()) {
|
||||
storage()->set_linear_objective_coefficient(
|
||||
var, coef + storage()->linear_objective_coefficient(var));
|
||||
}
|
||||
}
|
||||
|
||||
void Model::AddToObjective(const QuadraticExpression& objective_terms) {
|
||||
CheckOptionalModel(objective_terms.storage());
|
||||
storage()->set_objective_offset(objective_terms.offset() +
|
||||
storage()->objective_offset());
|
||||
for (auto [var, coef] : objective_terms.raw_linear_terms()) {
|
||||
storage()->set_linear_objective_coefficient(
|
||||
var, coef + storage()->linear_objective_coefficient(var));
|
||||
}
|
||||
for (auto [vars, coef] : objective_terms.raw_quadratic_terms()) {
|
||||
storage()->set_quadratic_objective_coefficient(
|
||||
vars.first, vars.second,
|
||||
coef + storage()->quadratic_objective_coefficient(vars.first,
|
||||
vars.second));
|
||||
}
|
||||
}
|
||||
|
||||
LinearExpression Model::ObjectiveAsLinearExpression() const {
|
||||
CHECK(storage()->quadratic_objective().empty())
|
||||
<< "The objective function contains quadratic terms and cannot be "
|
||||
"represented as a LinearExpression";
|
||||
LinearExpression result = storage()->objective_offset();
|
||||
for (const auto& [v, coef] : storage()->linear_objective()) {
|
||||
result += Variable(storage(), v) * coef;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QuadraticExpression Model::ObjectiveAsQuadraticExpression() const {
|
||||
QuadraticExpression result = storage()->objective_offset();
|
||||
for (const auto& [v, coef] : storage()->linear_objective()) {
|
||||
result += Variable(storage(), v) * coef;
|
||||
}
|
||||
for (const auto& [vars, coef] : storage()->quadratic_objective()) {
|
||||
result += QuadraticTerm(Variable(storage(), vars.first),
|
||||
Variable(storage(), vars.second), coef);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModelProto Model::ExportModel() const { return storage()->ExportModel(); }
|
||||
|
||||
std::unique_ptr<UpdateTracker> Model::NewUpdateTracker() const {
|
||||
return std::make_unique<UpdateTracker>(storage_);
|
||||
}
|
||||
|
||||
absl::Status Model::ApplyUpdateProto(const ModelUpdateProto& update_proto) {
|
||||
return storage()->ApplyUpdateProto(update_proto);
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
794
ortools/math_opt/cpp/model.h
Normal file
794
ortools/math_opt/cpp/model.h
Normal file
@@ -0,0 +1,794 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_MODEL_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_MODEL_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
#include "ortools/math_opt/cpp/linear_constraint.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/update_tracker.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/model.pb.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/model_update.pb.h" // IWYU pragma: export
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// A C++ API for building optimization problems.
|
||||
//
|
||||
// Warning: Variable and LinearConstraint are value types, see "Memory Model"
|
||||
// below.
|
||||
//
|
||||
// A simple example:
|
||||
//
|
||||
// Model the problem:
|
||||
// max 2.0 * x + y
|
||||
// s.t. x + y <= 1.5
|
||||
// x in {0.0, 1.0}
|
||||
// y in [0.0, 2.5]
|
||||
//
|
||||
// using ::operations_research::math_opt::LinearConstraint;
|
||||
// using ::operations_research::math_opt::Model;
|
||||
// using ::operations_research::math_opt::SolveResult;
|
||||
// using ::operations_research::math_opt::SolveParameters;
|
||||
// using ::operations_research::math_opt::SolveResultProto;
|
||||
// using ::operations_research::math_opt::Variable;
|
||||
// using ::operations_research::math_opt::SOLVER_TYPE_GSCIP;
|
||||
//
|
||||
// Version 1:
|
||||
//
|
||||
// Model model("my_model");
|
||||
// const Variable x = model.AddBinaryVariable("x");
|
||||
// const Variable y = model.AddContinuousVariable(0.0, 2.5, "y");
|
||||
// const LinearConstraint c = model.AddLinearConstraint(
|
||||
// -std::numeric_limits<double>::infinity(), 1.5, "c");
|
||||
// model.set_coefficient(c, x, 1.0);
|
||||
// model.set_coefficient(c, y, 1.0);
|
||||
// model.set_objective_coefficient(x, 2.0);
|
||||
// model.set_objective_coefficient(y, 1.0);
|
||||
// model.set_maximize();
|
||||
// const SolveResult result = Solve(
|
||||
// model, SOLVER_TYPE_GSCIP, SolveParametersProto()).value();
|
||||
// for (const auto& warning : result.warnings) {
|
||||
// std::cerr << "Solver warning: " << warning << std::endl;
|
||||
// }
|
||||
// CHECK_EQ(result.termination.reason, TerminationReason::kOptimal)
|
||||
// << result.termination_detail;
|
||||
// // The following code will print:
|
||||
// // objective value: 2.5
|
||||
// // value for variable x: 1
|
||||
// std::cout << "objective value: " << result.objective_value()
|
||||
// << "\nvalue for variable x: " << result.variable_values().at(x)
|
||||
// << std::endl;
|
||||
//
|
||||
// Version 2 (with linear expressions):
|
||||
//
|
||||
// Model model("my_model");
|
||||
// const Variable x = model.AddBinaryVariable("x");
|
||||
// const Variable y = model.AddContinuousVariable(0.0, 2.5, "y");
|
||||
// // We can directly use linear combinations of variables ...
|
||||
// model.AddLinearConstraint(x + y <= 1.5, "c");
|
||||
// // ... or build them incrementally.
|
||||
// LinearExpression objective_expression;
|
||||
// objective_expression += 2*x;
|
||||
// objective_expression += y;
|
||||
// model.Maximize(objective_expression);
|
||||
// const SolveResult result = Solve(
|
||||
// model, SOLVER_TYPE_GSCIP, SolveParametersProto()).value();
|
||||
// for (const auto& warning : result.warnings) {
|
||||
// std::cerr << "Solver warning: " << warning << std::endl;
|
||||
// }
|
||||
// CHECK_EQ(result.termination.reason, TerminationReason::kOptimal)
|
||||
// << result.termination_detail;
|
||||
// // The following code will print:
|
||||
// // objective value: 2.5
|
||||
// // value for variable x: 1
|
||||
// std::cout << "objective value: " << result.objective_value()
|
||||
// << "\nvalue for variable x: " << result.variable_values().at(x)
|
||||
// << std::endl;
|
||||
//
|
||||
// Memory model:
|
||||
//
|
||||
// Variable and LinearConstraint are value types that represent references to
|
||||
// the underlying Model object. They don't hold any of the actual model data,
|
||||
// they can be copied, and they should be passed by value. They can be
|
||||
// regenerated arbitrarily from Model. Model holds all the data.
|
||||
//
|
||||
// Performance:
|
||||
//
|
||||
// This class is a thin wrapper around ModelStorage (for incrementally building
|
||||
// the model and reading it back, and producing the Model proto) and Solver (for
|
||||
// consuming the Model proto to solve the optimization problem). Operations for
|
||||
// building/reading/modifying the problem typically run in O(read/write size)
|
||||
// and rely on hashing, see the indexed model documentation for details. At
|
||||
// solve time (if you are solving locally) beware that there will be (at least)
|
||||
// three copies of the model in memory, ModelStorage, the Model proto, and the
|
||||
// underlying solver's copy(/ies). Note that the Model proto is reclaimed before
|
||||
// the underlying solver begins solving.
|
||||
class Model {
|
||||
public:
|
||||
// Returns a model from the input proto. Returns a failure status if the input
|
||||
// proto is invalid.
|
||||
//
|
||||
// On top of loading a model from a MathOpt ModelProto, this function can also
|
||||
// be used to load a model from other formats using the functions in
|
||||
// math_opt/io/ like ReadMpsFile().
|
||||
//
|
||||
// See ExportModel() to get the proto of a Model. See ApplyUpdateProto() to
|
||||
// apply an update to the model.
|
||||
//
|
||||
// Usage example reading an MPS file:
|
||||
// ASSIGN_OR_RETURN(const ModelProto model_proto, ReadMpsFile(path));
|
||||
// ASSIGN_OR_RETURN(const std::unique_ptr<Model> model,
|
||||
// Model::FromModelProto(model_proto));
|
||||
static absl::StatusOr<std::unique_ptr<Model>> FromModelProto(
|
||||
const ModelProto& model_proto);
|
||||
|
||||
// Creates an empty minimization problem.
|
||||
explicit Model(absl::string_view name = "");
|
||||
|
||||
// Creates a model from the existing model storage.
|
||||
//
|
||||
// This constructor is used when loading a model, for example from a
|
||||
// ModelProto or an MPS file. Note that in those cases the FromModelProto()
|
||||
// should be used.
|
||||
explicit Model(std::unique_ptr<ModelStorage> storage);
|
||||
|
||||
Model(const Model&) = delete;
|
||||
Model& operator=(const Model&) = delete;
|
||||
|
||||
// Returns a clone of this model.
|
||||
//
|
||||
// The variables and constraints have the same integer ids. The clone will
|
||||
// also not reused any id of variable/constraint that was deleted in the
|
||||
// original.
|
||||
//
|
||||
// That said, the Variable and LinearConstraint reference objects are model
|
||||
// specific. Hence the ones linked to the original model must NOT be used with
|
||||
// the clone. The Variable and LinearConstraint reference objects for the
|
||||
// clone can be obtained via Variables() and LinearConstraints(). One can also
|
||||
// use SortedVariables() and SortedLinearConstraints() that will return (until
|
||||
// one of the two models is modified) the variables and constraints in the
|
||||
// same order for the two models and provide a one-to-one correspondence.
|
||||
//
|
||||
// Note that the returned model does not have any update tracker.
|
||||
std::unique_ptr<Model> Clone() const;
|
||||
|
||||
inline const std::string& name() const;
|
||||
|
||||
// Adds a variable to the model and returns a reference to it.
|
||||
inline Variable AddVariable(double lower_bound, double upper_bound,
|
||||
bool is_integer, absl::string_view name = "");
|
||||
|
||||
// Adds a continuous unbounded variable to the model.
|
||||
inline Variable AddVariable(absl::string_view name = "");
|
||||
|
||||
// Adds an variable to the model with domain {0, 1}.
|
||||
inline Variable AddBinaryVariable(absl::string_view name = "");
|
||||
|
||||
// Adds a variable to the model with domain [lower_bound, upper_bound].
|
||||
inline Variable AddContinuousVariable(double lower_bound, double upper_bound,
|
||||
absl::string_view name = "");
|
||||
|
||||
// Adds a variable to the model that can take integer values between
|
||||
// lower_bound and upper_bound (inclusive).
|
||||
inline Variable AddIntegerVariable(double lower_bound, double upper_bound,
|
||||
absl::string_view name = "");
|
||||
|
||||
// Removes a variable from the model.
|
||||
//
|
||||
// It is an error to use any reference to this variable after this operation.
|
||||
// Runs in O(#constraints containing the variable).
|
||||
inline void DeleteVariable(Variable variable);
|
||||
|
||||
// The number of variables in the model.
|
||||
//
|
||||
// Equal to the number of variables created minus the number of variables
|
||||
// deleted.
|
||||
inline int num_variables() const;
|
||||
|
||||
// The returned id of the next call to AddVariable.
|
||||
//
|
||||
// Equal to the number of variables created.
|
||||
inline int next_variable_id() const;
|
||||
|
||||
// Returns true if this id has been created and not yet deleted.
|
||||
inline bool has_variable(int id) const;
|
||||
|
||||
// Returns the variable name.
|
||||
inline const std::string& name(Variable variable) const;
|
||||
|
||||
// Sets a variable lower bound.
|
||||
inline void set_lower_bound(Variable variable, double lower_bound);
|
||||
|
||||
// Returns a variable lower bound.
|
||||
inline double lower_bound(Variable variable) const;
|
||||
|
||||
// Sets a variable upper bound.
|
||||
inline void set_upper_bound(Variable variable, double upper_bound);
|
||||
|
||||
// Returns a variable upper bound.
|
||||
inline double upper_bound(Variable variable) const;
|
||||
|
||||
// Sets the integrality of a variable.
|
||||
inline void set_is_integer(Variable variable, bool is_integer);
|
||||
|
||||
// Makes the input variable integer.
|
||||
inline void set_integer(Variable variable);
|
||||
|
||||
// Makes the input variable continuous.
|
||||
inline void set_continuous(Variable variable);
|
||||
|
||||
// Returns the integrality of a variable.
|
||||
inline bool is_integer(Variable variable) const;
|
||||
|
||||
// Returns all the existing (created and not deleted) variables in the model
|
||||
// in an arbitrary order.
|
||||
std::vector<Variable> Variables() const;
|
||||
|
||||
// Returns all the existing (created and not deleted) variables in the model,
|
||||
// sorted by id.
|
||||
std::vector<Variable> SortedVariables() const;
|
||||
|
||||
std::vector<LinearConstraint> ColumnNonzeros(Variable variable);
|
||||
|
||||
// Adds a linear constraint to the model with bounds [-inf, +inf].
|
||||
inline LinearConstraint AddLinearConstraint(absl::string_view name = "");
|
||||
|
||||
// Adds a linear constraint with bounds [lower_bound, upper_bound].
|
||||
inline LinearConstraint AddLinearConstraint(double lower_bound,
|
||||
double upper_bound,
|
||||
absl::string_view name = "");
|
||||
|
||||
// Adds a linear constraint from the given bounded linear expression.
|
||||
//
|
||||
// Usage:
|
||||
// Model model = ...;
|
||||
// const Variable x = ...;
|
||||
// const Variable y = ...;
|
||||
// model.AddLinearConstraint(3 <= 2 * x + y + 1 <= 5, "c");
|
||||
// // The new constraint formula is:
|
||||
// // 3 - 1 <= 2 * x + y <= 5 - 1
|
||||
// // Which is:
|
||||
// // 2 <= 2 * x + y <= 4
|
||||
// // since the offset has been removed from bounds.
|
||||
//
|
||||
// model.AddLinearConstraint(2 * x + y == x + 5 * z + 3);
|
||||
// model.AddLinearConstraint(x >= 5);
|
||||
LinearConstraint AddLinearConstraint(
|
||||
const BoundedLinearExpression& bounded_expr, absl::string_view name = "");
|
||||
|
||||
// Removes a linear constraint from the model.
|
||||
//
|
||||
// It is an error to use any reference to this linear constraint after this
|
||||
// operation. Runs in O(#variables in the linear constraint).
|
||||
inline void DeleteLinearConstraint(LinearConstraint constraint);
|
||||
|
||||
// The number of linear constraints in the model.
|
||||
//
|
||||
// Equal to the number of linear constraints created minus the number of
|
||||
// linear constraints deleted.
|
||||
inline int num_linear_constraints() const;
|
||||
|
||||
// The returned id of the next call to AddLinearConstraint.
|
||||
//
|
||||
// Equal to the number of linear constraints created.
|
||||
inline int next_linear_constraint_id() const;
|
||||
|
||||
// Returns true if this id has been created and not yet deleted.
|
||||
inline bool has_linear_constraint(int id) const;
|
||||
|
||||
// Returns the linear constraint name.
|
||||
inline const std::string& name(LinearConstraint constraint) const;
|
||||
|
||||
// Sets a linear constraint lower bound.
|
||||
inline void set_lower_bound(LinearConstraint constraint, double lower_bound);
|
||||
|
||||
// Returns a linear constraint lower bound.
|
||||
inline double lower_bound(LinearConstraint constraint) const;
|
||||
|
||||
// Sets a linear constraint upper bound.
|
||||
inline void set_upper_bound(LinearConstraint constraint, double upper_bound);
|
||||
|
||||
// Returns a linear constraint upper bound.
|
||||
inline double upper_bound(LinearConstraint constraint) const;
|
||||
|
||||
// Setting a value to 0.0 will delete the {constraint, variable} pair from the
|
||||
// underlying sparse matrix representation (and has no effect if the pair is
|
||||
// not present).
|
||||
inline void set_coefficient(LinearConstraint constraint, Variable variable,
|
||||
double value);
|
||||
|
||||
// Returns 0.0 if the variable is not used in the constraint.
|
||||
inline double coefficient(LinearConstraint constraint,
|
||||
Variable variable) const;
|
||||
|
||||
inline bool is_coefficient_nonzero(LinearConstraint constraint,
|
||||
Variable variable) const;
|
||||
|
||||
// This method modifies some internal structures of the model and thus is not
|
||||
// const.
|
||||
std::vector<Variable> RowNonzeros(LinearConstraint constraint);
|
||||
|
||||
// This method modifies some internal structures of the model and thus is not
|
||||
// const.
|
||||
BoundedLinearExpression AsBoundedLinearExpression(
|
||||
LinearConstraint constraint);
|
||||
|
||||
// Returns all the existing (created and not deleted) linear constraints in
|
||||
// the model in an arbitrary order.
|
||||
std::vector<LinearConstraint> LinearConstraints() const;
|
||||
|
||||
// Returns all the existing (created and not deleted) linear constraints in
|
||||
// the model sorted by id.
|
||||
std::vector<LinearConstraint> SortedLinearConstraints() const;
|
||||
|
||||
// Sets the objective to maximize the provided expression.
|
||||
inline void Maximize(double objective);
|
||||
// Sets the objective to maximize the provided expression.
|
||||
inline void Maximize(Variable objective);
|
||||
// Sets the objective to maximize the provided expression.
|
||||
inline void Maximize(LinearTerm objective);
|
||||
// Sets the objective to maximize the provided expression.
|
||||
inline void Maximize(const LinearExpression& objective);
|
||||
// Sets the objective to maximize the provided expression.
|
||||
inline void Maximize(const QuadraticExpression& objective);
|
||||
|
||||
// Sets the objective to minimize the provided expression.
|
||||
inline void Minimize(double objective);
|
||||
// Sets the objective to minimize the provided expression.
|
||||
inline void Minimize(Variable objective);
|
||||
// Sets the objective to minimize the provided expression.
|
||||
inline void Minimize(LinearTerm objective);
|
||||
// Sets the objective to minimize the provided expression.
|
||||
inline void Minimize(const LinearExpression& objective);
|
||||
// Sets the objective to minimize the provided expression.
|
||||
inline void Minimize(const QuadraticExpression& objective);
|
||||
|
||||
// Sets the objective to optimize the provided expression.
|
||||
inline void SetObjective(double objective, bool is_maximize);
|
||||
// Sets the objective to optimize the provided expression.
|
||||
inline void SetObjective(Variable objective, bool is_maximize);
|
||||
// Sets the objective to optimize the provided expression.
|
||||
inline void SetObjective(LinearTerm objective, bool is_maximize);
|
||||
// Sets the objective to optimize the provided expression.
|
||||
void SetObjective(const LinearExpression& objective, bool is_maximize);
|
||||
// Sets the objective to optimize the provided expression.
|
||||
void SetObjective(const QuadraticExpression& objective, bool is_maximize);
|
||||
|
||||
// Adds the provided expression terms to the objective.
|
||||
inline void AddToObjective(double objective);
|
||||
// Adds the provided expression terms to the objective.
|
||||
inline void AddToObjective(Variable objective);
|
||||
// Adds the provided expression terms to the objective.
|
||||
inline void AddToObjective(LinearTerm objective);
|
||||
// Adds the provided expression terms to the objective.
|
||||
void AddToObjective(const LinearExpression& objective);
|
||||
// Adds the provided expression terms to the objective.
|
||||
void AddToObjective(const QuadraticExpression& objective);
|
||||
|
||||
// NOTE: This will CHECK fail if the objective has quadratic terms.
|
||||
LinearExpression ObjectiveAsLinearExpression() const;
|
||||
QuadraticExpression ObjectiveAsQuadraticExpression() const;
|
||||
|
||||
// Returns 0.0 if this variable has no linear objective coefficient.
|
||||
inline double objective_coefficient(Variable variable) const;
|
||||
|
||||
// Returns 0.0 if this variable pair has no quadratic objective coefficient.
|
||||
// The order of the variables does not matter.
|
||||
inline double objective_coefficient(Variable first_variable,
|
||||
Variable second_variable) const;
|
||||
|
||||
// Setting a value to 0.0 will delete the variable from the underlying sparse
|
||||
// representation (and has no effect if the variable is not present).
|
||||
inline void set_objective_coefficient(Variable variable, double value);
|
||||
|
||||
// Set quadratic objective terms for the product of two variables. Setting a
|
||||
// value to 0.0 will delete the variable pair from the underlying sparse
|
||||
// representation (and has no effect if the pair is not present). The order of
|
||||
// the variables does not matter.
|
||||
inline void set_objective_coefficient(Variable first_variable,
|
||||
Variable second_variable, double value);
|
||||
|
||||
// Equivalent to calling set_linear_coefficient(v, 0.0) for every variable
|
||||
// with nonzero objective coefficient.
|
||||
//
|
||||
// Runs in O(#linear and quadratic objective terms with nonzero coefficient).
|
||||
inline void clear_objective();
|
||||
|
||||
inline bool is_objective_coefficient_nonzero(Variable variable) const;
|
||||
inline bool is_objective_coefficient_nonzero(Variable first_variable,
|
||||
Variable second_variable) const;
|
||||
|
||||
inline double objective_offset() const;
|
||||
|
||||
inline void set_objective_offset(double value);
|
||||
|
||||
inline bool is_maximize() const;
|
||||
|
||||
inline void set_maximize();
|
||||
inline void set_minimize();
|
||||
|
||||
// Prefer set_maximize() and set_minimize() above for more readable code.
|
||||
inline void set_is_maximize(bool is_maximize);
|
||||
|
||||
// Returns a proto representation of the optimization model.
|
||||
//
|
||||
// See FromModelProto() to build a Model from a proto.
|
||||
ModelProto ExportModel() const;
|
||||
|
||||
// Returns a tracker that can be used to generate a ModelUpdateProto with the
|
||||
// updates that happened since the last checkpoint. The tracker initial
|
||||
// checkpoint corresponds to the current state of the model.
|
||||
//
|
||||
// The returned UpdateTracker keeps a reference to this model. See the
|
||||
// implications in the documentation of the UpdateTracker class.
|
||||
//
|
||||
// Thread-safety: this method must not be used while modifying the model
|
||||
// (variables, constraints, ...). The user is expected to use proper
|
||||
// synchronization primitive to serialize changes to the model and the use of
|
||||
// this method.
|
||||
std::unique_ptr<UpdateTracker> NewUpdateTracker() const;
|
||||
|
||||
// Apply the provided update to this model. Returns a failure if the update is
|
||||
// not valid.
|
||||
//
|
||||
// As with FromModelProto(), duplicated names are ignored.
|
||||
//
|
||||
// Note that it takes O(num_variables + num_constraints) extra memory and
|
||||
// execution to apply the update (due to the need to build a ModelSummary). So
|
||||
// even a small update will have some cost.
|
||||
absl::Status ApplyUpdateProto(const ModelUpdateProto& update_proto);
|
||||
|
||||
// TODO(user): expose a way to efficiently iterate through the nonzeros of
|
||||
// the linear constraint matrix.
|
||||
|
||||
// Returns a pointer to the underlying model storage.
|
||||
//
|
||||
// This API is for internal use only and regular users should have no need for
|
||||
// it.
|
||||
const ModelStorage* storage() const { return storage_.get(); }
|
||||
|
||||
// Returns a pointer to the underlying model storage.
|
||||
//
|
||||
// This API is for internal use only and regular users should have no need for
|
||||
// it.
|
||||
ModelStorage* storage() { return storage_.get(); }
|
||||
|
||||
private:
|
||||
// Asserts (with CHECK) that the input pointer is either nullptr or that it
|
||||
// points to the same model as storage_.
|
||||
//
|
||||
// Use CheckModel() when nullptr is not a valid value.
|
||||
inline void CheckOptionalModel(const ModelStorage* other_storage) const;
|
||||
|
||||
// Asserts (with CHECK) that the input pointer is the same as storage_.
|
||||
//
|
||||
// Use CheckOptionalModel() if nullptr is a valid value too.
|
||||
inline void CheckModel(const ModelStorage* other_storage) const;
|
||||
|
||||
// Don't use storage_ directly; prefer to use storage() so that const member
|
||||
// functions don't have modifying access to the underlying storage.
|
||||
//
|
||||
// We use a shared_ptr here so that the UpdateTracker class can have a
|
||||
// weak_ptr on the ModelStorage. This let it have a destructor that don't
|
||||
// crash when called after the destruction of the associated Model.
|
||||
const std::shared_ptr<ModelStorage> storage_;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Inline function implementations
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const std::string& Model::name() const { return storage()->name(); }
|
||||
|
||||
Variable Model::AddVariable(const absl::string_view name) {
|
||||
return Variable(storage(), storage()->AddVariable(name));
|
||||
}
|
||||
Variable Model::AddVariable(const double lower_bound, const double upper_bound,
|
||||
const bool is_integer,
|
||||
const absl::string_view name) {
|
||||
return Variable(storage(), storage()->AddVariable(lower_bound, upper_bound,
|
||||
is_integer, name));
|
||||
}
|
||||
|
||||
Variable Model::AddBinaryVariable(const absl::string_view name) {
|
||||
return AddVariable(0.0, 1.0, true, name);
|
||||
}
|
||||
|
||||
Variable Model::AddContinuousVariable(const double lower_bound,
|
||||
const double upper_bound,
|
||||
const absl::string_view name) {
|
||||
return AddVariable(lower_bound, upper_bound, false, name);
|
||||
}
|
||||
|
||||
Variable Model::AddIntegerVariable(const double lower_bound,
|
||||
const double upper_bound,
|
||||
const absl::string_view name) {
|
||||
return AddVariable(lower_bound, upper_bound, true, name);
|
||||
}
|
||||
|
||||
void Model::DeleteVariable(const Variable variable) {
|
||||
CheckModel(variable.storage());
|
||||
storage()->DeleteVariable(variable.typed_id());
|
||||
}
|
||||
|
||||
int Model::num_variables() const { return storage()->num_variables(); }
|
||||
|
||||
int Model::next_variable_id() const {
|
||||
return storage()->next_variable_id().value();
|
||||
}
|
||||
|
||||
bool Model::has_variable(const int id) const {
|
||||
return storage()->has_variable(VariableId(id));
|
||||
}
|
||||
|
||||
const std::string& Model::name(const Variable variable) const {
|
||||
CheckModel(variable.storage());
|
||||
return storage()->variable_name(variable.typed_id());
|
||||
}
|
||||
|
||||
void Model::set_lower_bound(const Variable variable, double lower_bound) {
|
||||
CheckModel(variable.storage());
|
||||
storage()->set_variable_lower_bound(variable.typed_id(), lower_bound);
|
||||
}
|
||||
|
||||
double Model::lower_bound(const Variable variable) const {
|
||||
CheckModel(variable.storage());
|
||||
return storage()->variable_lower_bound(variable.typed_id());
|
||||
}
|
||||
|
||||
void Model::set_upper_bound(const Variable variable, double upper_bound) {
|
||||
CheckModel(variable.storage());
|
||||
storage()->set_variable_upper_bound(variable.typed_id(), upper_bound);
|
||||
}
|
||||
|
||||
double Model::upper_bound(const Variable variable) const {
|
||||
CheckModel(variable.storage());
|
||||
return storage()->variable_upper_bound(variable.typed_id());
|
||||
}
|
||||
|
||||
void Model::set_is_integer(const Variable variable, bool is_integer) {
|
||||
CheckModel(variable.storage());
|
||||
storage()->set_variable_is_integer(variable.typed_id(), is_integer);
|
||||
}
|
||||
|
||||
void Model::set_integer(const Variable variable) {
|
||||
set_is_integer(variable, true);
|
||||
}
|
||||
|
||||
void Model::set_continuous(const Variable variable) {
|
||||
set_is_integer(variable, false);
|
||||
}
|
||||
|
||||
bool Model::is_integer(const Variable variable) const {
|
||||
CheckModel(variable.storage());
|
||||
return storage()->is_variable_integer(variable.typed_id());
|
||||
}
|
||||
|
||||
LinearConstraint Model::AddLinearConstraint(const absl::string_view name) {
|
||||
return LinearConstraint(storage(), storage()->AddLinearConstraint(name));
|
||||
}
|
||||
LinearConstraint Model::AddLinearConstraint(const double lower_bound,
|
||||
const double upper_bound,
|
||||
const absl::string_view name) {
|
||||
return LinearConstraint(storage(), storage()->AddLinearConstraint(
|
||||
lower_bound, upper_bound, name));
|
||||
}
|
||||
|
||||
void Model::DeleteLinearConstraint(const LinearConstraint constraint) {
|
||||
CheckModel(constraint.storage());
|
||||
storage()->DeleteLinearConstraint(constraint.typed_id());
|
||||
}
|
||||
|
||||
int Model::num_linear_constraints() const {
|
||||
return storage()->num_linear_constraints();
|
||||
}
|
||||
|
||||
int Model::next_linear_constraint_id() const {
|
||||
return storage()->next_linear_constraint_id().value();
|
||||
}
|
||||
|
||||
bool Model::has_linear_constraint(const int id) const {
|
||||
return storage()->has_linear_constraint(LinearConstraintId(id));
|
||||
}
|
||||
|
||||
const std::string& Model::name(const LinearConstraint constraint) const {
|
||||
CheckModel(constraint.storage());
|
||||
return storage()->linear_constraint_name(constraint.typed_id());
|
||||
}
|
||||
|
||||
void Model::set_lower_bound(const LinearConstraint constraint,
|
||||
double lower_bound) {
|
||||
CheckModel(constraint.storage());
|
||||
storage()->set_linear_constraint_lower_bound(constraint.typed_id(),
|
||||
lower_bound);
|
||||
}
|
||||
|
||||
double Model::lower_bound(const LinearConstraint constraint) const {
|
||||
CheckModel(constraint.storage());
|
||||
return storage()->linear_constraint_lower_bound(constraint.typed_id());
|
||||
}
|
||||
|
||||
void Model::set_upper_bound(const LinearConstraint constraint,
|
||||
const double upper_bound) {
|
||||
CheckModel(constraint.storage());
|
||||
storage()->set_linear_constraint_upper_bound(constraint.typed_id(),
|
||||
upper_bound);
|
||||
}
|
||||
|
||||
double Model::upper_bound(const LinearConstraint constraint) const {
|
||||
CheckModel(constraint.storage());
|
||||
return storage()->linear_constraint_upper_bound(constraint.typed_id());
|
||||
}
|
||||
|
||||
void Model::set_coefficient(const LinearConstraint constraint,
|
||||
const Variable variable, const double value) {
|
||||
CheckModel(constraint.storage());
|
||||
CheckModel(variable.storage());
|
||||
storage()->set_linear_constraint_coefficient(constraint.typed_id(),
|
||||
variable.typed_id(), value);
|
||||
}
|
||||
|
||||
double Model::coefficient(const LinearConstraint constraint,
|
||||
const Variable variable) const {
|
||||
CheckModel(constraint.storage());
|
||||
CheckModel(variable.storage());
|
||||
return storage()->linear_constraint_coefficient(constraint.typed_id(),
|
||||
variable.typed_id());
|
||||
}
|
||||
|
||||
bool Model::is_coefficient_nonzero(const LinearConstraint constraint,
|
||||
const Variable variable) const {
|
||||
CheckModel(constraint.storage());
|
||||
CheckModel(variable.storage());
|
||||
return storage()->is_linear_constraint_coefficient_nonzero(
|
||||
constraint.typed_id(), variable.typed_id());
|
||||
}
|
||||
|
||||
void Model::Maximize(const double objective) {
|
||||
SetObjective(LinearExpression(objective), /*is_maximize=*/true);
|
||||
}
|
||||
void Model::Maximize(const Variable objective) {
|
||||
SetObjective(LinearExpression(objective), /*is_maximize=*/true);
|
||||
}
|
||||
void Model::Maximize(const LinearTerm objective) {
|
||||
SetObjective(LinearExpression(objective), /*is_maximize=*/true);
|
||||
}
|
||||
void Model::Maximize(const LinearExpression& objective) {
|
||||
SetObjective(objective, /*is_maximize=*/true);
|
||||
}
|
||||
void Model::Maximize(const QuadraticExpression& objective) {
|
||||
SetObjective(objective, /*is_maximize=*/true);
|
||||
}
|
||||
|
||||
void Model::Minimize(const double objective) {
|
||||
SetObjective(LinearExpression(objective), /*is_maximize=*/false);
|
||||
}
|
||||
void Model::Minimize(const Variable objective) {
|
||||
SetObjective(LinearExpression(objective), /*is_maximize=*/false);
|
||||
}
|
||||
void Model::Minimize(const LinearTerm objective) {
|
||||
SetObjective(LinearExpression(objective), /*is_maximize=*/false);
|
||||
}
|
||||
void Model::Minimize(const LinearExpression& objective) {
|
||||
SetObjective(objective, /*is_maximize=*/false);
|
||||
}
|
||||
void Model::Minimize(const QuadraticExpression& objective) {
|
||||
SetObjective(objective, /*is_maximize=*/false);
|
||||
}
|
||||
|
||||
void Model::SetObjective(const double objective, const bool is_maximize) {
|
||||
SetObjective(LinearExpression(objective), /*is_maximize=*/is_maximize);
|
||||
}
|
||||
void Model::SetObjective(const Variable objective, const bool is_maximize) {
|
||||
SetObjective(LinearExpression(objective), /*is_maximize=*/is_maximize);
|
||||
}
|
||||
void Model::SetObjective(const LinearTerm objective, const bool is_maximize) {
|
||||
SetObjective(LinearExpression(objective), /*is_maximize=*/is_maximize);
|
||||
}
|
||||
|
||||
void Model::AddToObjective(const double objective) {
|
||||
AddToObjective(LinearExpression(objective));
|
||||
}
|
||||
void Model::AddToObjective(const Variable objective) {
|
||||
AddToObjective(LinearExpression(objective));
|
||||
}
|
||||
void Model::AddToObjective(const LinearTerm objective) {
|
||||
AddToObjective(LinearExpression(objective));
|
||||
}
|
||||
|
||||
double Model::objective_coefficient(const Variable variable) const {
|
||||
CheckModel(variable.storage());
|
||||
return storage()->linear_objective_coefficient(variable.typed_id());
|
||||
}
|
||||
|
||||
double Model::objective_coefficient(const Variable first_variable,
|
||||
const Variable second_variable) const {
|
||||
CheckModel(first_variable.storage());
|
||||
CheckModel(second_variable.storage());
|
||||
return storage()->quadratic_objective_coefficient(first_variable.typed_id(),
|
||||
second_variable.typed_id());
|
||||
}
|
||||
|
||||
void Model::set_objective_coefficient(const Variable variable,
|
||||
const double value) {
|
||||
CheckModel(variable.storage());
|
||||
storage()->set_linear_objective_coefficient(variable.typed_id(), value);
|
||||
}
|
||||
|
||||
void Model::set_objective_coefficient(const Variable first_variable,
|
||||
const Variable second_variable,
|
||||
const double value) {
|
||||
CheckModel(first_variable.storage());
|
||||
CheckModel(second_variable.storage());
|
||||
storage()->set_quadratic_objective_coefficient(
|
||||
first_variable.typed_id(), second_variable.typed_id(), value);
|
||||
}
|
||||
|
||||
void Model::clear_objective() { storage()->clear_objective(); }
|
||||
|
||||
bool Model::is_objective_coefficient_nonzero(const Variable variable) const {
|
||||
CheckModel(variable.storage());
|
||||
return storage()->is_linear_objective_coefficient_nonzero(
|
||||
variable.typed_id());
|
||||
}
|
||||
|
||||
bool Model::is_objective_coefficient_nonzero(
|
||||
const Variable first_variable, const Variable second_variable) const {
|
||||
CheckModel(first_variable.storage());
|
||||
CheckModel(second_variable.storage());
|
||||
return storage()->is_quadratic_objective_coefficient_nonzero(
|
||||
first_variable.typed_id(), second_variable.typed_id());
|
||||
}
|
||||
|
||||
double Model::objective_offset() const { return storage()->objective_offset(); }
|
||||
|
||||
void Model::set_objective_offset(const double value) {
|
||||
storage()->set_objective_offset(value);
|
||||
}
|
||||
|
||||
bool Model::is_maximize() const { return storage()->is_maximize(); }
|
||||
|
||||
void Model::set_maximize() { storage()->set_maximize(); }
|
||||
|
||||
void Model::set_minimize() { storage()->set_minimize(); }
|
||||
|
||||
void Model::set_is_maximize(const bool is_maximize) {
|
||||
storage()->set_is_maximize(is_maximize);
|
||||
}
|
||||
|
||||
void Model::CheckOptionalModel(const ModelStorage* const other_storage) const {
|
||||
if (other_storage != nullptr) {
|
||||
CHECK_EQ(other_storage, storage())
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
}
|
||||
|
||||
void Model::CheckModel(const ModelStorage* const other_storage) const {
|
||||
CHECK_EQ(other_storage, storage()) << internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_MODEL_H_
|
||||
@@ -20,10 +20,10 @@
|
||||
#include <utility>
|
||||
|
||||
#include "google/protobuf/message.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
#include "ortools/math_opt/cpp/linear_constraint.h"
|
||||
#include "ortools/math_opt/cpp/result.h"
|
||||
#include "ortools/math_opt/cpp/solution.h"
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h"
|
||||
#include "ortools/math_opt/model_parameters.pb.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
@@ -36,9 +36,8 @@ using ::google::protobuf::RepeatedField;
|
||||
|
||||
ModelSolveParameters ModelSolveParameters::OnlyPrimalVariables() {
|
||||
ModelSolveParameters parameters;
|
||||
parameters.dual_linear_constraints_filter =
|
||||
MakeSkipAllFilter<LinearConstraint>();
|
||||
parameters.dual_variables_filter = MakeSkipAllFilter<Variable>();
|
||||
parameters.dual_values_filter = MakeSkipAllFilter<LinearConstraint>();
|
||||
parameters.reduced_costs_filter = MakeSkipAllFilter<Variable>();
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@@ -47,25 +46,24 @@ ModelSolveParameters ModelSolveParameters::OnlySomePrimalVariables(
|
||||
return OnlySomePrimalVariables<std::initializer_list<Variable>>(variables);
|
||||
}
|
||||
|
||||
IndexedModel* ModelSolveParameters::model() const {
|
||||
return internal::ConsistentModel({primal_variables_filter.model(),
|
||||
dual_linear_constraints_filter.model(),
|
||||
dual_variables_filter.model()});
|
||||
const ModelStorage* ModelSolveParameters::storage() const {
|
||||
return internal::ConsistentModelStorage({variable_values_filter.storage(),
|
||||
dual_values_filter.storage(),
|
||||
reduced_costs_filter.storage()});
|
||||
}
|
||||
|
||||
ModelSolveParametersProto ModelSolveParameters::Proto() const {
|
||||
// We call model() here for its side effect of asserting that all filters use
|
||||
// variables and linear constraints use the same model.
|
||||
model();
|
||||
// We call storage() here for its side effect of asserting that all filters
|
||||
// use variables and linear constraints use the same model.
|
||||
storage();
|
||||
|
||||
ModelSolveParametersProto ret;
|
||||
*ret.mutable_primal_variables_filter() = primal_variables_filter.Proto();
|
||||
*ret.mutable_dual_linear_constraints_filter() =
|
||||
dual_linear_constraints_filter.Proto();
|
||||
*ret.mutable_dual_variables_filter() = dual_variables_filter.Proto();
|
||||
*ret.mutable_variable_values_filter() = variable_values_filter.Proto();
|
||||
*ret.mutable_dual_values_filter() = dual_values_filter.Proto();
|
||||
*ret.mutable_reduced_costs_filter() = reduced_costs_filter.Proto();
|
||||
|
||||
// TODO(user): consolidate code. Probably best to add an export_to_proto
|
||||
// to IdMap
|
||||
// TODO(b/183616124): consolidate code. Probably best to add an
|
||||
// export_to_proto to IdMap
|
||||
if (initial_basis) {
|
||||
RepeatedField<int64_t>* const constraint_status_ids =
|
||||
ret.mutable_initial_basis()->mutable_constraint_status()->mutable_ids();
|
||||
@@ -78,7 +76,8 @@ ModelSolveParametersProto ModelSolveParameters::Proto() const {
|
||||
for (const LinearConstraint& key :
|
||||
initial_basis->constraint_status.SortedKeys()) {
|
||||
constraint_status_ids->Add(key.id());
|
||||
constraint_status_values->Add(initial_basis->constraint_status.at(key));
|
||||
constraint_status_values->Add(
|
||||
EnumToProto(initial_basis->constraint_status.at(key)));
|
||||
}
|
||||
RepeatedField<int64_t>* const variable_status_ids =
|
||||
ret.mutable_initial_basis()->mutable_variable_status()->mutable_ids();
|
||||
@@ -90,7 +89,33 @@ ModelSolveParametersProto ModelSolveParameters::Proto() const {
|
||||
variable_status_values->Reserve(initial_basis->variable_status.size());
|
||||
for (const Variable& key : initial_basis->variable_status.SortedKeys()) {
|
||||
variable_status_ids->Add(key.id());
|
||||
variable_status_values->Add(initial_basis->variable_status.at(key));
|
||||
variable_status_values->Add(
|
||||
EnumToProto(initial_basis->variable_status.at(key)));
|
||||
}
|
||||
}
|
||||
for (const SolutionHint& solution_hint : solution_hints) {
|
||||
SolutionHintProto& hint = *ret.add_solution_hints();
|
||||
RepeatedField<int64_t>* const variable_ids =
|
||||
hint.mutable_variable_values()->mutable_ids();
|
||||
RepeatedField<double>* const variable_values =
|
||||
hint.mutable_variable_values()->mutable_values();
|
||||
variable_ids->Reserve(solution_hint.variable_values.size());
|
||||
variable_values->Reserve(solution_hint.variable_values.size());
|
||||
for (const Variable& key : solution_hint.variable_values.SortedKeys()) {
|
||||
variable_ids->Add(key.id());
|
||||
variable_values->Add(solution_hint.variable_values.at(key));
|
||||
}
|
||||
}
|
||||
if (!branching_priorities.empty()) {
|
||||
RepeatedField<int64_t>* const variable_ids =
|
||||
ret.mutable_branching_priorities()->mutable_ids();
|
||||
RepeatedField<int32_t>* const variable_values =
|
||||
ret.mutable_branching_priorities()->mutable_values();
|
||||
variable_ids->Reserve(branching_priorities.size());
|
||||
variable_values->Reserve(branching_priorities.size());
|
||||
for (const Variable& key : branching_priorities.SortedKeys()) {
|
||||
variable_ids->Add(key.id());
|
||||
variable_values->Add(branching_priorities.at(key));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
|
||||
@@ -14,13 +14,16 @@
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_MODEL_SOLVE_PARAMETERS_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_MODEL_SOLVE_PARAMETERS_H_
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/linear_constraint.h"
|
||||
#include "ortools/math_opt/cpp/map_filter.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/result.h"
|
||||
#include "ortools/math_opt/cpp/solution.h"
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h"
|
||||
#include "ortools/math_opt/model_parameters.pb.h"
|
||||
|
||||
@@ -30,24 +33,22 @@ namespace math_opt {
|
||||
// Parameters to control a single solve that that are specific to the input
|
||||
// model (see SolveParametersProto for model independent parameters).
|
||||
struct ModelSolveParameters {
|
||||
// Returns the parameters that empty Result::DualSolution and Result::DualRay,
|
||||
// only keep the values of all variables in Result::PrimalSolution and
|
||||
// Result::PrimalRay.
|
||||
// Returns the parameters that empty DualSolution and DualRay, only keep the
|
||||
// values of all variables in PrimalSolution and PrimalRay.
|
||||
//
|
||||
// This is a shortcut method that is equivalent to setting the dual filters
|
||||
// with MakeSkipAllFilter().
|
||||
static ModelSolveParameters OnlyPrimalVariables();
|
||||
|
||||
// Returns the parameters that empty Result::DualSolution and Result::DualRay,
|
||||
// only keep the values of the specified variables in Result::PrimalSolution
|
||||
// and Result::PrimalRay.
|
||||
// Returns the parameters that empty DualSolution and DualRay, only keep the
|
||||
// values of the specified variables in PrimalSolution and PrimalRay.
|
||||
//
|
||||
// The input Collection must be usable in a for-range loop with Variable
|
||||
// values. This will be typically a std::vector<Variable> or and
|
||||
// std::initializer_list<Variable> (see the other overload).
|
||||
//
|
||||
// This is a shortcut method that is equivalent to setting the dual filters
|
||||
// with MakeSkipAllFilter() and the primal_variables_filter with
|
||||
// with MakeSkipAllFilter() and the variable_values_filter with
|
||||
// MakeKeepKeysFilter(variables).
|
||||
//
|
||||
// Example:
|
||||
@@ -58,9 +59,8 @@ struct ModelSolveParameters {
|
||||
static ModelSolveParameters OnlySomePrimalVariables(
|
||||
const Collection& variables);
|
||||
|
||||
// Returns the parameters that empty Result::DualSolution and Result::DualRay,
|
||||
// only keeping the values of the specified variables in
|
||||
// Result::PrimalSolution and Result::PrimalRay.
|
||||
// Returns the parameters that empty DualSolution and DualRay, only keeping
|
||||
// the values of the specified variables in PrimalSolution and PrimalRay.
|
||||
//
|
||||
// See the other overload's documentation for details. This overload is needed
|
||||
// since C++ can't guess the type when using an initializer list expression.
|
||||
@@ -73,28 +73,43 @@ struct ModelSolveParameters {
|
||||
static ModelSolveParameters OnlySomePrimalVariables(
|
||||
std::initializer_list<Variable> variables);
|
||||
|
||||
// The filter that is applied to variable_values of both
|
||||
// Result::PrimalSolution and Result::PrimalRay.
|
||||
MapFilter<Variable> primal_variables_filter;
|
||||
// The filter that is applied to variable_values of both PrimalSolution and
|
||||
// PrimalRay.
|
||||
MapFilter<Variable> variable_values_filter;
|
||||
|
||||
// The filter that is applied to dual_values of Result::DualSolution and
|
||||
// Result::DualRay.
|
||||
MapFilter<LinearConstraint> dual_linear_constraints_filter;
|
||||
// The filter that is applied to dual_values of DualSolution and DualRay.
|
||||
MapFilter<LinearConstraint> dual_values_filter;
|
||||
|
||||
// The filter that is applied to reduced_costs of Result::DualSolution and
|
||||
// Result::DualRay.
|
||||
MapFilter<Variable> dual_variables_filter;
|
||||
// The filter that is applied to reduced_costs of DualSolution and DualRay.
|
||||
MapFilter<Variable> reduced_costs_filter;
|
||||
|
||||
// Optional initial basis for warm starting simplex LP solvers. If set, it is
|
||||
// expected to be valid.
|
||||
absl::optional<Result::Basis> initial_basis;
|
||||
std::optional<Basis> initial_basis;
|
||||
|
||||
struct SolutionHint {
|
||||
SolutionHint() = default;
|
||||
VariableMap<double> variable_values;
|
||||
};
|
||||
|
||||
// Optional solution hints. If set, they are expected to consist of
|
||||
// assignments of finite values to primal or dual variables in the model (some
|
||||
// variables may lack assignments and the assignment does not necessarily have
|
||||
// to lead to a feasible solution).
|
||||
std::vector<SolutionHint> solution_hints;
|
||||
|
||||
// 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 (usualy zero). If set, they are expected to
|
||||
// consist of finite priorities for primal variables in the model.
|
||||
VariableMap<int32_t> branching_priorities;
|
||||
|
||||
// Returns the model of filtered keys. It returns a non-null value if and only
|
||||
// if one of the filters have a set and non empty filtered_keys().
|
||||
//
|
||||
// Asserts (using CHECK) that all variables and linear constraints referenced
|
||||
// by the filters are in the same model.
|
||||
IndexedModel* model() const;
|
||||
const ModelStorage* storage() const;
|
||||
|
||||
// Returns a new proto corresponding to these parameters.
|
||||
//
|
||||
@@ -111,7 +126,7 @@ template <typename Collection>
|
||||
ModelSolveParameters ModelSolveParameters::OnlySomePrimalVariables(
|
||||
const Collection& variables) {
|
||||
ModelSolveParameters parameters = OnlyPrimalVariables();
|
||||
parameters.primal_variables_filter = MakeKeepKeysFilter(variables);
|
||||
parameters.variable_values_filter = MakeKeepKeysFilter(variables);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/objective.h"
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
void Objective::Maximize(const LinearExpression& objective) const {
|
||||
SetObjective(objective, true);
|
||||
}
|
||||
|
||||
void Objective::Minimize(const LinearExpression& objective) const {
|
||||
SetObjective(objective, false);
|
||||
}
|
||||
|
||||
void Objective::SetObjective(const LinearExpression& objective,
|
||||
bool is_maximize) const {
|
||||
// LinearExpression that have no terms have a null model().
|
||||
if (!objective.raw_terms().empty()) {
|
||||
CHECK_EQ(objective.model(), model_)
|
||||
<< internal::kObjectsFromOtherIndexedModel;
|
||||
}
|
||||
model_->clear_objective();
|
||||
model_->set_is_maximize(is_maximize);
|
||||
model_->set_objective_offset(objective.offset());
|
||||
for (auto [var, coef] : objective.raw_terms()) {
|
||||
model_->set_linear_objective_coefficient(var, coef);
|
||||
}
|
||||
}
|
||||
|
||||
void Objective::Add(const LinearExpression& objective_terms) const {
|
||||
// LinearExpression that have no terms have a null model().
|
||||
if (!objective_terms.raw_terms().empty()) {
|
||||
CHECK_EQ(objective_terms.model(), model_)
|
||||
<< internal::kObjectsFromOtherIndexedModel;
|
||||
}
|
||||
model_->set_objective_offset(objective_terms.offset() +
|
||||
model_->objective_offset());
|
||||
for (auto [var, coef] : objective_terms.raw_terms()) {
|
||||
model_->set_linear_objective_coefficient(
|
||||
var, coef + model_->linear_objective_coefficient(var));
|
||||
}
|
||||
}
|
||||
|
||||
LinearExpression Objective::AsLinearExpression() const {
|
||||
LinearExpression result = model_->objective_offset();
|
||||
for (const auto& [v, coef] : model_->linear_objective()) {
|
||||
result += Variable(model_, v) * coef;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
@@ -1,108 +0,0 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// An object oriented wrapper for the linear objective in IndexedModel.
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_OBJECTIVE_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_OBJECTIVE_H_
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// The objective of an optimization problem for an IndexedModel.
|
||||
//
|
||||
// Objective is a value type and is typically passed by copy.
|
||||
class Objective {
|
||||
public:
|
||||
inline explicit Objective(IndexedModel* model);
|
||||
|
||||
inline IndexedModel* model() const;
|
||||
|
||||
// Setting a value to 0.0 will delete the variable from the underlying sparse
|
||||
// representation (and has no effect if the variable is not present).
|
||||
inline void set_linear_coefficient(Variable variable, double value) const;
|
||||
|
||||
inline bool is_linear_coefficient_nonzero(Variable variable) const;
|
||||
|
||||
// Returns 0.0 if this variable has no linear objective coefficient.
|
||||
inline double linear_coefficient(Variable variable) const;
|
||||
|
||||
inline void set_offset(double value) const;
|
||||
inline double offset() const;
|
||||
|
||||
// Equivalent to calling set_linear_coefficient(v, 0.0) for every variable
|
||||
// with nonzero objective coefficient.
|
||||
//
|
||||
// Runs in O(#variables with nonzero objective coefficient).
|
||||
inline void clear() const;
|
||||
inline bool is_maximize() const;
|
||||
inline void set_maximize() const;
|
||||
inline void set_minimize() const;
|
||||
|
||||
// Prefer set_maximize() and set_minimize() above for more readable code.
|
||||
inline void set_is_maximize(bool is_maximize) const;
|
||||
|
||||
void Maximize(const LinearExpression& objective) const;
|
||||
void Minimize(const LinearExpression& objective) const;
|
||||
void SetObjective(const LinearExpression& objective, bool is_maximize) const;
|
||||
void Add(const LinearExpression& objective_terms) const;
|
||||
|
||||
LinearExpression AsLinearExpression() const;
|
||||
|
||||
private:
|
||||
IndexedModel* model_;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Inline function implementations
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Objective::Objective(IndexedModel* const model) : model_(model) {}
|
||||
|
||||
IndexedModel* Objective::model() const { return model_; }
|
||||
|
||||
void Objective::set_linear_coefficient(const Variable variable,
|
||||
const double value) const {
|
||||
CHECK_EQ(variable.model(), model_) << internal::kObjectsFromOtherIndexedModel;
|
||||
model_->set_linear_objective_coefficient(variable.typed_id(), value);
|
||||
}
|
||||
bool Objective::is_linear_coefficient_nonzero(const Variable variable) const {
|
||||
CHECK_EQ(variable.model(), model_) << internal::kObjectsFromOtherIndexedModel;
|
||||
return model_->is_linear_objective_coefficient_nonzero(variable.typed_id());
|
||||
}
|
||||
double Objective::linear_coefficient(const Variable variable) const {
|
||||
CHECK_EQ(variable.model(), model_) << internal::kObjectsFromOtherIndexedModel;
|
||||
return model_->linear_objective_coefficient(variable.typed_id());
|
||||
}
|
||||
|
||||
void Objective::set_offset(const double value) const {
|
||||
model_->set_objective_offset(value);
|
||||
}
|
||||
double Objective::offset() const { return model_->objective_offset(); }
|
||||
|
||||
void Objective::clear() const { model_->clear_objective(); }
|
||||
bool Objective::is_maximize() const { return model_->is_maximize(); }
|
||||
void Objective::set_is_maximize(const bool is_maximize) const {
|
||||
model_->set_is_maximize(is_maximize);
|
||||
}
|
||||
void Objective::set_maximize() const { model_->set_maximize(); }
|
||||
void Objective::set_minimize() const { model_->set_minimize(); }
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_OBJECTIVE_H_
|
||||
223
ortools/math_opt/cpp/parameters.cc
Normal file
223
ortools/math_opt/cpp/parameters.cc
Normal file
@@ -0,0 +1,223 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/parameters.h"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/math_opt/solvers/gurobi.pb.h"
|
||||
#include "ortools/port/proto_utils.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
#include "ortools/base/protoutil.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
std::optional<absl::string_view> Enum<SolverType>::ToOptString(
|
||||
SolverType value) {
|
||||
switch (value) {
|
||||
case SolverType::kGscip:
|
||||
return "gscip";
|
||||
case SolverType::kGurobi:
|
||||
return "gurobi";
|
||||
case SolverType::kGlop:
|
||||
return "glop";
|
||||
case SolverType::kCpSat:
|
||||
return "cp_sat";
|
||||
case SolverType::kGlpk:
|
||||
return "glpk";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::Span<const SolverType> Enum<SolverType>::AllValues() {
|
||||
static constexpr SolverType kSolverTypeValues[] = {
|
||||
SolverType::kGscip, SolverType::kGurobi, SolverType::kGlop,
|
||||
SolverType::kCpSat,
|
||||
SolverType::kGlpk,
|
||||
};
|
||||
return absl::MakeConstSpan(kSolverTypeValues);
|
||||
}
|
||||
|
||||
bool AbslParseFlag(const absl::string_view text, SolverType* const value,
|
||||
std::string* const error) {
|
||||
const std::optional enum_value = EnumFromString<SolverType>(text);
|
||||
if (!enum_value.has_value()) {
|
||||
*error = "unknown value for enumeration";
|
||||
return false;
|
||||
}
|
||||
|
||||
*value = *enum_value;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string AbslUnparseFlag(const SolverType value) {
|
||||
std::ostringstream oss;
|
||||
oss << value;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::optional<absl::string_view> Enum<LPAlgorithm>::ToOptString(
|
||||
LPAlgorithm value) {
|
||||
switch (value) {
|
||||
case LPAlgorithm::kPrimalSimplex:
|
||||
return "primal_simplex";
|
||||
case LPAlgorithm::kDualSimplex:
|
||||
return "dual_simplex";
|
||||
case LPAlgorithm::kBarrier:
|
||||
return "barrier";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::Span<const LPAlgorithm> Enum<LPAlgorithm>::AllValues() {
|
||||
static constexpr LPAlgorithm kLPAlgorithmValues[] = {
|
||||
LPAlgorithm::kPrimalSimplex,
|
||||
LPAlgorithm::kDualSimplex,
|
||||
LPAlgorithm::kBarrier,
|
||||
};
|
||||
return absl::MakeConstSpan(kLPAlgorithmValues);
|
||||
}
|
||||
|
||||
std::optional<absl::string_view> Enum<Emphasis>::ToOptString(Emphasis value) {
|
||||
switch (value) {
|
||||
case Emphasis::kOff:
|
||||
return "off";
|
||||
case Emphasis::kLow:
|
||||
return "low";
|
||||
case Emphasis::kMedium:
|
||||
return "medium";
|
||||
case Emphasis::kHigh:
|
||||
return "high";
|
||||
case Emphasis::kVeryHigh:
|
||||
return "very_high";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::Span<const Emphasis> Enum<Emphasis>::AllValues() {
|
||||
static constexpr Emphasis kEmphasisValues[] = {
|
||||
Emphasis::kOff, Emphasis::kLow, Emphasis::kMedium,
|
||||
Emphasis::kHigh, Emphasis::kVeryHigh,
|
||||
};
|
||||
return absl::MakeConstSpan(kEmphasisValues);
|
||||
}
|
||||
|
||||
StrictnessProto Strictness::Proto() const {
|
||||
StrictnessProto result;
|
||||
result.set_bad_parameter(bad_parameter);
|
||||
return result;
|
||||
}
|
||||
|
||||
Strictness Strictness::FromProto(const StrictnessProto& proto) {
|
||||
return {.bad_parameter = proto.bad_parameter()};
|
||||
}
|
||||
|
||||
GurobiParametersProto GurobiParameters::Proto() const {
|
||||
GurobiParametersProto result;
|
||||
for (const auto& [key, val] : param_values) {
|
||||
GurobiParametersProto::Parameter& p = *result.add_parameters();
|
||||
p.set_name(key);
|
||||
p.set_value(val);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
GurobiParameters GurobiParameters::FromProto(
|
||||
const GurobiParametersProto& proto) {
|
||||
GurobiParameters result;
|
||||
for (const GurobiParametersProto::Parameter& p : proto.parameters()) {
|
||||
result.param_values[p.name()] = p.value();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
SolveParametersProto SolveParameters::Proto() const {
|
||||
SolveParametersProto result;
|
||||
*result.mutable_strictness() = strictness.Proto();
|
||||
result.set_enable_output(enable_output);
|
||||
if (time_limit < absl::InfiniteDuration()) {
|
||||
CHECK_OK(util_time::EncodeGoogleApiProto(time_limit,
|
||||
result.mutable_time_limit()));
|
||||
}
|
||||
if (iteration_limit.has_value()) {
|
||||
result.set_iteration_limit(*iteration_limit);
|
||||
}
|
||||
if (threads.has_value()) {
|
||||
result.set_threads(*threads);
|
||||
}
|
||||
if (random_seed.has_value()) {
|
||||
result.set_random_seed(*random_seed);
|
||||
}
|
||||
if (relative_gap_limit.has_value()) {
|
||||
result.set_relative_gap_limit(*relative_gap_limit);
|
||||
}
|
||||
if (absolute_gap_limit.has_value()) {
|
||||
result.set_absolute_gap_limit(*absolute_gap_limit);
|
||||
}
|
||||
result.set_lp_algorithm(EnumToProto(lp_algorithm));
|
||||
result.set_presolve(EnumToProto(presolve));
|
||||
result.set_cuts(EnumToProto(cuts));
|
||||
result.set_heuristics(EnumToProto(heuristics));
|
||||
result.set_scaling(EnumToProto(scaling));
|
||||
*result.mutable_gscip() = gscip;
|
||||
*result.mutable_gurobi() = gurobi.Proto();
|
||||
*result.mutable_glop() = glop;
|
||||
*result.mutable_cp_sat() = cp_sat;
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<SolveParameters> SolveParameters::FromProto(
|
||||
const SolveParametersProto& proto) {
|
||||
SolveParameters result;
|
||||
result.strictness = Strictness::FromProto(proto.strictness());
|
||||
result.enable_output = proto.enable_output();
|
||||
if (proto.has_time_limit()) {
|
||||
ASSIGN_OR_RETURN(result.time_limit,
|
||||
util_time::DecodeGoogleApiProto(proto.time_limit()));
|
||||
} else {
|
||||
result.time_limit = absl::InfiniteDuration();
|
||||
}
|
||||
if (proto.has_iteration_limit()) {
|
||||
result.iteration_limit = proto.iteration_limit();
|
||||
}
|
||||
if (proto.has_threads()) {
|
||||
result.threads = proto.threads();
|
||||
}
|
||||
if (proto.has_random_seed()) {
|
||||
result.random_seed = proto.random_seed();
|
||||
}
|
||||
if (proto.has_absolute_gap_limit()) {
|
||||
result.absolute_gap_limit = proto.absolute_gap_limit();
|
||||
}
|
||||
if (proto.has_relative_gap_limit()) {
|
||||
result.relative_gap_limit = proto.relative_gap_limit();
|
||||
}
|
||||
result.lp_algorithm = EnumFromProto(proto.lp_algorithm());
|
||||
result.presolve = EnumFromProto(proto.presolve());
|
||||
result.cuts = EnumFromProto(proto.cuts());
|
||||
result.heuristics = EnumFromProto(proto.heuristics());
|
||||
result.scaling = EnumFromProto(proto.scaling());
|
||||
result.gscip = proto.gscip();
|
||||
result.gurobi = GurobiParameters::FromProto(proto.gurobi());
|
||||
result.glop = proto.glop();
|
||||
result.cp_sat = proto.cp_sat();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
277
ortools/math_opt/cpp/parameters.h
Normal file
277
ortools/math_opt/cpp/parameters.h
Normal file
@@ -0,0 +1,277 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_PARAMETERS_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_PARAMETERS_H_
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/base/linked_hash_map.h"
|
||||
#include "ortools/glop/parameters.pb.h" // IWYU pragma: export
|
||||
#include "ortools/gscip/gscip.pb.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/enums.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/parameters.pb.h"
|
||||
#include "ortools/math_opt/solvers/gurobi.pb.h" // IWYU pragma: export
|
||||
#include "ortools/sat/sat_parameters.pb.h" // IWYU pragma: export
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// The solvers wrapped by MathOpt.
|
||||
enum class SolverType {
|
||||
// Solving Constraint Integer Programs (SCIP) solver.
|
||||
//
|
||||
// It supports both MIPs and LPs. No dual data for LPs is returned though. To
|
||||
// solve LPs, kGlop should be preferred.
|
||||
kGscip = SOLVER_TYPE_GSCIP,
|
||||
|
||||
// Gurobi solver.
|
||||
//
|
||||
// It supports both MIPs and LPs.
|
||||
kGurobi = SOLVER_TYPE_GUROBI,
|
||||
|
||||
// Google's Glop linear solver.
|
||||
//
|
||||
// It only solves LPs.
|
||||
kGlop = SOLVER_TYPE_GLOP,
|
||||
|
||||
// Google's CP-SAT solver.
|
||||
//
|
||||
// It supports solving IPs and can scale MIPs to solve them as IPs.
|
||||
kCpSat = SOLVER_TYPE_CP_SAT,
|
||||
|
||||
|
||||
// GNU Linear Programming Kit (GLPK).
|
||||
//
|
||||
// It supports both MIPs and LPs.
|
||||
//
|
||||
// Thread-safety: GLPK use thread-local storage for memory allocations. As a
|
||||
// consequence when using IncrementalSolver, the user must make sure that
|
||||
// instances are destroyed on the same thread as they are created or GLPK will
|
||||
// crash. It seems OK to call IncrementalSolver::Solve() from another thread
|
||||
// than the one used to create the Solver but it is not documented by GLPK and
|
||||
// should be avoided. Of course these limitations do not apply to the Solve()
|
||||
// function that recreates a new GLPK problem in the calling thread and
|
||||
// destroys before returning.
|
||||
//
|
||||
// When solving a LP with the presolver, a solution (and the unbound rays) are
|
||||
// only returned if an optimal solution has been found. Else nothing is
|
||||
// returned. See glpk-5.0/doc/glpk.pdf page #40 available from glpk-5.0.tar.gz
|
||||
// for details.
|
||||
kGlpk = SOLVER_TYPE_GLPK,
|
||||
|
||||
};
|
||||
|
||||
MATH_OPT_DEFINE_ENUM(SolverType, SOLVER_TYPE_UNSPECIFIED);
|
||||
|
||||
// Parses a flag of type SolverType.
|
||||
//
|
||||
// The expected values are the one returned by EnumToString().
|
||||
bool AbslParseFlag(absl::string_view text, SolverType* value,
|
||||
std::string* error);
|
||||
|
||||
// Unparses a flag of type SolverType.
|
||||
//
|
||||
// The returned values are the same as EnumToString().
|
||||
std::string AbslUnparseFlag(SolverType value);
|
||||
|
||||
// Selects an algorithm for solving linear programs.
|
||||
enum class LPAlgorithm {
|
||||
// The (primal) simplex method. Typically can provide primal and dual
|
||||
// solutions, primal/dual rays on primal/dual unbounded problems, and a basis.
|
||||
kPrimalSimplex = LP_ALGORITHM_PRIMAL_SIMPLEX,
|
||||
|
||||
// The dual simplex method. Typically can provide primal and dual
|
||||
// solutions, primal/dual rays on primal/dual unbounded problems, and a basis.
|
||||
kDualSimplex = LP_ALGORITHM_DUAL_SIMPLEX,
|
||||
|
||||
// The barrier method, also commonly called an interior point method (IPM).
|
||||
// Can typically give both primal and dual solutions. Some implementations can
|
||||
// also produce rays on unbounded/infeasible problems. A basis is not given
|
||||
// unless the underlying solver does "crossover" and finishes with simplex.
|
||||
kBarrier = LP_ALGORITHM_BARRIER
|
||||
};
|
||||
|
||||
MATH_OPT_DEFINE_ENUM(LPAlgorithm, LP_ALGORITHM_UNSPECIFIED);
|
||||
|
||||
// Effort level applied to an optional task while solving (see SolveParameters
|
||||
// for use).
|
||||
//
|
||||
// Typically used as a std::optional<Emphasis>. It used to configure a solver
|
||||
// feature as follows:
|
||||
// * If a solver doesn't support the feature, only nullopt and kOff are
|
||||
// valid, any other setting will give either a warning or error (as
|
||||
// configured for Strictness).
|
||||
// * If the solver supports the feature:
|
||||
// - When unset, the underlying default is used.
|
||||
// - When the feature cannot be turned off, kOff will a warning/error.
|
||||
// - If the feature is enabled by default, the solver default is typically
|
||||
// mapped to kMedium.
|
||||
// - If the feature is supported, kLow, kMedium, kHigh, and kVeryHigh will
|
||||
// never give a warning or error, and will map onto their best match.
|
||||
enum class Emphasis {
|
||||
kOff = EMPHASIS_OFF,
|
||||
kLow = EMPHASIS_LOW,
|
||||
kMedium = EMPHASIS_MEDIUM,
|
||||
kHigh = EMPHASIS_HIGH,
|
||||
kVeryHigh = EMPHASIS_VERY_HIGH
|
||||
};
|
||||
|
||||
MATH_OPT_DEFINE_ENUM(Emphasis, EMPHASIS_UNSPECIFIED);
|
||||
|
||||
// Configures if potentially bad solver input is a warning or an error.
|
||||
struct Strictness {
|
||||
// If true, warnings on bad parameters are converted to Status errors.
|
||||
bool bad_parameter = false;
|
||||
|
||||
StrictnessProto Proto() const;
|
||||
static Strictness FromProto(const StrictnessProto& proto);
|
||||
};
|
||||
|
||||
// Gurobi specific parameters for solving. See
|
||||
// https://www.gurobi.com/documentation/9.1/refman/parameters.html
|
||||
// for a list of possible parameters.
|
||||
//
|
||||
// Example use:
|
||||
// GurobiParameters gurobi;
|
||||
// gurobi.param_values["BarIterLimit"] = "10";
|
||||
//
|
||||
// With Gurobi, the order that parameters are applied can have an impact in rare
|
||||
// situations. Parameters are applied in the following order:
|
||||
// * LogToConsole is set from SolveParameters.enable_output.
|
||||
// * Any common parameters not overwritten by GurobiParameters.
|
||||
// * param_values in iteration order (insertion order).
|
||||
// We set LogToConsole first because setting other parameters can generate
|
||||
// output.
|
||||
struct GurobiParameters {
|
||||
// Parameter name-value pairs to set in insertion order.
|
||||
gtl::linked_hash_map<std::string, std::string> param_values;
|
||||
|
||||
GurobiParametersProto Proto() const;
|
||||
static GurobiParameters FromProto(const GurobiParametersProto& proto);
|
||||
|
||||
bool empty() const { return param_values.empty(); }
|
||||
};
|
||||
|
||||
// Parameters to control a single solve.
|
||||
//
|
||||
// Contains both parameters common to all solvers e.g. time_limit, and
|
||||
// parameters for a specific solver, e.g. gscip. If a value is set in both
|
||||
// common and solver specific field, the solver specific setting is used.
|
||||
//
|
||||
// The common parameters that are optional and unset indicate that the solver
|
||||
// default is used.
|
||||
//
|
||||
// Solver specific parameters for solvers other than the one in use are ignored.
|
||||
//
|
||||
// Parameters that depends on the model (e.g. branching priority is set for
|
||||
// each variable) are passed in ModelSolveParametersProto.
|
||||
struct SolveParameters {
|
||||
// Enables printing the solver implementation traces. These traces are sent
|
||||
// to the standard output stream.
|
||||
//
|
||||
// Note that if the solver supports message callback and the user registers a
|
||||
// callback for it, then this parameter value is ignored and no traces are
|
||||
// printed.
|
||||
bool enable_output = false;
|
||||
|
||||
// Maximum time a solver should spend on the problem.
|
||||
//
|
||||
// This value is not a hard limit, solve time may slightly exceed this value.
|
||||
// Always passed to the underlying solver, the solver default is not used.
|
||||
absl::Duration time_limit = absl::InfiniteDuration();
|
||||
|
||||
// Limit on the iterations of the underlying algorithm (e.g. simplex pivots).
|
||||
// The specific behavior is dependent on the solver and algorithm used, but
|
||||
// should result in a deterministic solve limit.
|
||||
// TODO(b/195295177): suggest node_limit as an alternative when it's added
|
||||
std::optional<int64_t> iteration_limit;
|
||||
|
||||
// Optimality tolerances (primarily) for MIP solvers. The absolute GAP of a
|
||||
// feasible solution is the distance between its objective value and a dual
|
||||
// bound (e.g. an upper bound on the optimal value for maximization problems).
|
||||
// The relative GAP is a solver-dependent scaled version of the absolute GAP
|
||||
// (e.g. it could be the relative GAP divided by the objective value of the
|
||||
// feasible solution if this is non-zero). Solvers consider a solution optimal
|
||||
// if its GAPs are below these limits (most solvers use both versions).
|
||||
std::optional<double> relative_gap_limit;
|
||||
std::optional<double> absolute_gap_limit;
|
||||
|
||||
// If unset, use the solver default. If set, it must be >= 1.
|
||||
std::optional<int32_t> threads;
|
||||
|
||||
// Seed for the pseudo-random number generator in the underlying
|
||||
// solver. Note that all solvers use pseudo-random numbers to select things
|
||||
// such as perturbation in the LP algorithm, for tie-break-up rules, and for
|
||||
// heuristic fixings. Varying this can have a noticeable impact on solver
|
||||
// behavior.
|
||||
//
|
||||
// Although all solvers have a concept of seeds, note that valid values
|
||||
// depend on the actual solver.
|
||||
// - Gurobi: [0:GRB_MAXINT] (which as of Gurobi 9.0 is 2x10^9).
|
||||
// - GSCIP: [0:2147483647] (which is MAX_INT or kint32max or 2^31-1).
|
||||
// - GLOP: [0:2147483647] (same as above)
|
||||
// In all cases, the solver will receive a value equal to:
|
||||
// MAX(0, MIN(MAX_VALID_VALUE_FOR_SOLVER, random_seed)).
|
||||
std::optional<int32_t> random_seed;
|
||||
|
||||
// The algorithm for solving a linear program. If nullopt, use the solver
|
||||
// default algorithm.
|
||||
//
|
||||
// For problems that are not linear programs but where linear programming is
|
||||
// a subroutine, solvers may use this value. E.g. MIP solvers will typically
|
||||
// use this for the root LP solve only (and use dual simplex otherwise).
|
||||
std::optional<LPAlgorithm> lp_algorithm;
|
||||
|
||||
// Effort on simplifying the problem before starting the main algorithm, or
|
||||
// the solver default effort level if unset.
|
||||
std::optional<Emphasis> presolve;
|
||||
|
||||
// Effort on getting a stronger LP relaxation (MIP only) or the solver default
|
||||
// effort level if unset.
|
||||
//
|
||||
// NOTE: disabling cuts may prevent callbacks from having a chance to add cuts
|
||||
// at MIP_NODE, this behavior is solver specific.
|
||||
std::optional<Emphasis> cuts;
|
||||
|
||||
// Effort in finding feasible solutions beyond those encountered in the
|
||||
// complete search procedure (MIP only), or the solver default effort level if
|
||||
// unset.
|
||||
std::optional<Emphasis> heuristics;
|
||||
|
||||
// Effort in rescaling the problem to improve numerical stability, or the
|
||||
// solver default effort level if unset.
|
||||
std::optional<Emphasis> scaling;
|
||||
|
||||
GScipParameters gscip;
|
||||
GurobiParameters gurobi;
|
||||
glop::GlopParameters glop;
|
||||
sat::SatParameters cp_sat;
|
||||
|
||||
|
||||
// TODO(b/196132970): this needs to move into SolverInitializerProto.
|
||||
Strictness strictness;
|
||||
|
||||
SolveParametersProto Proto() const;
|
||||
static absl::StatusOr<SolveParameters> FromProto(
|
||||
const SolveParametersProto& proto);
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_PARAMETERS_H_
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/result.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/math_opt/core/indexed_model.h"
|
||||
#include "ortools/math_opt/result.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
Result::PrimalSolution::PrimalSolution(IndexedModel* const model,
|
||||
IndexedPrimalSolution indexed_solution)
|
||||
: variable_values(model, std::move(indexed_solution.variable_values)),
|
||||
objective_value(indexed_solution.objective_value) {}
|
||||
|
||||
Result::PrimalRay::PrimalRay(IndexedModel* const model,
|
||||
IndexedPrimalRay indexed_ray)
|
||||
: variable_values(model, std::move(indexed_ray.variable_values)) {}
|
||||
|
||||
Result::DualSolution::DualSolution(IndexedModel* const model,
|
||||
IndexedDualSolution indexed_solution)
|
||||
: dual_values(model, std::move(indexed_solution.dual_values)),
|
||||
reduced_costs(model, std::move(indexed_solution.reduced_costs)),
|
||||
objective_value(indexed_solution.objective_value) {}
|
||||
|
||||
Result::DualRay::DualRay(IndexedModel* const model, IndexedDualRay indexed_ray)
|
||||
: dual_values(model, std::move(indexed_ray.dual_values)),
|
||||
reduced_costs(model, std::move(indexed_ray.reduced_costs)) {}
|
||||
|
||||
Result::Basis::Basis(IndexedModel* const model, IndexedBasis indexed_basis)
|
||||
: constraint_status(model, std::move(indexed_basis.constraint_status)),
|
||||
variable_status(model, std::move(indexed_basis.variable_status)) {}
|
||||
|
||||
Result::Result(IndexedModel* const model, const SolveResultProto& solve_result)
|
||||
: warnings(solve_result.warnings().begin(), solve_result.warnings().end()),
|
||||
termination_reason(solve_result.termination_reason()),
|
||||
termination_detail(solve_result.termination_detail()),
|
||||
solve_stats(solve_result.solve_stats()) {
|
||||
IndexedSolutions solutions = IndexedSolutionsFromProto(solve_result);
|
||||
for (auto& primal_solution : solutions.primal_solutions) {
|
||||
primal_solutions.emplace_back(model, std::move(primal_solution));
|
||||
}
|
||||
for (auto& primal_ray : solutions.primal_rays) {
|
||||
primal_rays.emplace_back(model, std::move(primal_ray));
|
||||
}
|
||||
for (auto& dual_solution : solutions.dual_solutions) {
|
||||
dual_solutions.emplace_back(model, std::move(dual_solution));
|
||||
}
|
||||
for (auto& dual_ray : solutions.dual_rays) {
|
||||
dual_rays.emplace_back(model, std::move(dual_ray));
|
||||
}
|
||||
for (auto& base : solutions.basis) {
|
||||
basis.emplace_back(model, std::move(base));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
@@ -1,303 +0,0 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_RESULT_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_RESULT_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "ortools/math_opt/core/indexed_model.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/base/protoutil.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// The result of solving an optimization problem with MathOpt::Solve.
|
||||
//
|
||||
// TODO(b/172211596): there is already a parallel proto named solve result, the
|
||||
// naming convention should be more consistent.
|
||||
struct Result {
|
||||
// A solution to an optimization problem.
|
||||
//
|
||||
// E.g. consider a simple linear program:
|
||||
// min c * x
|
||||
// s.t. A * x >= b
|
||||
// x >= 0.
|
||||
// A primal solution is assignment values to x. It is feasible if it satisfies
|
||||
// A * x >= b and x >= 0 from above. In the class PrimalSolution below,
|
||||
// variable_values is x and objective_value is c * x.
|
||||
//
|
||||
// For the general case of a MathOpt optimization model, see
|
||||
// go/mathopt-solutions for details.
|
||||
struct PrimalSolution {
|
||||
PrimalSolution() = default;
|
||||
PrimalSolution(IndexedModel* model, IndexedPrimalSolution indexed_solution);
|
||||
|
||||
VariableMap<double> variable_values;
|
||||
double objective_value = 0.0;
|
||||
};
|
||||
|
||||
// A direction of unbounded improvement to an optimization problem;
|
||||
// equivalently, a certificate of infeasibility for the dual of the
|
||||
// optimization problem.
|
||||
//
|
||||
// E.g. consider a simple linear program:
|
||||
// min c * x
|
||||
// s.t. A * x >= b
|
||||
// x >= 0
|
||||
// A primal ray is an x that satisfies:
|
||||
// c * x < 0
|
||||
// A * x >= 0
|
||||
// x >= 0
|
||||
// Observe that given a feasible solution, any positive multiple of the primal
|
||||
// ray plus that solution is still feasible, and gives a better objective
|
||||
// value. A primal ray also proves the dual optimization problem infeasible.
|
||||
//
|
||||
// In the class PrimalRay below, variable_values is this x.
|
||||
//
|
||||
// For the general case of a MathOpt optimization model, see
|
||||
// go/mathopt-solutions for details.
|
||||
struct PrimalRay {
|
||||
PrimalRay() = default;
|
||||
PrimalRay(IndexedModel* model, IndexedPrimalRay indexed_ray);
|
||||
|
||||
VariableMap<double> variable_values;
|
||||
};
|
||||
|
||||
// A solution to the dual of an optimization problem.
|
||||
//
|
||||
// E.g. consider the primal dual pair linear program pair:
|
||||
// (Primal) (Dual)
|
||||
// min c * x max b * y
|
||||
// s.t. A * x >= b s.t. y * A + r = c
|
||||
// x >= 0 y, r >= 0.
|
||||
// The dual solution is the pair (y, r). It is feasible if it satisfies the
|
||||
// constraints from (Dual) above.
|
||||
//
|
||||
// Below, y is dual_values, r is reduced_costs, and b * y is objective value.
|
||||
//
|
||||
// For the general case, see go/mathopt-solutions and go/mathopt-dual (and
|
||||
// note that the dual objective depends on r in the general case).
|
||||
struct DualSolution {
|
||||
DualSolution() = default;
|
||||
DualSolution(IndexedModel* model, IndexedDualSolution indexed_solution);
|
||||
|
||||
LinearConstraintMap<double> dual_values;
|
||||
VariableMap<double> reduced_costs;
|
||||
double objective_value = 0.0;
|
||||
};
|
||||
|
||||
// A direction of unbounded improvement to the dual of an optimization,
|
||||
// problem; equivalently, a certificate of primal infeasibility.
|
||||
//
|
||||
// E.g. consider the primal dual pair linear program pair:
|
||||
// (Primal) (Dual)
|
||||
// min c * x max b * y
|
||||
// s.t. A * x >= b s.t. y * A + r = c
|
||||
// x >= 0 y, r >= 0.
|
||||
// The dual ray is the pair (y, r) satisfying:
|
||||
// b * y > 0
|
||||
// y * A + r = 0
|
||||
// y, r >= 0
|
||||
// Observe that adding a positive multiple of (y, r) to dual feasible solution
|
||||
// maintains dual feasibility and improves the objective (proving the dual is
|
||||
// unbounded). The dual ray also proves the primal problem is infeasible.
|
||||
//
|
||||
// In the class DualRay below, y is dual_values and r is reduced_costs.
|
||||
//
|
||||
// For the general case, see go/mathopt-solutions and go/mathopt-dual (and
|
||||
// note that the dual objective depends on r in the general case).
|
||||
struct DualRay {
|
||||
DualRay() = default;
|
||||
DualRay(IndexedModel* model, IndexedDualRay indexed_ray);
|
||||
|
||||
LinearConstraintMap<double> dual_values;
|
||||
VariableMap<double> reduced_costs;
|
||||
};
|
||||
|
||||
// A combinatorial characterization for a solution to a linear program.
|
||||
//
|
||||
// The simplex method for solving linear programs always returns a "basic
|
||||
// feasible solution" which can be described combinatorially as a Basis. A
|
||||
// basis assigns a BasisStatus for every variable and linear constraint.
|
||||
//
|
||||
// E.g. consider a standard form LP:
|
||||
// min c * x
|
||||
// s.t. A * x = b
|
||||
// x >= 0
|
||||
// that has more variables than constraints and with full row rank A.
|
||||
//
|
||||
// Let n be the number of variables and m the number of linear constraints. A
|
||||
// valid basis for this problem can be constructed as follows:
|
||||
// * All constraints will have basis status FIXED.
|
||||
// * Pick m variables such that the columns of A are linearly independent and
|
||||
// assign the status BASIC.
|
||||
// * Assign the status AT_LOWER for the remaining n - m variables.
|
||||
//
|
||||
// The basic solution for this basis is the unique solution of A * x = b that
|
||||
// has all variables with status AT_LOWER fixed to their lower bounds (all
|
||||
// zero). The resulting solution is called a basic feasible solution if it
|
||||
// also satisfies x >= 0.
|
||||
//
|
||||
// See go/mathopt-basis for treatment of the general case and an explanation
|
||||
// of how a dual solution is determined for a basis.
|
||||
struct Basis {
|
||||
Basis() = default;
|
||||
Basis(IndexedModel* model, IndexedBasis indexed_basis);
|
||||
|
||||
LinearConstraintMap<BasisStatus> constraint_status;
|
||||
VariableMap<BasisStatus> variable_status;
|
||||
};
|
||||
|
||||
Result(IndexedModel* model, const SolveResultProto& solve_result);
|
||||
|
||||
// The objective value of the best primal solution. Will CHECK fail if there
|
||||
// are no primal solutions.
|
||||
double objective_value() const {
|
||||
CHECK(has_solution());
|
||||
return primal_solutions[0].objective_value;
|
||||
}
|
||||
|
||||
absl::Duration solve_time() const {
|
||||
return util_time::DecodeGoogleApiProto(solve_stats.solve_time()).value();
|
||||
}
|
||||
|
||||
// Indicates if at least one primal feasible solution is available.
|
||||
//
|
||||
// When termination_reason is TERMINATION_REASON_OPTIMAL, this is guaranteed
|
||||
// to be true and need not be checked.
|
||||
bool has_solution() const { return !primal_solutions.empty(); }
|
||||
|
||||
// The variable values from the best primal solution. Will CHECK fail if there
|
||||
// are no primal solutions.
|
||||
const VariableMap<double>& variable_values() const {
|
||||
CHECK(has_solution());
|
||||
return primal_solutions[0].variable_values;
|
||||
}
|
||||
|
||||
// Indicates if at least one primal ray is available.
|
||||
//
|
||||
// This is NOT guaranteed to be true when termination_reason is
|
||||
// UNBOUNDED or DUAL_INFEASIBLE.
|
||||
bool has_ray() const { return !primal_rays.empty(); }
|
||||
|
||||
// The variable values from the first primal ray. Will CHECK fail if there
|
||||
// are no primal rays.
|
||||
const VariableMap<double>& ray_variable_values() const {
|
||||
CHECK(has_ray());
|
||||
return primal_rays[0].variable_values;
|
||||
}
|
||||
|
||||
// Indicates if at least one dual solution is available.
|
||||
//
|
||||
// This is NOT guaranteed to be true when termination_reason is
|
||||
// TERMINATION_REASON_OPTIMAL.
|
||||
bool has_dual_solution() const { return !dual_solutions.empty(); }
|
||||
|
||||
// The dual values from the best dual solution. Will CHECK fail if there
|
||||
// are no dual solutions.
|
||||
const LinearConstraintMap<double>& dual_values() const {
|
||||
CHECK(has_dual_solution());
|
||||
return dual_solutions[0].dual_values;
|
||||
}
|
||||
|
||||
// The reduced from the best dual solution. Will CHECK fail if there
|
||||
// are no dual solutions.
|
||||
// TODO(b/174564572): if reduced_costs in DualSolution was something like
|
||||
// dual_reduced cost it would help prevent people forgetting to call
|
||||
// has_dual_solution().
|
||||
const VariableMap<double>& reduced_costs() const {
|
||||
CHECK(has_dual_solution());
|
||||
return dual_solutions[0].reduced_costs;
|
||||
}
|
||||
|
||||
// Indicates if at least one dual ray is available.
|
||||
//
|
||||
// This is NOT guaranteed to be true when termination_reason is
|
||||
// INFEASIBLE.
|
||||
bool has_dual_ray() const { return !dual_rays.empty(); }
|
||||
|
||||
// The dual values from the first dual ray. Will CHECK fail if there
|
||||
// are no dual rays.
|
||||
// TODO(b/174564572): note the redunancy of the "double" dual and the
|
||||
// inconsistency with `dual_values` in the proto.
|
||||
const LinearConstraintMap<double>& ray_dual_values() const {
|
||||
CHECK(has_dual_ray());
|
||||
return dual_rays[0].dual_values;
|
||||
}
|
||||
|
||||
// The reduced from the first dual ray. Will CHECK fail if there
|
||||
// are no dual rays.
|
||||
const VariableMap<double>& ray_reduced_costs() const {
|
||||
CHECK(has_dual_ray());
|
||||
return dual_rays[0].reduced_costs;
|
||||
}
|
||||
|
||||
// Indicates if at least one basis is available.
|
||||
bool has_basis() const { return !basis.empty(); }
|
||||
|
||||
// The constraint basis status for the first primal/dual pair.
|
||||
const LinearConstraintMap<BasisStatus>& constraint_status() const {
|
||||
CHECK(has_basis());
|
||||
return basis[0].constraint_status;
|
||||
}
|
||||
|
||||
// The variable basis status for the first primal/dual pair.
|
||||
const VariableMap<BasisStatus>& variable_status() const {
|
||||
CHECK(has_basis());
|
||||
return basis[0].variable_status;
|
||||
}
|
||||
|
||||
std::vector<std::string> warnings;
|
||||
SolveResultProto::TerminationReason termination_reason =
|
||||
SolveResultProto::TERMINATION_REASON_UNSPECIFIED;
|
||||
std::string termination_detail;
|
||||
SolveStatsProto solve_stats;
|
||||
|
||||
// Primal solutions should be ordered best objective value first.
|
||||
std::vector<PrimalSolution> primal_solutions;
|
||||
std::vector<PrimalRay> primal_rays;
|
||||
|
||||
// Dual solutions should be ordered best objective value first.
|
||||
std::vector<DualSolution> dual_solutions;
|
||||
std::vector<DualRay> dual_rays;
|
||||
|
||||
// basis[i] corresponds to the primal dual pair:
|
||||
// {primal_solutions[i], dual_solutions[i]}. These fields must have at least
|
||||
// as many elements as basis. Basis will only be populated for LPs, and may
|
||||
// not be populated.
|
||||
std::vector<Basis> basis;
|
||||
|
||||
// Set to true if MathOpt::Solve() has attempted an incremental solve instead
|
||||
// of starting from scratch.
|
||||
//
|
||||
// We have three components involve in Solve(): MathOpt, the solver wrapper
|
||||
// (solver.h) and the actual solver (SCIP, ...). For some model modifications,
|
||||
// the wrapper can support modifying the actual solver's in-memory model
|
||||
// instead of recreating it from scratch. This member is set to true when this
|
||||
// happens.
|
||||
bool attempted_incremental_solve = false;
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_RESULT_H_
|
||||
189
ortools/math_opt/cpp/solution.cc
Normal file
189
ortools/math_opt/cpp/solution.cc
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/solution.h"
|
||||
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
namespace {
|
||||
|
||||
template <typename Key>
|
||||
IdMap<Key, double> ValuesFrom(const ModelStorage* const model,
|
||||
const SparseDoubleVectorProto& vars_proto) {
|
||||
return IdMap<Key, double>(
|
||||
model, MakeView(vars_proto).as_map<typename Key::IdType>());
|
||||
}
|
||||
|
||||
template <typename Key>
|
||||
IdMap<Key, BasisStatus> BasisValues(
|
||||
const ModelStorage* const model,
|
||||
const SparseBasisStatusVector& basis_proto) {
|
||||
absl::flat_hash_map<typename Key::IdType, BasisStatus> id_map;
|
||||
for (const auto& [id, basis_status_proto] : MakeView(basis_proto)) {
|
||||
// CHECK fails on BASIS_STATUS_UNSPECIFIED (the validation code should have
|
||||
// tested that).
|
||||
// We need to cast because the C++ proto API stores repeated enums as ints.
|
||||
//
|
||||
// On top of that iOS 11 does not support .value() on optionals so we must
|
||||
// use operator*.
|
||||
const std::optional<BasisStatus> basis_status =
|
||||
EnumFromProto(static_cast<BasisStatusProto>(basis_status_proto));
|
||||
CHECK(basis_status.has_value());
|
||||
id_map[static_cast<typename Key::IdType>(id)] = *basis_status;
|
||||
}
|
||||
return IdMap<Key, BasisStatus>(model, std::move(id_map));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<absl::string_view> Enum<SolutionStatus>::ToOptString(
|
||||
SolutionStatus value) {
|
||||
switch (value) {
|
||||
case SolutionStatus::kFeasible:
|
||||
return "feasible";
|
||||
case SolutionStatus::kInfeasible:
|
||||
return "infeasible";
|
||||
case SolutionStatus::kUndetermined:
|
||||
return "undetermined";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::Span<const SolutionStatus> Enum<SolutionStatus>::AllValues() {
|
||||
static constexpr SolutionStatus kSolutionStatusValues[] = {
|
||||
SolutionStatus::kFeasible,
|
||||
SolutionStatus::kInfeasible,
|
||||
SolutionStatus::kUndetermined,
|
||||
};
|
||||
return absl::MakeConstSpan(kSolutionStatusValues);
|
||||
}
|
||||
|
||||
std::optional<absl::string_view> Enum<BasisStatus>::ToOptString(
|
||||
BasisStatus value) {
|
||||
switch (value) {
|
||||
case BasisStatus::kFree:
|
||||
return "free";
|
||||
case BasisStatus::kAtLowerBound:
|
||||
return "at_lower_bound";
|
||||
case BasisStatus::kAtUpperBound:
|
||||
return "at_upper_bound";
|
||||
case BasisStatus::kFixedValue:
|
||||
return "fixed_value";
|
||||
case BasisStatus::kBasic:
|
||||
return "basic";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::Span<const BasisStatus> Enum<BasisStatus>::AllValues() {
|
||||
static constexpr BasisStatus kBasisStatusValues[] = {
|
||||
BasisStatus::kFree, BasisStatus::kAtLowerBound,
|
||||
BasisStatus::kAtUpperBound, BasisStatus::kFixedValue,
|
||||
BasisStatus::kBasic,
|
||||
};
|
||||
return absl::MakeConstSpan(kBasisStatusValues);
|
||||
}
|
||||
|
||||
PrimalSolution PrimalSolution::FromProto(
|
||||
const ModelStorage* model,
|
||||
const PrimalSolutionProto& primal_solution_proto) {
|
||||
PrimalSolution primal_solution;
|
||||
primal_solution.variable_values =
|
||||
ValuesFrom<Variable>(model, primal_solution_proto.variable_values());
|
||||
primal_solution.objective_value = primal_solution_proto.objective_value();
|
||||
// TODO(b/209014770): consider adding a function to simplify this pattern.
|
||||
const std::optional<SolutionStatus> feasibility_status =
|
||||
EnumFromProto(primal_solution_proto.feasibility_status());
|
||||
CHECK(feasibility_status.has_value());
|
||||
primal_solution.feasibility_status = *feasibility_status;
|
||||
return primal_solution;
|
||||
}
|
||||
|
||||
PrimalRay PrimalRay::FromProto(const ModelStorage* model,
|
||||
const PrimalRayProto& primal_ray_proto) {
|
||||
return {.variable_values =
|
||||
ValuesFrom<Variable>(model, primal_ray_proto.variable_values())};
|
||||
}
|
||||
|
||||
DualSolution DualSolution::FromProto(
|
||||
const ModelStorage* model, const DualSolutionProto& dual_solution_proto) {
|
||||
DualSolution dual_solution;
|
||||
dual_solution.dual_values =
|
||||
ValuesFrom<LinearConstraint>(model, dual_solution_proto.dual_values());
|
||||
dual_solution.reduced_costs =
|
||||
ValuesFrom<Variable>(model, dual_solution_proto.reduced_costs());
|
||||
if (dual_solution_proto.has_objective_value()) {
|
||||
dual_solution.objective_value = dual_solution_proto.objective_value();
|
||||
}
|
||||
// TODO(b/209014770): consider adding a function to simplify this pattern.
|
||||
const std::optional<SolutionStatus> feasibility_status =
|
||||
EnumFromProto(dual_solution_proto.feasibility_status());
|
||||
CHECK(feasibility_status.has_value());
|
||||
dual_solution.feasibility_status = *feasibility_status;
|
||||
return dual_solution;
|
||||
}
|
||||
|
||||
DualRay DualRay::FromProto(const ModelStorage* model,
|
||||
const DualRayProto& dual_ray_proto) {
|
||||
return {.dual_values =
|
||||
ValuesFrom<LinearConstraint>(model, dual_ray_proto.dual_values()),
|
||||
.reduced_costs =
|
||||
ValuesFrom<Variable>(model, dual_ray_proto.reduced_costs())};
|
||||
}
|
||||
|
||||
Basis Basis::FromProto(const ModelStorage* model,
|
||||
const BasisProto& basis_proto) {
|
||||
Basis basis;
|
||||
basis.constraint_status =
|
||||
BasisValues<LinearConstraint>(model, basis_proto.constraint_status());
|
||||
basis.variable_status =
|
||||
BasisValues<Variable>(model, basis_proto.variable_status());
|
||||
// TODO(b/209014770): consider adding a function to simplify this pattern.
|
||||
const std::optional<SolutionStatus> basic_dual_feasibility =
|
||||
EnumFromProto(basis_proto.basic_dual_feasibility());
|
||||
CHECK(basic_dual_feasibility.has_value());
|
||||
basis.basic_dual_feasibility = *basic_dual_feasibility;
|
||||
return basis;
|
||||
}
|
||||
|
||||
Solution Solution::FromProto(const ModelStorage* model,
|
||||
const SolutionProto& solution_proto) {
|
||||
Solution solution;
|
||||
if (solution_proto.has_primal_solution()) {
|
||||
solution.primal_solution =
|
||||
PrimalSolution::FromProto(model, solution_proto.primal_solution());
|
||||
}
|
||||
if (solution_proto.has_dual_solution()) {
|
||||
solution.dual_solution =
|
||||
DualSolution::FromProto(model, solution_proto.dual_solution());
|
||||
}
|
||||
if (solution_proto.has_basis()) {
|
||||
solution.basis = Basis::FromProto(model, solution_proto.basis());
|
||||
}
|
||||
return solution;
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
232
ortools/math_opt/cpp/solution.h
Normal file
232
ortools/math_opt/cpp/solution.h
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_SOLUTION_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_SOLUTION_H_
|
||||
|
||||
// IWYU pragma: private, include "ortools/math_opt/cpp/math_opt.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/enums.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/linear_constraint.h"
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h"
|
||||
#include "ortools/math_opt/result.pb.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// Feasibility of a primal or dual solution as claimed by the solver.
|
||||
enum class SolutionStatus {
|
||||
// Solver does not claim a feasibility status.
|
||||
kUndetermined = SOLUTION_STATUS_UNDETERMINED,
|
||||
|
||||
// Solver claims the solution is feasible.
|
||||
kFeasible = SOLUTION_STATUS_FEASIBLE,
|
||||
|
||||
// Solver claims the solution is infeasible.
|
||||
kInfeasible = SOLUTION_STATUS_INFEASIBLE,
|
||||
};
|
||||
|
||||
MATH_OPT_DEFINE_ENUM(SolutionStatus, SOLUTION_STATUS_UNSPECIFIED);
|
||||
|
||||
// Status of a variable/constraint in a LP basis.
|
||||
enum class BasisStatus : int8_t {
|
||||
// The variable/constraint is free (it has no finite bounds).
|
||||
kFree = BASIS_STATUS_FREE,
|
||||
|
||||
// The variable/constraint is at its lower bound (which must be finite).
|
||||
kAtLowerBound = BASIS_STATUS_AT_LOWER_BOUND,
|
||||
|
||||
// The variable/constraint is at its upper bound (which must be finite).
|
||||
kAtUpperBound = BASIS_STATUS_AT_UPPER_BOUND,
|
||||
|
||||
// The variable/constraint has identical finite lower and upper bounds.
|
||||
kFixedValue = BASIS_STATUS_FIXED_VALUE,
|
||||
|
||||
// The variable/constraint is basic.
|
||||
kBasic = BASIS_STATUS_BASIC,
|
||||
};
|
||||
|
||||
MATH_OPT_DEFINE_ENUM(BasisStatus, BASIS_STATUS_UNSPECIFIED);
|
||||
|
||||
// A solution to an optimization problem.
|
||||
//
|
||||
// E.g. consider a simple linear program:
|
||||
// min c * x
|
||||
// s.t. A * x >= b
|
||||
// x >= 0.
|
||||
// A primal solution is assignment values to x. It is feasible if it satisfies
|
||||
// A * x >= b and x >= 0 from above. In the class PrimalSolution,
|
||||
// variable_values is x and objective_value is c * x.
|
||||
//
|
||||
// For the general case of a MathOpt optimization model, see
|
||||
// go/mathopt-solutions for details.
|
||||
struct PrimalSolution {
|
||||
static PrimalSolution FromProto(
|
||||
const ModelStorage* model,
|
||||
const PrimalSolutionProto& primal_solution_proto);
|
||||
|
||||
VariableMap<double> variable_values;
|
||||
double objective_value = 0.0;
|
||||
|
||||
SolutionStatus feasibility_status = SolutionStatus::kUndetermined;
|
||||
};
|
||||
|
||||
// A direction of unbounded improvement to an optimization problem;
|
||||
// equivalently, a certificate of infeasibility for the dual of the
|
||||
// optimization problem.
|
||||
//
|
||||
// E.g. consider a simple linear program:
|
||||
// min c * x
|
||||
// s.t. A * x >= b
|
||||
// x >= 0
|
||||
// A primal ray is an x that satisfies:
|
||||
// c * x < 0
|
||||
// A * x >= 0
|
||||
// x >= 0
|
||||
// Observe that given a feasible solution, any positive multiple of the primal
|
||||
// ray plus that solution is still feasible, and gives a better objective
|
||||
// value. A primal ray also proves the dual optimization problem infeasible.
|
||||
//
|
||||
// In the class PrimalRay, variable_values is this x.
|
||||
//
|
||||
// For the general case of a MathOpt optimization model, see
|
||||
// go/mathopt-solutions for details.
|
||||
struct PrimalRay {
|
||||
static PrimalRay FromProto(const ModelStorage* model,
|
||||
const PrimalRayProto& primal_ray_proto);
|
||||
|
||||
VariableMap<double> variable_values;
|
||||
};
|
||||
|
||||
// A solution to the dual of an optimization problem.
|
||||
//
|
||||
// E.g. consider the primal dual pair linear program pair:
|
||||
// (Primal) (Dual)
|
||||
// min c * x max b * y
|
||||
// s.t. A * x >= b s.t. y * A + r = c
|
||||
// x >= 0 y, r >= 0.
|
||||
// The dual solution is the pair (y, r). It is feasible if it satisfies the
|
||||
// constraints from (Dual) above.
|
||||
//
|
||||
// Below, y is dual_values, r is reduced_costs, and b * y is objective value.
|
||||
//
|
||||
// For the general case, see go/mathopt-solutions and go/mathopt-dual (and
|
||||
// note that the dual objective depends on r in the general case).
|
||||
struct DualSolution {
|
||||
static DualSolution FromProto(const ModelStorage* model,
|
||||
const DualSolutionProto& dual_solution_proto);
|
||||
|
||||
LinearConstraintMap<double> dual_values;
|
||||
VariableMap<double> reduced_costs;
|
||||
std::optional<double> objective_value;
|
||||
|
||||
SolutionStatus feasibility_status = SolutionStatus::kUndetermined;
|
||||
};
|
||||
|
||||
// A direction of unbounded improvement to the dual of an optimization,
|
||||
// problem; equivalently, a certificate of primal infeasibility.
|
||||
//
|
||||
// E.g. consider the primal dual pair linear program pair:
|
||||
// (Primal) (Dual)
|
||||
// min c * x max b * y
|
||||
// s.t. A * x >= b s.t. y * A + r = c
|
||||
// x >= 0 y, r >= 0.
|
||||
// The dual ray is the pair (y, r) satisfying:
|
||||
// b * y > 0
|
||||
// y * A + r = 0
|
||||
// y, r >= 0
|
||||
// Observe that adding a positive multiple of (y, r) to dual feasible solution
|
||||
// maintains dual feasibility and improves the objective (proving the dual is
|
||||
// unbounded). The dual ray also proves the primal problem is infeasible.
|
||||
//
|
||||
// In the class DualRay, y is dual_values and r is reduced_costs.
|
||||
//
|
||||
// For the general case, see go/mathopt-solutions and go/mathopt-dual (and
|
||||
// note that the dual objective depends on r in the general case).
|
||||
struct DualRay {
|
||||
static DualRay FromProto(const ModelStorage* model,
|
||||
const DualRayProto& dual_ray_proto);
|
||||
|
||||
LinearConstraintMap<double> dual_values;
|
||||
VariableMap<double> reduced_costs;
|
||||
};
|
||||
|
||||
// A combinatorial characterization for a solution to a linear program.
|
||||
//
|
||||
// The simplex method for solving linear programs always returns a "basic
|
||||
// feasible solution" which can be described combinatorially as a Basis. A
|
||||
// basis assigns a BasisStatus for every variable and linear constraint.
|
||||
//
|
||||
// E.g. consider a standard form LP:
|
||||
// min c * x
|
||||
// s.t. A * x = b
|
||||
// x >= 0
|
||||
// that has more variables than constraints and with full row rank A.
|
||||
//
|
||||
// Let n be the number of variables and m the number of linear constraints. A
|
||||
// valid basis for this problem can be constructed as follows:
|
||||
// * All constraints will have basis status FIXED.
|
||||
// * Pick m variables such that the columns of A are linearly independent and
|
||||
// assign the status BASIC.
|
||||
// * Assign the status AT_LOWER for the remaining n - m variables.
|
||||
//
|
||||
// The basic solution for this basis is the unique solution of A * x = b that
|
||||
// has all variables with status AT_LOWER fixed to their lower bounds (all
|
||||
// zero). The resulting solution is called a basic feasible solution if it
|
||||
// also satisfies x >= 0.
|
||||
//
|
||||
// See go/mathopt-basis for treatment of the general case and an explanation
|
||||
// of how a dual solution is determined for a basis.
|
||||
struct Basis {
|
||||
// Returns a Basis built from the input indexed_basis, CHECKing that no
|
||||
// values is BASIS_STATUS_UNSPECIFIED. No check is done on other values so
|
||||
// out of bounds values e.g. BasisStatusProto_MAX+1 won't raise an
|
||||
// assertion. See SpaseBasisStatusVectorIsValid().
|
||||
static Basis FromProto(const ModelStorage* model,
|
||||
const BasisProto& basis_proto);
|
||||
|
||||
LinearConstraintMap<BasisStatus> constraint_status;
|
||||
VariableMap<BasisStatus> variable_status;
|
||||
|
||||
// This is an advanced status. For single-sided LPs it should be equal to the
|
||||
// feasibility status of the associated dual solution. For two-sided LPs it
|
||||
// may be different in some edge cases (e.g. incomplete solves with primal
|
||||
// simplex). For more details see go/mathopt-basis-advanced#dualfeasibility.
|
||||
SolutionStatus basic_dual_feasibility = SolutionStatus::kUndetermined;
|
||||
};
|
||||
|
||||
// What is included in a solution depends on the kind of problem and solver.
|
||||
// The current common patterns are
|
||||
// 1. MIP solvers return only a primal solution.
|
||||
// 2. Simplex LP solvers often return a basis and the primal and dual
|
||||
// solutions associated to this basis.
|
||||
// 3. Other continuous solvers often return a primal and dual solution
|
||||
// solution that are connected in a solver-dependent form.
|
||||
struct Solution {
|
||||
static Solution FromProto(const ModelStorage* model,
|
||||
const SolutionProto& solution_proto);
|
||||
std::optional<PrimalSolution> primal_solution;
|
||||
std::optional<DualSolution> dual_solution;
|
||||
std::optional<Basis> basis;
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_SOLUTION_H_
|
||||
250
ortools/math_opt/cpp/solve.cc
Normal file
250
ortools/math_opt/cpp/solve.cc
Normal file
@@ -0,0 +1,250 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/solve.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/base/thread_annotations.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "ortools/base/source_location.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/core/solver.h"
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
#include "ortools/math_opt/cpp/model.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
namespace {
|
||||
|
||||
Solver::InitArgs ToSolverInitArgs(const SolverInitArguments& arguments) {
|
||||
Solver::InitArgs solver_init_args;
|
||||
solver_init_args.streamable = arguments.streamable.Proto();
|
||||
if (arguments.non_streamable != nullptr) {
|
||||
solver_init_args.non_streamable = arguments.non_streamable.get();
|
||||
}
|
||||
|
||||
return solver_init_args;
|
||||
}
|
||||
|
||||
// Asserts (with CHECK) that the input pointer is either nullptr or that it
|
||||
// points to the same model storage as storage_.
|
||||
void CheckModelStorage(const ModelStorage* const storage,
|
||||
const ModelStorage* const expected_storage) {
|
||||
if (storage != nullptr) {
|
||||
CHECK_EQ(storage, expected_storage)
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
}
|
||||
|
||||
absl::StatusOr<SolveResult> CallSolve(
|
||||
Solver& solver, const ModelStorage* const expected_storage,
|
||||
const SolveArguments& arguments) {
|
||||
CheckModelStorage(/*storage=*/arguments.model_parameters.storage(),
|
||||
/*expected_storage=*/expected_storage);
|
||||
CheckModelStorage(/*storage=*/arguments.callback_registration.storage(),
|
||||
/*expected_storage=*/expected_storage);
|
||||
|
||||
if (arguments.callback == nullptr) {
|
||||
CHECK(arguments.callback_registration.events.empty())
|
||||
<< "No callback was provided to run, but callback events were "
|
||||
"registered.";
|
||||
}
|
||||
|
||||
Solver::Callback cb = nullptr;
|
||||
if (arguments.callback != nullptr) {
|
||||
cb = [&](const CallbackDataProto& callback_data_proto) {
|
||||
const CallbackData data(expected_storage, callback_data_proto);
|
||||
const CallbackResult result = arguments.callback(data);
|
||||
CheckModelStorage(/*storage=*/result.storage(),
|
||||
/*expected_storage=*/expected_storage);
|
||||
return result.Proto();
|
||||
};
|
||||
}
|
||||
ASSIGN_OR_RETURN(
|
||||
SolveResultProto solve_result,
|
||||
solver.Solve(
|
||||
{.parameters = arguments.parameters.Proto(),
|
||||
.model_parameters = arguments.model_parameters.Proto(),
|
||||
.message_callback = arguments.message_callback,
|
||||
.callback_registration = arguments.callback_registration.Proto(),
|
||||
.user_cb = std::move(cb),
|
||||
.interrupter = arguments.interrupter}));
|
||||
return SolveResult::FromProto(expected_storage, solve_result);
|
||||
}
|
||||
|
||||
class PrinterMessageCallbackImpl {
|
||||
public:
|
||||
PrinterMessageCallbackImpl(std::ostream& output_stream,
|
||||
const absl::string_view prefix)
|
||||
: output_stream_(output_stream), prefix_(prefix) {}
|
||||
|
||||
void Call(const std::vector<std::string>& messages) {
|
||||
const absl::MutexLock lock(&mutex_);
|
||||
for (const std::string& message : messages) {
|
||||
output_stream_ << prefix_ << message << '\n';
|
||||
}
|
||||
output_stream_.flush();
|
||||
}
|
||||
|
||||
private:
|
||||
absl::Mutex mutex_;
|
||||
std::ostream& output_stream_ ABSL_GUARDED_BY(mutex_);
|
||||
const std::string prefix_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
SolverInitArguments::SolverInitArguments(
|
||||
StreamableSolverInitArguments streamable)
|
||||
: streamable(std::move(streamable)) {}
|
||||
|
||||
SolverInitArguments::SolverInitArguments(
|
||||
const NonStreamableSolverInitArguments& non_streamable)
|
||||
: non_streamable(non_streamable.Clone()) {}
|
||||
|
||||
SolverInitArguments::SolverInitArguments(
|
||||
StreamableSolverInitArguments streamable,
|
||||
const NonStreamableSolverInitArguments& non_streamable)
|
||||
: streamable(std::move(streamable)),
|
||||
non_streamable(non_streamable.Clone()) {}
|
||||
|
||||
SolverInitArguments::SolverInitArguments(const SolverInitArguments& other)
|
||||
: streamable(other.streamable),
|
||||
non_streamable(other.non_streamable != nullptr
|
||||
? other.non_streamable->Clone()
|
||||
: nullptr) {}
|
||||
|
||||
SolverInitArguments& SolverInitArguments::operator=(
|
||||
const SolverInitArguments& other) {
|
||||
// Assignment to self is possible.
|
||||
if (&other == this) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
streamable = other.streamable;
|
||||
non_streamable =
|
||||
other.non_streamable != nullptr ? other.non_streamable->Clone() : nullptr;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
absl::StatusOr<SolveResult> Solve(const Model& model,
|
||||
const SolverType solver_type,
|
||||
const SolveArguments& solve_args,
|
||||
const SolverInitArguments& init_args) {
|
||||
ASSIGN_OR_RETURN(const std::unique_ptr<Solver> solver,
|
||||
Solver::New(EnumToProto(solver_type), model.ExportModel(),
|
||||
ToSolverInitArgs(init_args)));
|
||||
return CallSolve(*solver, model.storage(), solve_args);
|
||||
}
|
||||
|
||||
absl::StatusOr<std::unique_ptr<IncrementalSolver>> IncrementalSolver::New(
|
||||
Model& model, const SolverType solver_type, SolverInitArguments arguments) {
|
||||
std::unique_ptr<UpdateTracker> update_tracker = model.NewUpdateTracker();
|
||||
ASSIGN_OR_RETURN(
|
||||
std::unique_ptr<Solver> solver,
|
||||
Solver::New(EnumToProto(solver_type), update_tracker->ExportModel(),
|
||||
ToSolverInitArgs(arguments)));
|
||||
return absl::WrapUnique<IncrementalSolver>(
|
||||
new IncrementalSolver(solver_type, std::move(arguments), model.storage(),
|
||||
std::move(update_tracker), std::move(solver)));
|
||||
}
|
||||
|
||||
IncrementalSolver::IncrementalSolver(
|
||||
SolverType solver_type, SolverInitArguments init_args,
|
||||
const ModelStorage* const expected_storage,
|
||||
std::unique_ptr<UpdateTracker> update_tracker,
|
||||
std::unique_ptr<Solver> solver)
|
||||
: solver_type_(solver_type),
|
||||
init_args_(std::move(init_args)),
|
||||
expected_storage_(expected_storage),
|
||||
update_tracker_(std::move(update_tracker)),
|
||||
solver_(std::move(solver)) {}
|
||||
|
||||
absl::StatusOr<SolveResult> IncrementalSolver::Solve(
|
||||
const SolveArguments& arguments) {
|
||||
RETURN_IF_ERROR(Update().status());
|
||||
return SolveWithoutUpdate(arguments);
|
||||
}
|
||||
|
||||
absl::StatusOr<IncrementalSolver::UpdateResult> IncrementalSolver::Update() {
|
||||
std::optional<ModelUpdateProto> model_update =
|
||||
update_tracker_->ExportModelUpdate();
|
||||
if (!model_update) {
|
||||
return UpdateResult(true, std::move(model_update));
|
||||
}
|
||||
|
||||
ASSIGN_OR_RETURN(const bool did_update, solver_->Update(*model_update));
|
||||
update_tracker_->Checkpoint();
|
||||
|
||||
if (did_update) {
|
||||
return UpdateResult(true, std::move(model_update));
|
||||
}
|
||||
|
||||
ASSIGN_OR_RETURN(solver_, Solver::New(EnumToProto(solver_type_),
|
||||
update_tracker_->ExportModel(),
|
||||
ToSolverInitArgs(init_args_)));
|
||||
|
||||
return UpdateResult(false, std::move(model_update));
|
||||
}
|
||||
|
||||
absl::StatusOr<SolveResult> IncrementalSolver::SolveWithoutUpdate(
|
||||
const SolveArguments& arguments) const {
|
||||
return CallSolve(*solver_, expected_storage_, arguments);
|
||||
}
|
||||
|
||||
MessageCallback PrinterMessageCallback(std::ostream& output_stream,
|
||||
const absl::string_view prefix) {
|
||||
// Here we must use an std::shared_ptr since std::function requires that its
|
||||
// input is copyable. And PrinterMessageCallbackImpl can't be copyable since
|
||||
// it uses an absl::Mutex that is not.
|
||||
const auto impl =
|
||||
std::make_shared<PrinterMessageCallbackImpl>(output_stream, prefix);
|
||||
return
|
||||
[=](const std::vector<std::string>& messages) { impl->Call(messages); };
|
||||
}
|
||||
|
||||
MessageCallback InfoLoggerMessageCallback(const absl::string_view prefix,
|
||||
const absl::SourceLocation loc) {
|
||||
return [=](const std::vector<std::string>& messages) {
|
||||
for (const std::string& message : messages) {
|
||||
LOG(INFO).AtLocation(loc) << prefix << message;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
MessageCallback VLoggerMessageCallback(int level, absl::string_view prefix,
|
||||
absl::SourceLocation loc) {
|
||||
return [=](const std::vector<std::string>& messages) {
|
||||
for (const std::string& message : messages) {
|
||||
VLOG(level).AtLocation(loc) << prefix << message;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
393
ortools/math_opt/cpp/solve.h
Normal file
393
ortools/math_opt/cpp/solve.h
Normal file
@@ -0,0 +1,393 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Functions and classes used to solve a Model.
|
||||
//
|
||||
// The main entry point is the Solve() function.
|
||||
//
|
||||
// For users that need incremental solving, there is the IncrementalSolver
|
||||
// class.
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_SOLVE_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_SOLVE_H_
|
||||
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "ortools/base/source_location.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/core/non_streamable_solver_init_arguments.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/core/solve_interrupter.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/core/solver.h"
|
||||
#include "ortools/math_opt/cpp/callback.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/model.h"
|
||||
#include "ortools/math_opt/cpp/model_solve_parameters.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/parameters.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/solve_result.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/streamable_solver_init_arguments.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/parameters.pb.h" // IWYU pragma: export
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// Callback function for messages callback sent by the solver.
|
||||
//
|
||||
// Each message represents a single output line from the solver, and each
|
||||
// message does not contain any '\n' character in it.
|
||||
//
|
||||
// Thread-safety: a callback may be called concurrently from multiple
|
||||
// threads. The users is expected to use proper synchronization primitives to
|
||||
// deal with that.
|
||||
using MessageCallback = std::function<void(const std::vector<std::string>&)>;
|
||||
|
||||
// Returns a message callback function that prints its output to the given
|
||||
// output stream, prefixing each line with the given prefix.
|
||||
//
|
||||
// For each call to the returned message callback, the output_stream is flushed.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// SolveArguments args;
|
||||
// args.message_callback = PrinterMessageCallback(std::cerr, "solver logs> ");
|
||||
MessageCallback PrinterMessageCallback(std::ostream& output_stream = std::cout,
|
||||
absl::string_view prefix = "");
|
||||
|
||||
// Returns a message callback function that prints each line to LOG(INFO),
|
||||
// prefixing each line with the given prefix.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// SolveArguments args;
|
||||
// args.message_callback = InfoLoggerMessageCallback("[solver] ");
|
||||
MessageCallback InfoLoggerMessageCallback(
|
||||
absl::string_view prefix = "",
|
||||
absl::SourceLocation loc = absl::SourceLocation::current());
|
||||
|
||||
// Returns a message callback function that prints each line to VLOG(level),
|
||||
// prefixing each line with the given prefix.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// SolveArguments args;
|
||||
// args.message_callback = VLoggerMessageCallback(1, "[solver] ");
|
||||
MessageCallback VLoggerMessageCallback(
|
||||
int level, absl::string_view prefix = "",
|
||||
absl::SourceLocation loc = absl::SourceLocation::current());
|
||||
|
||||
// Arguments passed to Solve() and IncrementalSolver::New() to control the
|
||||
// instantiation of the solver.
|
||||
//
|
||||
// For convenience, constructors with streamable or/and non-streamable arguments
|
||||
// are provided. The non-streamable arguments are cloned so any change made
|
||||
// after passing them to this class are ignored.
|
||||
//
|
||||
// Usage with streamable arguments:
|
||||
//
|
||||
// Solve(model, SOLVER_TYPE_GUROBI, /*solver_args=*/{},
|
||||
// SolverInitArguments({
|
||||
// .gurobi = StreamableGurobiInitArguments{
|
||||
// .isv_key = GurobiISVKey{
|
||||
// .name = "some name",
|
||||
// .application_name = "some app name",
|
||||
// .expiration = -1,
|
||||
// .key = "random",
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// Usage with non-streamable arguments:
|
||||
//
|
||||
// NonStreamableGurobiInitArguments gurobi_args;
|
||||
// gurobi_args.master_env = master_env.get();
|
||||
//
|
||||
// Solve(model, SOLVER_TYPE_GUROBI, /*solver_args=*/{},
|
||||
// SolverInitArguments(gurobi_args));
|
||||
//
|
||||
struct SolverInitArguments {
|
||||
SolverInitArguments() = default;
|
||||
|
||||
// Initializes this class with a copy of the provided streamable arguments.
|
||||
explicit SolverInitArguments(StreamableSolverInitArguments streamable);
|
||||
|
||||
// Initializes this class with a clone of the provided non-streamable
|
||||
// arguments.
|
||||
//
|
||||
// Note that since this constructors calls Clone() to initialize the
|
||||
// non_streamable_solver_init_arguments field, changes made after calling it
|
||||
// to the input non_streamable are ignored.
|
||||
explicit SolverInitArguments(
|
||||
const NonStreamableSolverInitArguments& non_streamable);
|
||||
|
||||
// Initializes this class with both the provided a copy streamable arguments
|
||||
// and a clone of the non-streamable ones.
|
||||
SolverInitArguments(StreamableSolverInitArguments streamable,
|
||||
const NonStreamableSolverInitArguments& non_streamable);
|
||||
|
||||
// Initializes this class as a copy of the provided arguments. The
|
||||
// non_streamable field is cloned if not nullptr.
|
||||
SolverInitArguments(const SolverInitArguments& other);
|
||||
|
||||
// Sets this class as a copy of the provided arguments. The non_streamable
|
||||
// field is cloned if not nullptr.
|
||||
SolverInitArguments& operator=(const SolverInitArguments& other);
|
||||
|
||||
SolverInitArguments(SolverInitArguments&&) = default;
|
||||
SolverInitArguments& operator=(SolverInitArguments&&) = default;
|
||||
|
||||
StreamableSolverInitArguments streamable;
|
||||
|
||||
// This should either be the solver specific class or nullptr.
|
||||
//
|
||||
// Solvers will fail (by returning an absl::Status) if called with arguments
|
||||
// for another solver.
|
||||
std::unique_ptr<const NonStreamableSolverInitArguments> non_streamable;
|
||||
};
|
||||
|
||||
// Arguments passed to Solve() and IncrementalSolver::Solve() to control the
|
||||
// solve.
|
||||
struct SolveArguments {
|
||||
// Model independent parameters, e.g. time limit.
|
||||
SolveParameters parameters;
|
||||
|
||||
// Model dependent parameters, e.g. solution hint.
|
||||
ModelSolveParameters model_parameters;
|
||||
|
||||
// An optional callback for messages emitted by the solver.
|
||||
//
|
||||
// When set it enables the solver messages and ignores the `enable_output` in
|
||||
// solve parameters; messages are redirected to the callback and not printed
|
||||
// on stdout/stderr/logs anymore.
|
||||
//
|
||||
// See PrinterMessageCallback() for logging to stdout/stderr.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // To print messages to stdout with a prefix.
|
||||
// ASSIGN_OR_RETURN(
|
||||
// const SolveResult result,
|
||||
// Solve(model, SOLVER_TYPE_GLOP,
|
||||
// { .message_callback = PrinterMessageCallback(std::cout,
|
||||
// "logs| "); });
|
||||
//
|
||||
// // To print messages to the INFO log.
|
||||
// ASSIGN_OR_RETURN(
|
||||
// const SolveResult result,
|
||||
// Solve(model, SOLVER_TYPE_GLOP,
|
||||
// { .message_callback = InfoLoggerMessageCallback("[solver] "); });
|
||||
//
|
||||
// // To print messages to the VLOG(1) log.
|
||||
// ASSIGN_OR_RETURN(
|
||||
// const SolveResult result,
|
||||
// Solve(model, SOLVER_TYPE_GLOP,
|
||||
// { .message_callback = VLoggerMessageCallback(1, "[solver] "); });
|
||||
//
|
||||
MessageCallback message_callback = nullptr;
|
||||
|
||||
// Callback registration parameters. Usually `callback` should also be set
|
||||
// when these parameters are modified.
|
||||
CallbackRegistration callback_registration;
|
||||
|
||||
// The callback. The `callback_registration` parameters have to be set, in
|
||||
// particular `callback_registration.events`.
|
||||
Callback callback = nullptr;
|
||||
|
||||
// An optional interrupter that the solver can use to interrupt the solve
|
||||
// early.
|
||||
//
|
||||
// Usage:
|
||||
// auto interrupter = std::make_shared<SolveInterrupter>();
|
||||
//
|
||||
// // Use another thread to trigger the interrupter.
|
||||
// RunInOtherThread([interrupter](){
|
||||
// ... wait for something that should interrupt the solve ...
|
||||
// interrupter->Interrupt();
|
||||
// });
|
||||
//
|
||||
// ASSIGN_OR_RETURN(const SolveResult result,
|
||||
// Solve(model, SOLVER_TYPE_GLOP,
|
||||
// { .interrupter = interrupter.get() });
|
||||
//
|
||||
SolveInterrupter* interrupter = nullptr;
|
||||
};
|
||||
|
||||
// Solves the input model.
|
||||
//
|
||||
// A Status error will be returned if there is an unexpected failure in an
|
||||
// underlying solver or for some internal math_opt errors. Otherwise, check
|
||||
// SolveResult::termination.reason to see if an optimal solution was found.
|
||||
//
|
||||
// Memory model: the returned SolveResult owns its own memory (for solutions,
|
||||
// solve stats, etc.), EXPECT for a pointer back to the model. As a result:
|
||||
// * Keep the model alive to access SolveResult,
|
||||
// * Avoid unnecessarily copying SolveResult,
|
||||
// * The result is generally accessible after mutating the model, but some care
|
||||
// is needed if variables or linear constraints are added or deleted.
|
||||
//
|
||||
// Asserts (using CHECK) that the inputs solve_args.model_parameters and
|
||||
// solve_args.callback_registration only contain variables and constraints from
|
||||
// the input model.
|
||||
//
|
||||
// See callback.h for documentation on solve_args.callback and
|
||||
// solve_args.callback_registration.
|
||||
//
|
||||
// Thread-safety: this method is safe to call concurrently on the same Model.
|
||||
//
|
||||
// Some solvers may add more restrictions regarding threading. Please see
|
||||
// SolverType::kXxx documentation for details.
|
||||
absl::StatusOr<SolveResult> Solve(const Model& model, SolverType solver_type,
|
||||
const SolveArguments& solve_args = {},
|
||||
const SolverInitArguments& init_args = {});
|
||||
|
||||
// Incremental solve of a model.
|
||||
//
|
||||
// This is a feature for advance users. Most users should only use the Solve()
|
||||
// function above.
|
||||
//
|
||||
// Here incremental means that the we try to reuse the existing underlying
|
||||
// solver internals between each solve. There is no guarantee though that the
|
||||
// solver supports all possible model changes. Hence there is not guarantee that
|
||||
// performances will be improved when using this class; this is solver
|
||||
// dependent. Typically LPs have more to gain from incremental solve than
|
||||
// MIPs. In both cases, even if the solver supports the model changes,
|
||||
// incremental solve may actually be slower.
|
||||
//
|
||||
// The New() function instantiates the solver and setup it from the current
|
||||
// state of the Model. Calling Solve() will update the underlying solver with
|
||||
// latest model changes and solve this model.
|
||||
//
|
||||
// Usage:
|
||||
// Model model = ...;
|
||||
// ASSIGN_OR_RETURN(
|
||||
// const std::unique_ptr<IncrementalSolver> incremental_solve,
|
||||
// IncrementalSolver::New(model, SOLVER_TYPE_XXX));
|
||||
//
|
||||
// ASSIGN_OR_RETURN(const SolveResult result1, incremental_solve->Solve());
|
||||
//
|
||||
// model.AddVariable(...);
|
||||
// ...
|
||||
//
|
||||
// ASSIGN_OR_RETURN(const SolveResult result2, incremental_solve->Solve());
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// Thread-safety: The New(), Solve() and Update() methods must not be called
|
||||
// while modifying the Model() (adding variables...). The user is expected to
|
||||
// use proper synchronization primitives to serialize changes to the model and
|
||||
// the use of this object. Note though that it is safe to call methods from
|
||||
// different IncrementalSolver instances on the same Model concurrently.
|
||||
//
|
||||
// There is no problem calling SolveWithoutUpdate() concurrently on different
|
||||
// instances of IncrementalSolver or while the model is being modified (unless
|
||||
// of course the underlying solver itself is not thread-safe and can only be
|
||||
// called from a single-thread).
|
||||
//
|
||||
// Note that Solve(), Update() and SolveWithoutUpdate() are not reentrant so
|
||||
// they should not be called concurrently on the same instance of
|
||||
// IncrementalSolver.
|
||||
//
|
||||
// Some solvers may add more restrictions regarding threading. Please see
|
||||
// SolverType::kXxx documentation for details.
|
||||
class IncrementalSolver {
|
||||
public:
|
||||
struct UpdateResult {
|
||||
UpdateResult(const bool did_update, std::optional<ModelUpdateProto> update)
|
||||
: did_update(did_update), update(std::move(update)) {}
|
||||
|
||||
// True if the solver has been successfully updated or if no update was
|
||||
// necessary (in which case `update` will be nullopt). False if the solver
|
||||
// had to be recreated.
|
||||
bool did_update;
|
||||
|
||||
// The update that was attempted on the solver. Can be nullopt when no
|
||||
// update was needed (the model was not changed).
|
||||
std::optional<ModelUpdateProto> update;
|
||||
};
|
||||
|
||||
// Creates a new incremental solve for the given model. It may returns an
|
||||
// error if the parameters are invalid (for example if the selected solver is
|
||||
// not linked in the binary).
|
||||
//
|
||||
// The returned IncrementalSolver keeps a copy of `arguments`. Thus the
|
||||
// content of arguments.non_streamable (for example pointers to solver
|
||||
// specific struct) must be valid until the destruction of the
|
||||
// IncrementalSolver.
|
||||
static absl::StatusOr<std::unique_ptr<IncrementalSolver>> New(
|
||||
Model& model, SolverType solver_type, SolverInitArguments arguments = {});
|
||||
|
||||
// Updates the underlying solver with latest model changes and runs the solve.
|
||||
//
|
||||
// A Status error will be returned if there is an unexpected failure in an
|
||||
// underlying solver or for some internal math_opt errors. Otherwise, check
|
||||
// SolveResult::termination.reason to see if an optimal solution was found.
|
||||
//
|
||||
// Memory model: the returned SolveResult owns its own memory (for solutions,
|
||||
// solve stats, etc.), EXPECT for a pointer back to the model. As a result:
|
||||
// * Keep the model alive to access SolveResult,
|
||||
// * Avoid unnecessarily copying SolveResult,
|
||||
// * The result is generally accessible after mutating this, but some care
|
||||
// is needed if variables or linear constraints are added or deleted.
|
||||
//
|
||||
// Asserts (using CHECK) that the inputs arguments.model_parameters and
|
||||
// arguments.callback_registration only contain variables and constraints from
|
||||
// the input model.
|
||||
//
|
||||
// See callback.h for documentation on arguments.callback and
|
||||
// arguments.callback_registration.
|
||||
absl::StatusOr<SolveResult> Solve(const SolveArguments& arguments = {});
|
||||
|
||||
// Updates the model to solve.
|
||||
//
|
||||
// This is an advanced API, most users should use Solve() above that does the
|
||||
// update and before calling the solver. Calling this function is only useful
|
||||
// for users that want to access to update data or users that need to use
|
||||
// SolveWithoutUpdate() (which should not be common).
|
||||
//
|
||||
// The returned value indicates if the update was possible or if the solver
|
||||
// had to be recreated from scratch (which may happen when the solver does not
|
||||
// support this specific update or any update at all). It also contains the
|
||||
// attempted update data.
|
||||
//
|
||||
// A status error will be returned if the underlying solver has an internal
|
||||
// error.
|
||||
absl::StatusOr<UpdateResult> Update();
|
||||
|
||||
// Same as Solve() but does not update the underlying solver with the latest
|
||||
// changes to the model.
|
||||
//
|
||||
// This is an advanced API, most users should use Solve().
|
||||
absl::StatusOr<SolveResult> SolveWithoutUpdate(
|
||||
const SolveArguments& arguments = {}) const;
|
||||
|
||||
private:
|
||||
IncrementalSolver(SolverType solver_type, SolverInitArguments init_args,
|
||||
const ModelStorage* expected_storage,
|
||||
std::unique_ptr<UpdateTracker> update_tracker,
|
||||
std::unique_ptr<Solver> solver);
|
||||
|
||||
const SolverType solver_type_;
|
||||
const SolverInitArguments init_args_;
|
||||
const ModelStorage* const expected_storage_;
|
||||
const std::unique_ptr<UpdateTracker> update_tracker_;
|
||||
std::unique_ptr<Solver> solver_;
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_SOLVE_H_
|
||||
376
ortools/math_opt/cpp/solve_result.cc
Normal file
376
ortools/math_opt/cpp/solve_result.cc
Normal file
@@ -0,0 +1,376 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/solve_result.h"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
#include "ortools/port/proto_utils.h"
|
||||
#include "absl/status/status.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
namespace {
|
||||
|
||||
// Converts a map with BasisStatusProto values to a map with BasisStatus values
|
||||
// CHECKing that no values are BASIS_STATUS_UNSPECIFIED (the validation code
|
||||
// should have tested that).
|
||||
//
|
||||
// TODO(b/201344491): use FromProto() factory methods on solution members and
|
||||
// remove the need for this conversion from `IndexedSolutions`.
|
||||
template <typename TypedIndex>
|
||||
absl::flat_hash_map<TypedIndex, BasisStatus> BasisStatusMapFromProto(
|
||||
const absl::flat_hash_map<TypedIndex, BasisStatusProto>& proto_map) {
|
||||
absl::flat_hash_map<TypedIndex, BasisStatus> cpp_map;
|
||||
for (const auto& [id, proto_value] : proto_map) {
|
||||
const std::optional<BasisStatus> opt_status = EnumFromProto(proto_value);
|
||||
CHECK(opt_status.has_value());
|
||||
cpp_map.emplace(id, *opt_status);
|
||||
}
|
||||
return cpp_map;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<absl::string_view> Enum<FeasibilityStatus>::ToOptString(
|
||||
FeasibilityStatus value) {
|
||||
switch (value) {
|
||||
case FeasibilityStatus::kUndetermined:
|
||||
return "undetermined";
|
||||
case FeasibilityStatus::kFeasible:
|
||||
return "feasible";
|
||||
case FeasibilityStatus::kInfeasible:
|
||||
return "infeasible";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::Span<const FeasibilityStatus> Enum<FeasibilityStatus>::AllValues() {
|
||||
static constexpr FeasibilityStatus kFeasibilityStatus[] = {
|
||||
FeasibilityStatus::kUndetermined,
|
||||
FeasibilityStatus::kFeasible,
|
||||
FeasibilityStatus::kInfeasible,
|
||||
};
|
||||
return absl::MakeConstSpan(kFeasibilityStatus);
|
||||
}
|
||||
|
||||
std::optional<absl::string_view> Enum<TerminationReason>::ToOptString(
|
||||
TerminationReason value) {
|
||||
switch (value) {
|
||||
case TerminationReason::kOptimal:
|
||||
return "optimal";
|
||||
case TerminationReason::kInfeasible:
|
||||
return "infeasible";
|
||||
case TerminationReason::kUnbounded:
|
||||
return "unbounded";
|
||||
case TerminationReason::kInfeasibleOrUnbounded:
|
||||
return "infeasible_or_unbounded";
|
||||
case TerminationReason::kImprecise:
|
||||
return "imprecise";
|
||||
case TerminationReason::kLimitReached:
|
||||
return "limit_reached";
|
||||
case TerminationReason::kNumericalError:
|
||||
return "numerical_error";
|
||||
case TerminationReason::kOtherError:
|
||||
return "other_error";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::Span<const TerminationReason> Enum<TerminationReason>::AllValues() {
|
||||
static constexpr TerminationReason kTerminationReasonValues[] = {
|
||||
TerminationReason::kOptimal,
|
||||
TerminationReason::kInfeasible,
|
||||
TerminationReason::kUnbounded,
|
||||
TerminationReason::kInfeasibleOrUnbounded,
|
||||
TerminationReason::kImprecise,
|
||||
TerminationReason::kLimitReached,
|
||||
TerminationReason::kNumericalError,
|
||||
TerminationReason::kOtherError,
|
||||
};
|
||||
return absl::MakeConstSpan(kTerminationReasonValues);
|
||||
}
|
||||
|
||||
std::optional<absl::string_view> Enum<Limit>::ToOptString(Limit value) {
|
||||
switch (value) {
|
||||
case Limit::kUndetermined:
|
||||
return "undetermined";
|
||||
case Limit::kIteration:
|
||||
return "iteration";
|
||||
case Limit::kTime:
|
||||
return "time";
|
||||
case Limit::kNode:
|
||||
return "node";
|
||||
case Limit::kSolution:
|
||||
return "solution";
|
||||
case Limit::kMemory:
|
||||
return "memory";
|
||||
case Limit::kObjective:
|
||||
return "objective";
|
||||
case Limit::kNorm:
|
||||
return "norm";
|
||||
case Limit::kInterrupted:
|
||||
return "interrupted";
|
||||
case Limit::kSlowProgress:
|
||||
return "slow_progress";
|
||||
case Limit::kOther:
|
||||
return "other";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::Span<const Limit> Enum<Limit>::AllValues() {
|
||||
static constexpr Limit kLimitValues[] = {
|
||||
Limit::kUndetermined, Limit::kIteration, Limit::kTime,
|
||||
Limit::kNode, Limit::kSolution, Limit::kMemory,
|
||||
Limit::kObjective, Limit::kNorm, Limit::kInterrupted,
|
||||
Limit::kSlowProgress, Limit::kOther};
|
||||
return absl::MakeConstSpan(kLimitValues);
|
||||
}
|
||||
|
||||
Termination::Termination(const TerminationReason reason, std::string detail)
|
||||
: reason(reason), detail(std::move(detail)) {}
|
||||
|
||||
Termination::Termination(const Limit limit, std::string detail)
|
||||
: reason(TerminationReason::kLimitReached),
|
||||
limit(limit),
|
||||
detail(std::move(detail)) {}
|
||||
|
||||
TerminationProto Termination::ToProto() const {
|
||||
TerminationProto proto;
|
||||
proto.set_reason(EnumToProto(reason));
|
||||
if (limit.has_value()) {
|
||||
proto.set_limit(EnumToProto(*limit));
|
||||
}
|
||||
proto.set_detail(detail);
|
||||
return proto;
|
||||
}
|
||||
|
||||
Termination Termination::FromProto(const TerminationProto& termination_proto) {
|
||||
const bool limit_reached =
|
||||
termination_proto.reason() == TERMINATION_REASON_LIMIT_REACHED;
|
||||
const bool has_limit = termination_proto.limit() != LIMIT_UNSPECIFIED;
|
||||
CHECK_EQ(limit_reached, has_limit)
|
||||
<< "Termination reason should be LIMIT_REACHED if and only if limit is "
|
||||
"specified, but found reason="
|
||||
<< ProtoEnumToString(termination_proto.reason())
|
||||
<< " and limit=" << ProtoEnumToString(termination_proto.limit());
|
||||
|
||||
if (has_limit) {
|
||||
const std::optional<Limit> opt_limit =
|
||||
EnumFromProto(termination_proto.limit());
|
||||
CHECK(opt_limit.has_value());
|
||||
return Termination(*opt_limit, termination_proto.detail());
|
||||
}
|
||||
|
||||
const std::optional<TerminationReason> opt_reason =
|
||||
EnumFromProto(termination_proto.reason());
|
||||
CHECK(opt_reason.has_value());
|
||||
return Termination(*opt_reason, termination_proto.detail());
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostr, const Termination& termination) {
|
||||
ostr << "{reason: " << termination.reason;
|
||||
if (termination.limit.has_value()) {
|
||||
ostr << ", limit: " << *termination.limit;
|
||||
}
|
||||
if (!termination.detail.empty()) {
|
||||
// TODO(b/200835670): quote detail and escape it properly.
|
||||
ostr << ", detail: " << termination.detail;
|
||||
}
|
||||
ostr << "}";
|
||||
return ostr;
|
||||
}
|
||||
|
||||
std::string Termination::ToString() const {
|
||||
std::ostringstream stream;
|
||||
stream << *this;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
ProblemStatusProto ProblemStatus::ToProto() const {
|
||||
ProblemStatusProto proto;
|
||||
proto.set_primal_status(EnumToProto(primal_status));
|
||||
proto.set_dual_status(EnumToProto(dual_status));
|
||||
proto.set_primal_or_dual_infeasible(primal_or_dual_infeasible);
|
||||
return proto;
|
||||
}
|
||||
|
||||
ProblemStatus ProblemStatus::FromProto(
|
||||
const ProblemStatusProto& problem_status_proto) {
|
||||
ProblemStatus result;
|
||||
// TODO(b/209014770): consider adding a function to simplify this pattern.
|
||||
const std::optional<FeasibilityStatus> opt_primal_status =
|
||||
EnumFromProto(problem_status_proto.primal_status());
|
||||
const std::optional<FeasibilityStatus> opt_dual_status =
|
||||
EnumFromProto(problem_status_proto.dual_status());
|
||||
CHECK(opt_primal_status.has_value());
|
||||
CHECK(opt_dual_status.has_value());
|
||||
result.primal_status = *opt_primal_status;
|
||||
result.dual_status = *opt_dual_status;
|
||||
result.primal_or_dual_infeasible =
|
||||
problem_status_proto.primal_or_dual_infeasible();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostr,
|
||||
const ProblemStatus& problem_status) {
|
||||
ostr << "{primal_status: " << problem_status.primal_status;
|
||||
ostr << ", dual_status: " << problem_status.dual_status;
|
||||
ostr << ", primal_or_dual_infeasible: "
|
||||
<< (problem_status.primal_or_dual_infeasible ? "true" : "false");
|
||||
ostr << "}";
|
||||
return ostr;
|
||||
}
|
||||
|
||||
std::string ProblemStatus::ToString() const {
|
||||
std::ostringstream stream;
|
||||
stream << *this;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
SolveStatsProto SolveStats::ToProto() const {
|
||||
SolveStatsProto proto;
|
||||
CHECK_OK(
|
||||
util_time::EncodeGoogleApiProto(solve_time, proto.mutable_solve_time()));
|
||||
proto.set_best_primal_bound(best_primal_bound);
|
||||
proto.set_best_dual_bound(best_dual_bound);
|
||||
*proto.mutable_problem_status() = problem_status.ToProto();
|
||||
proto.set_simplex_iterations(simplex_iterations);
|
||||
proto.set_barrier_iterations(barrier_iterations);
|
||||
proto.set_node_count(node_count);
|
||||
return proto;
|
||||
}
|
||||
|
||||
SolveStats SolveStats::FromProto(const SolveStatsProto& solve_stats_proto) {
|
||||
SolveStats result;
|
||||
result.solve_time =
|
||||
util_time::DecodeGoogleApiProto(solve_stats_proto.solve_time()).value();
|
||||
result.best_primal_bound = solve_stats_proto.best_primal_bound();
|
||||
result.best_dual_bound = solve_stats_proto.best_dual_bound();
|
||||
result.problem_status =
|
||||
ProblemStatus::FromProto(solve_stats_proto.problem_status());
|
||||
result.simplex_iterations = solve_stats_proto.simplex_iterations();
|
||||
result.barrier_iterations = solve_stats_proto.barrier_iterations();
|
||||
result.node_count = solve_stats_proto.node_count();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostr, const SolveStats& solve_stats) {
|
||||
ostr << "{solve_time: " << solve_stats.solve_time;
|
||||
ostr << ", best_primal_bound: " << solve_stats.best_primal_bound;
|
||||
ostr << ", best_dual_bound: " << 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 << ", node_count: " << solve_stats.node_count;
|
||||
ostr << "}";
|
||||
return ostr;
|
||||
}
|
||||
|
||||
std::string SolveStats::ToString() const {
|
||||
std::ostringstream stream;
|
||||
stream << *this;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
SolveResult SolveResult::FromProto(const ModelStorage* model,
|
||||
const SolveResultProto& solve_result_proto) {
|
||||
SolveResult result(Termination::FromProto(solve_result_proto.termination()));
|
||||
result.warnings = {solve_result_proto.warnings().begin(),
|
||||
solve_result_proto.warnings().end()};
|
||||
result.solve_stats = SolveStats::FromProto(solve_result_proto.solve_stats());
|
||||
|
||||
for (const SolutionProto& solution : solve_result_proto.solutions()) {
|
||||
result.solutions.push_back(Solution::FromProto(model, solution));
|
||||
}
|
||||
for (const PrimalRayProto& primal_ray : solve_result_proto.primal_rays()) {
|
||||
result.primal_rays.push_back(PrimalRay::FromProto(model, primal_ray));
|
||||
}
|
||||
for (const DualRayProto& dual_ray : solve_result_proto.dual_rays()) {
|
||||
result.dual_rays.push_back(DualRay::FromProto(model, dual_ray));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool SolveResult::has_primal_feasible_solution() const {
|
||||
return !solutions.empty() && solutions[0].primal_solution.has_value() &&
|
||||
(solutions[0].primal_solution->feasibility_status ==
|
||||
SolutionStatus::kFeasible);
|
||||
}
|
||||
|
||||
double SolveResult::objective_value() const {
|
||||
CHECK(has_primal_feasible_solution());
|
||||
return solutions[0].primal_solution->objective_value;
|
||||
}
|
||||
|
||||
const VariableMap<double>& SolveResult::variable_values() const {
|
||||
CHECK(has_primal_feasible_solution());
|
||||
return solutions[0].primal_solution->variable_values;
|
||||
}
|
||||
|
||||
const VariableMap<double>& SolveResult::ray_variable_values() const {
|
||||
CHECK(has_ray());
|
||||
return primal_rays[0].variable_values;
|
||||
}
|
||||
|
||||
bool SolveResult::has_dual_feasible_solution() const {
|
||||
return !solutions.empty() && solutions[0].dual_solution.has_value() &&
|
||||
(solutions[0].dual_solution->feasibility_status ==
|
||||
SolutionStatus::kFeasible);
|
||||
}
|
||||
|
||||
const LinearConstraintMap<double>& SolveResult::dual_values() const {
|
||||
CHECK(has_dual_feasible_solution());
|
||||
return solutions[0].dual_solution->dual_values;
|
||||
}
|
||||
|
||||
const VariableMap<double>& SolveResult::reduced_costs() const {
|
||||
CHECK(has_dual_feasible_solution());
|
||||
return solutions[0].dual_solution->reduced_costs;
|
||||
}
|
||||
const LinearConstraintMap<double>& SolveResult::ray_dual_values() const {
|
||||
CHECK(has_dual_ray());
|
||||
return dual_rays[0].dual_values;
|
||||
}
|
||||
|
||||
const VariableMap<double>& SolveResult::ray_reduced_costs() const {
|
||||
CHECK(has_dual_ray());
|
||||
return dual_rays[0].reduced_costs;
|
||||
}
|
||||
|
||||
bool SolveResult::has_basis() const {
|
||||
return !solutions.empty() && solutions[0].basis.has_value();
|
||||
}
|
||||
|
||||
const LinearConstraintMap<BasisStatus>& SolveResult::constraint_status() const {
|
||||
CHECK(has_basis());
|
||||
return solutions[0].basis->constraint_status;
|
||||
}
|
||||
|
||||
const VariableMap<BasisStatus>& SolveResult::variable_status() const {
|
||||
CHECK(has_basis());
|
||||
return solutions[0].basis->variable_status;
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
383
ortools/math_opt/cpp/solve_result.h
Normal file
383
ortools/math_opt/cpp/solve_result.h
Normal file
@@ -0,0 +1,383 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_SOLVE_RESULT_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_SOLVE_RESULT_H_
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/enums.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/linear_constraint.h"
|
||||
#include "ortools/math_opt/cpp/solution.h" // IWYU pragma: export
|
||||
#include "ortools/math_opt/cpp/variable_and_expressions.h"
|
||||
#include "ortools/math_opt/result.pb.h" // IWYU pragma: export
|
||||
#include "ortools/base/protoutil.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// Problem feasibility status as claimed by the solver (solver is not required
|
||||
// to return a certificate for the claim).
|
||||
enum class FeasibilityStatus {
|
||||
// Solver does not claim a status.
|
||||
kUndetermined = FEASIBILITY_STATUS_UNDETERMINED,
|
||||
|
||||
// Solver claims the problem is feasible.
|
||||
kFeasible = FEASIBILITY_STATUS_FEASIBLE,
|
||||
|
||||
// Solver claims the problem is infeasible.
|
||||
kInfeasible = FEASIBILITY_STATUS_INFEASIBLE,
|
||||
};
|
||||
|
||||
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
|
||||
// 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
|
||||
// feasible status may imply the existence of an optimal solution, it does not
|
||||
// guarantee the solver has actually found such optimal solution.
|
||||
struct ProblemStatus {
|
||||
// Status for the primal problem.
|
||||
FeasibilityStatus primal_status = FeasibilityStatus::kUndetermined;
|
||||
|
||||
// Status for the dual problem (or for the dual of a continuous relaxation).
|
||||
FeasibilityStatus dual_status = FeasibilityStatus::kUndetermined;
|
||||
|
||||
// If true, the solver claims the primal or dual problem is infeasible, but
|
||||
// it does not know which (or if both are infeasible). Can be true only when
|
||||
// primal_problem_status = dual_problem_status = kUndetermined. This extra
|
||||
// information is often needed when preprocessing determines there is no
|
||||
// optimal solution to the problem (but can't determine if it is due to
|
||||
// infeasibility, unboundedness, or both).
|
||||
bool primal_or_dual_infeasible = false;
|
||||
|
||||
static ProblemStatus FromProto(
|
||||
const ProblemStatusProto& problem_status_proto);
|
||||
|
||||
ProblemStatusProto ToProto() const;
|
||||
std::string ToString() const;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostr, const ProblemStatus& status);
|
||||
|
||||
struct SolveStats {
|
||||
// Elapsed wall clock time as measured by math_opt, roughly the time inside
|
||||
// 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:
|
||||
// * best_primal_bound is trivial (+inf for minimization and -inf
|
||||
// maximization) when the solver does not claim to have such bound. This
|
||||
// may happen for some solvers (e.g., PDLP, typically continuous solvers)
|
||||
// even when returning optimal (solver could terminate with slightly
|
||||
// infeasible primal solutions).
|
||||
// * 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.
|
||||
// * best_dual_bound is always better (smaller for minimization and larger
|
||||
// for maximization) than best_primal_bound.
|
||||
|
||||
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:
|
||||
// * best_dual_bound is always better (smaller for minimization and larger
|
||||
// for maximization) than best_primal_bound.
|
||||
// * 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.
|
||||
double best_dual_bound = 0.0;
|
||||
|
||||
// Feasibility statuses for primal and dual problems.
|
||||
ProblemStatus problem_status;
|
||||
|
||||
int simplex_iterations = 0;
|
||||
|
||||
int barrier_iterations = 0;
|
||||
|
||||
int node_count = 0;
|
||||
|
||||
// Will CHECK fail on invalid input, if problem_status is invalid.
|
||||
static SolveStats FromProto(const SolveStatsProto& solve_stats_proto);
|
||||
|
||||
SolveStatsProto ToProto() const;
|
||||
std::string ToString() const;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostr, const SolveStats& stats);
|
||||
|
||||
// The reason a call to Solve() terminates.
|
||||
enum class TerminationReason {
|
||||
// A provably optimal solution (up to numerical tolerances) has been found.
|
||||
kOptimal = TERMINATION_REASON_OPTIMAL,
|
||||
|
||||
// The primal problem has no feasible solutions.
|
||||
kInfeasible = TERMINATION_REASON_INFEASIBLE,
|
||||
|
||||
// The primal problem is feasible and arbitrarily good solutions can be
|
||||
// found along a primal ray.
|
||||
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
|
||||
// Gurobi's unbounded status may be mapped here as explained in
|
||||
// go/mathopt-solver-specific#gurobi-inf-or-unb.
|
||||
kInfeasibleOrUnbounded = TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED,
|
||||
|
||||
// The problem was solved to one of the criteria above (Optimal, Infeasible,
|
||||
// Unbounded, or InfeasibleOrUnbounded), but one or more tolerances was not
|
||||
// met. Some primal/dual solutions/rays be present, but either they will be
|
||||
// slightly infeasible, or (if the problem was nearly optimal) their may be
|
||||
// a gap between the best solution objective and best objective bound.
|
||||
//
|
||||
// Users can still query primal/dual solutions/rays and solution stats, but
|
||||
// they are responsible for dealing with the numerical imprecision.
|
||||
kImprecise = TERMINATION_REASON_IMPRECISE,
|
||||
|
||||
// The optimizer reached some kind of limit. Partial solution information
|
||||
// may be available. See Termination::limit for more detail.
|
||||
kLimitReached = TERMINATION_REASON_LIMIT_REACHED,
|
||||
|
||||
// The algorithm stopped because it encountered unrecoverable numerical
|
||||
// error. No solution information is available.
|
||||
kNumericalError = TERMINATION_REASON_NUMERICAL_ERROR,
|
||||
|
||||
// The algorithm stopped because of an error not covered by one of the
|
||||
// statuses defined above. No solution information is available.
|
||||
kOtherError = TERMINATION_REASON_OTHER_ERROR
|
||||
};
|
||||
|
||||
MATH_OPT_DEFINE_ENUM(TerminationReason, TERMINATION_REASON_UNSPECIFIED);
|
||||
|
||||
// When a Solve() stops early with TerminationReason kLimitReached, the
|
||||
// specific limit that was hit.
|
||||
enum class Limit {
|
||||
// Used if the underlying solver cannot determine which limit was reached, or
|
||||
// as a null value when we terminated not from a limit (e.g. kOptimal).
|
||||
kUndetermined = LIMIT_UNDETERMINED,
|
||||
|
||||
// An iterative algorithm stopped after conducting the maximum number of
|
||||
// iterations (e.g. simplex or barrier iterations).
|
||||
kIteration = LIMIT_ITERATION,
|
||||
|
||||
// The algorithm stopped after a user-specified computation time.
|
||||
kTime = LIMIT_TIME,
|
||||
|
||||
// A branch-and-bound algorithm stopped because it explored a maximum number
|
||||
// of nodes in the branch-and-bound tree.
|
||||
kNode = LIMIT_NODE,
|
||||
|
||||
// The algorithm stopped because it found the required number of solutions.
|
||||
// This is often used in MIPs to get the solver to return the first feasible
|
||||
// solution it encounters.
|
||||
kSolution = LIMIT_SOLUTION,
|
||||
|
||||
// The algorithm stopped because it ran out of memory.
|
||||
kMemory = LIMIT_MEMORY,
|
||||
|
||||
// The algorithm stopped because it found a solution better than a minimum
|
||||
// limit set by the user.
|
||||
kObjective = LIMIT_OBJECTIVE,
|
||||
|
||||
// The algorithm stopped because the norm of an iterate became too large.
|
||||
kNorm = LIMIT_NORM,
|
||||
|
||||
// The algorithm stopped because of an interrupt signal or a user interrupt
|
||||
// request.
|
||||
kInterrupted = LIMIT_INTERRUPTED,
|
||||
|
||||
// The algorithm stopped because it was unable to continue making progress
|
||||
// towards the solution.
|
||||
kSlowProgress = LIMIT_SLOW_PROGRESS,
|
||||
|
||||
// The algorithm stopped due to a limit not covered by one of the above. Note
|
||||
// that kUndetermined is used when the reason cannot be determined, and kOther
|
||||
// is used when the reason is known but does not fit into any of the above
|
||||
// alternatives.
|
||||
kOther = LIMIT_OTHER
|
||||
};
|
||||
|
||||
MATH_OPT_DEFINE_ENUM(Limit, LIMIT_UNSPECIFIED);
|
||||
|
||||
// All information regarding why a call to Solve() terminated.
|
||||
struct Termination {
|
||||
// When the reason is kLimitReached, please prefer using the other
|
||||
// constructor that enables setting the limit.
|
||||
explicit Termination(TerminationReason reason, std::string detail = {});
|
||||
|
||||
// Sets the reason to kLimitReached.
|
||||
explicit Termination(Limit limit, std::string detail = {});
|
||||
|
||||
TerminationReason reason;
|
||||
|
||||
// Is set iff reason is kLimitReached.
|
||||
std::optional<Limit> limit;
|
||||
|
||||
// Additional typically solver specific information about termination.
|
||||
// Not all solvers can always determine the limit which caused termination,
|
||||
// Limit::kUndetermined is used when the cause cannot be determined.
|
||||
std::string detail;
|
||||
|
||||
// Will CHECK fail on invalid input, if reason is unspecified, if limit is
|
||||
// set when reason is not LIMIT_REACHED, or if limit is unspecified when
|
||||
// reason is LIMIT_REACHED (see solution_validator.h).
|
||||
static Termination FromProto(const TerminationProto& termination_proto);
|
||||
|
||||
TerminationProto ToProto() const;
|
||||
std::string ToString() const;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostr, const Termination& termination);
|
||||
|
||||
// The result of solving an optimization problem with Solve().
|
||||
struct SolveResult {
|
||||
explicit SolveResult(Termination termination)
|
||||
: termination(std::move(termination)) {}
|
||||
|
||||
// Non-fatal errors, e.g. an unsupported parameter that was skipped.
|
||||
std::vector<std::string> warnings;
|
||||
|
||||
// The reason the solver stopped.
|
||||
Termination termination;
|
||||
|
||||
// Statistics on the solve process, e.g. running time, iterations.
|
||||
SolveStats solve_stats;
|
||||
|
||||
// Basic solutions use, as of Nov 2021:
|
||||
// * All convex optimization solvers (LP, convex QP) return only one
|
||||
// solution as a primal dual pair.
|
||||
// * Only MI(Q)P solvers return more than one solution. MIP solvers do not
|
||||
// return any dual information, or primal infeasible solutions. Solutions
|
||||
// are returned in order of best primal objective first. Gurobi solves
|
||||
// nonconvex QP (integer or continuous) as MIQP.
|
||||
|
||||
// The general contract for the order of solutions that future solvers should
|
||||
// implement is to order by:
|
||||
// 1. The solutions with a primal feasible solution, ordered by best primal
|
||||
// objective first.
|
||||
// 2. The solutions with a dual feasible solution, ordered by best dual
|
||||
// objective (unknown dual objective is worst)
|
||||
// 3. All remaining solutions can be returned in any order.
|
||||
std::vector<Solution> solutions;
|
||||
|
||||
// Directions of unbounded primal improvement, or equivalently, dual
|
||||
// infeasibility certificates. Typically provided for TerminationReasons
|
||||
// kUnbounded and kInfeasibleOrUnbounded.
|
||||
std::vector<PrimalRay> primal_rays;
|
||||
|
||||
// Directions of unbounded dual improvement, or equivalently, primal
|
||||
// infeasibility certificates. Typically provided for TerminationReason
|
||||
// kInfeasible.
|
||||
std::vector<DualRay> dual_rays;
|
||||
|
||||
static SolveResult FromProto(const ModelStorage* model,
|
||||
const SolveResultProto& solve_result_proto);
|
||||
|
||||
absl::Duration solve_time() const { return solve_stats.solve_time; }
|
||||
|
||||
// Indicates if at least one primal feasible solution is available.
|
||||
//
|
||||
// When termination.reason is TerminationReason::kOptimal, this is guaranteed
|
||||
// to be true and need not be checked.
|
||||
bool has_primal_feasible_solution() const;
|
||||
|
||||
// The objective value of the best primal feasible solution. Will CHECK fail
|
||||
// if there are no primal feasible solutions.
|
||||
double objective_value() const;
|
||||
|
||||
// The variable values from the best primal feasible solution. Will CHECK fail
|
||||
// if there are no primal feasible solutions.
|
||||
const VariableMap<double>& variable_values() const;
|
||||
|
||||
// Indicates if at least one primal ray is available.
|
||||
//
|
||||
// This is NOT guaranteed to be true when termination.reason is
|
||||
// TerminationReason::kUnbounded or TerminationReason::kInfeasibleOrUnbounded.
|
||||
bool has_ray() const { return !primal_rays.empty(); }
|
||||
|
||||
// The variable values from the first primal ray. Will CHECK fail if there
|
||||
// are no primal rays.
|
||||
const VariableMap<double>& ray_variable_values() const;
|
||||
|
||||
// Indicates if the best primal solution has an associated dual feasible
|
||||
// solution.
|
||||
//
|
||||
// This is NOT guaranteed to be true when termination.reason is
|
||||
// TerminationReason::kOptimal. It also may be true even when the best primal
|
||||
// solution is not feasible.
|
||||
bool has_dual_feasible_solution() const;
|
||||
|
||||
// The dual values from the best dual solution. Will CHECK fail if there
|
||||
// are no dual solutions.
|
||||
const LinearConstraintMap<double>& dual_values() const;
|
||||
|
||||
// The reduced from the best dual solution. Will CHECK fail if there
|
||||
// are no dual solutions.
|
||||
const VariableMap<double>& reduced_costs() const;
|
||||
|
||||
// Indicates if at least one dual ray is available.
|
||||
//
|
||||
// This is NOT guaranteed to be true when termination.reason is
|
||||
// TerminationReason::kInfeasible.
|
||||
bool has_dual_ray() const { return !dual_rays.empty(); }
|
||||
|
||||
// The dual values from the first dual ray. Will CHECK fail if there
|
||||
// are no dual rays.
|
||||
const LinearConstraintMap<double>& ray_dual_values() const;
|
||||
|
||||
// The reduced from the first dual ray. Will CHECK fail if there
|
||||
// are no dual rays.
|
||||
const VariableMap<double>& ray_reduced_costs() const;
|
||||
|
||||
// Indicates if at least one basis is available.
|
||||
bool has_basis() const;
|
||||
|
||||
// The constraint basis status for the first primal/dual pair.
|
||||
const LinearConstraintMap<BasisStatus>& constraint_status() const;
|
||||
|
||||
// The variable basis status for the first primal/dual pair.
|
||||
const VariableMap<BasisStatus>& variable_status() const;
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_SOLVE_RESULT_H_
|
||||
55
ortools/math_opt/cpp/streamable_solver_init_arguments.cc
Normal file
55
ortools/math_opt/cpp/streamable_solver_init_arguments.cc
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/streamable_solver_init_arguments.h"
|
||||
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
|
||||
#include "ortools/math_opt/parameters.pb.h"
|
||||
#include "ortools/math_opt/solvers/gurobi.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
GurobiInitializerProto::ISVKey GurobiISVKey::Proto() const {
|
||||
GurobiInitializerProto::ISVKey isv_key_proto;
|
||||
isv_key_proto.set_name(name);
|
||||
isv_key_proto.set_application_name(application_name);
|
||||
isv_key_proto.set_expiration(expiration);
|
||||
isv_key_proto.set_key(key);
|
||||
return isv_key_proto;
|
||||
}
|
||||
|
||||
GurobiInitializerProto StreamableGurobiInitArguments::Proto() const {
|
||||
GurobiInitializerProto params_proto;
|
||||
|
||||
if (isv_key) {
|
||||
*params_proto.mutable_isv_key() = isv_key->Proto();
|
||||
}
|
||||
|
||||
return params_proto;
|
||||
}
|
||||
|
||||
SolverInitializerProto StreamableSolverInitArguments::Proto() const {
|
||||
SolverInitializerProto params_proto;
|
||||
|
||||
if (gurobi) {
|
||||
*params_proto.mutable_gurobi() = gurobi->Proto();
|
||||
}
|
||||
|
||||
return params_proto;
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
90
ortools/math_opt/cpp/streamable_solver_init_arguments.h
Normal file
90
ortools/math_opt/cpp/streamable_solver_init_arguments.h
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This headers defines C++ wrappers of solver specific initialization
|
||||
// parameters that can be streamed to be exchanged with another process.
|
||||
//
|
||||
// Parameters that can't be streamed (for example instances of C/C++ types that
|
||||
// only exist in the process memory) are dealt with implementations of
|
||||
// the NonStreamableSolverInitArguments.
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_STREAMABLE_SOLVER_INIT_ARGUMENTS_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_STREAMABLE_SOLVER_INIT_ARGUMENTS_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "ortools/math_opt/parameters.pb.h"
|
||||
#include "ortools/math_opt/solvers/gurobi.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// Streamable Pdlp specific parameters for solver instantiation.
|
||||
struct StreamablePdlpInitArguments {};
|
||||
|
||||
// Streamable CpSat specific parameters for solver instantiation.
|
||||
struct StreamableCpSatInitArguments {};
|
||||
|
||||
// Streamable GScip specific parameters for solver instantiation.
|
||||
struct StreamableGScipInitArguments {};
|
||||
|
||||
// Streamable Glop specific parameters for solver instantiation.
|
||||
struct StreamableGlopInitArguments {};
|
||||
|
||||
// Streamable Glpk specific parameters for solver instantiation.
|
||||
struct StreamableGlpkInitArguments {};
|
||||
|
||||
// An ISV key for the Gurobi solver.
|
||||
//
|
||||
// See http://www.gurobi.com/products/licensing-pricing/isv-program.
|
||||
struct GurobiISVKey {
|
||||
std::string name;
|
||||
std::string application_name;
|
||||
int64_t expiration = 0;
|
||||
std::string key;
|
||||
|
||||
GurobiInitializerProto::ISVKey Proto() const;
|
||||
};
|
||||
|
||||
// Streamable Gurobi specific parameters for solver instantiation.
|
||||
struct StreamableGurobiInitArguments {
|
||||
// An optional ISV key to use to instantiate the solver. This is ignored if a
|
||||
// `master_env` is provided in `NonStreamableGurobiInitArguments`.
|
||||
std::optional<GurobiISVKey> isv_key;
|
||||
|
||||
// Returns the proto corresponding to these parameters.
|
||||
GurobiInitializerProto Proto() const;
|
||||
};
|
||||
|
||||
// Solver specific initialization parameters that can be streamed to be
|
||||
// exchanged with another process.
|
||||
//
|
||||
// Parameters that can't be streamed (for example instances of C/C++ types that
|
||||
// only exist in the process memory) are dealt with implementations of
|
||||
// the NonStreamableSolverInitArguments.
|
||||
struct StreamableSolverInitArguments {
|
||||
std::optional<StreamableCpSatInitArguments> cp_sat;
|
||||
std::optional<StreamableGScipInitArguments> gscip;
|
||||
std::optional<StreamableGlopInitArguments> glop;
|
||||
std::optional<StreamableGlpkInitArguments> glpk;
|
||||
std::optional<StreamableGurobiInitArguments> gurobi;
|
||||
|
||||
// Returns the proto corresponding to these parameters.
|
||||
SolverInitializerProto Proto() const;
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_STREAMABLE_SOLVER_INIT_ARGUMENTS_H_
|
||||
59
ortools/math_opt/cpp/update_tracker.cc
Normal file
59
ortools/math_opt/cpp/update_tracker.cc
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/cpp/update_tracker.h"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
UpdateTracker::~UpdateTracker() {
|
||||
const std::shared_ptr<ModelStorage> storage = storage_.lock();
|
||||
|
||||
// If the model has already been destroyed, the update tracker has been
|
||||
// automatically cleaned.
|
||||
if (storage != nullptr) {
|
||||
storage->DeleteUpdateTracker(update_tracker_);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateTracker::UpdateTracker(const std::shared_ptr<ModelStorage>& storage)
|
||||
: storage_(ABSL_DIE_IF_NULL(storage)),
|
||||
update_tracker_(storage->NewUpdateTracker()) {}
|
||||
|
||||
std::optional<ModelUpdateProto> UpdateTracker::ExportModelUpdate() {
|
||||
const std::shared_ptr<ModelStorage> storage = storage_.lock();
|
||||
CHECK(storage != nullptr) << internal::kModelIsDestroyed;
|
||||
return storage->ExportModelUpdate(update_tracker_);
|
||||
}
|
||||
|
||||
void UpdateTracker::Checkpoint() {
|
||||
const std::shared_ptr<ModelStorage> storage = storage_.lock();
|
||||
CHECK(storage != nullptr) << internal::kModelIsDestroyed;
|
||||
storage->Checkpoint(update_tracker_);
|
||||
}
|
||||
|
||||
ModelProto UpdateTracker::ExportModel() const {
|
||||
const std::shared_ptr<ModelStorage> storage = storage_.lock();
|
||||
CHECK(storage != nullptr) << internal::kModelIsDestroyed;
|
||||
return storage->ExportModel();
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
106
ortools/math_opt/cpp/update_tracker.h
Normal file
106
ortools/math_opt/cpp/update_tracker.h
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_CPP_UPDATE_TRACKER_H_
|
||||
#define OR_TOOLS_MATH_OPT_CPP_UPDATE_TRACKER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_update.pb.h" // IWYU pragma: export
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// Tracks the changes of the model.
|
||||
//
|
||||
// This is an advanced feature that most users won't need. It is used internally
|
||||
// to implement incrementalism but users don't have to understand how it works
|
||||
// to use incremental solve.
|
||||
//
|
||||
// For each update tracker we define a checkpoint that is the starting point
|
||||
// used to compute the ModelUpdateProto.
|
||||
//
|
||||
// No member function should be called after the destruction of the Model
|
||||
// object. Note though that it is safe to call the destructor of UpdateTracker
|
||||
// even if the Model object has been destroyed already.
|
||||
//
|
||||
// Thread-safety: UpdateTracker methods must not be used while modifying the
|
||||
// model (variables, constraints, ...). The user is expected to use proper
|
||||
// synchronization primitives to serialize changes to the model and the use of
|
||||
// the update trackers. The methods of different instances of UpdateTracker are
|
||||
// safe to be called concurrently (i.e. multiple trackers can be called
|
||||
// concurrently on ExportModelUpdate() or Checkpoint()). The destructor of
|
||||
// UpdateTracker is thread-safe.
|
||||
//
|
||||
// Example:
|
||||
// Model model;
|
||||
// ...
|
||||
// const std::unique_ptr<UpdateTracker> update_tracker =
|
||||
// model.NewUpdateTracker();
|
||||
//
|
||||
// model.AddVariable(0.0, 1.0, true, "y");
|
||||
// model.set_maximize(true);
|
||||
//
|
||||
// const std::optional<ModelUpdateProto> update_proto =
|
||||
// update_tracker.ExportModelUpdate();
|
||||
// update_tracker.Checkpoint();
|
||||
//
|
||||
// if (update_proto) {
|
||||
// ... use *update_proto here ...
|
||||
// }
|
||||
class UpdateTracker {
|
||||
public:
|
||||
// This constructor should not be used directly. Instead use
|
||||
// Model::NewUpdateTracker().
|
||||
explicit UpdateTracker(const std::shared_ptr<ModelStorage>& storage);
|
||||
|
||||
~UpdateTracker();
|
||||
|
||||
// Returns a proto representation of the changes to the model since the most
|
||||
// recent checkpoint (i.e. last time Checkpoint() was called); nullopt if
|
||||
// the update would have been empty.
|
||||
std::optional<ModelUpdateProto> ExportModelUpdate();
|
||||
|
||||
// Uses the current model state as the starting point to calculate the
|
||||
// ModelUpdateProto next time ExportModelUpdate() is called.
|
||||
void Checkpoint();
|
||||
|
||||
// Returns a proto representation of the whole model.
|
||||
//
|
||||
// This is a shortcut method that is equivalent to calling
|
||||
// Model::ExportModel(). It is there so that users of the UpdateTracker
|
||||
// can avoid having to keep a reference to the Model model.
|
||||
ModelProto ExportModel() const;
|
||||
|
||||
private:
|
||||
const std::weak_ptr<ModelStorage> storage_;
|
||||
const UpdateTrackerId update_tracker_;
|
||||
};
|
||||
|
||||
namespace internal {
|
||||
|
||||
// The CHECK message used when a function of UpdateTracker is called after the
|
||||
// destruction of the model..
|
||||
constexpr absl::string_view kModelIsDestroyed =
|
||||
"Can't call this function after the associated model has been destroyed.";
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_CPP_UPDATE_TRACKER_H_
|
||||
@@ -18,15 +18,16 @@
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/base/attributes.h"
|
||||
#include "ortools/base/map_util.h"
|
||||
#include "ortools/base/int_type.h"
|
||||
#include "ortools/math_opt/core/model_storage.h"
|
||||
#include "ortools/math_opt/cpp/key_types.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
#ifdef USE_LINEAR_EXPRESSION_COUNTERS
|
||||
#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS
|
||||
LinearExpression::LinearExpression() { ++num_calls_default_constructor_; }
|
||||
|
||||
LinearExpression::LinearExpression(const LinearExpression& other)
|
||||
@@ -61,13 +62,13 @@ void LinearExpression::ResetCounters() {
|
||||
num_calls_move_constructor_ = 0;
|
||||
num_calls_initializer_list_constructor_ = 0;
|
||||
}
|
||||
#endif // USE_LINEAR_EXPRESSION_COUNTERS
|
||||
#endif // MATH_OPT_USE_EXPRESSION_COUNTERS
|
||||
|
||||
double LinearExpression::Evaluate(
|
||||
const VariableMap<double>& variable_values) const {
|
||||
if (variable_values.model() != nullptr && model() != nullptr) {
|
||||
CHECK_EQ(variable_values.model(), model())
|
||||
<< internal::kObjectsFromOtherIndexedModel;
|
||||
if (variable_values.storage() != nullptr && storage() != nullptr) {
|
||||
CHECK_EQ(variable_values.storage(), storage())
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
double result = offset_;
|
||||
for (const auto& [variable_id, coef] : terms_.raw_map()) {
|
||||
@@ -78,9 +79,9 @@ double LinearExpression::Evaluate(
|
||||
|
||||
double LinearExpression::EvaluateWithDefaultZero(
|
||||
const VariableMap<double>& variable_values) const {
|
||||
if (variable_values.model() != nullptr && model() != nullptr) {
|
||||
CHECK_EQ(variable_values.model(), model())
|
||||
<< internal::kObjectsFromOtherIndexedModel;
|
||||
if (variable_values.storage() != nullptr && storage() != nullptr) {
|
||||
CHECK_EQ(variable_values.storage(), storage())
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
double result = offset_;
|
||||
for (const auto& [variable_id, coef] : terms_.raw_map()) {
|
||||
@@ -107,7 +108,8 @@ std::ostream& operator<<(std::ostream& ostr,
|
||||
ostr << " + ";
|
||||
}
|
||||
ostr << expression.terms_.at(v) << "*";
|
||||
const std::string& name = v.name();
|
||||
const std::string& name =
|
||||
expression.terms_.storage()->variable_name(v.typed_id());
|
||||
if (name.empty()) {
|
||||
ostr << "[" << v << "]";
|
||||
} else {
|
||||
@@ -133,5 +135,124 @@ std::ostream& operator<<(std::ostream& ostr,
|
||||
return ostr;
|
||||
}
|
||||
|
||||
double QuadraticExpression::Evaluate(
|
||||
const VariableMap<double>& variable_values) const {
|
||||
if (variable_values.storage() != nullptr && storage() != nullptr) {
|
||||
CHECK_EQ(variable_values.storage(), storage())
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
double result = offset();
|
||||
for (const auto& [variable_id, coef] : linear_terms_.raw_map()) {
|
||||
result += coef * variable_values.raw_map().at(variable_id);
|
||||
}
|
||||
for (const auto& [variable_ids, coef] : quadratic_terms_.raw_map()) {
|
||||
result += coef * variable_values.raw_map().at(variable_ids.first) *
|
||||
variable_values.raw_map().at(variable_ids.second);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
double QuadraticExpression::EvaluateWithDefaultZero(
|
||||
const VariableMap<double>& variable_values) const {
|
||||
if (variable_values.storage() != nullptr && storage() != nullptr) {
|
||||
CHECK_EQ(variable_values.storage(), storage())
|
||||
<< internal::kObjectsFromOtherModelStorage;
|
||||
}
|
||||
double result = offset();
|
||||
for (const auto& [variable_id, coef] : linear_terms_.raw_map()) {
|
||||
result +=
|
||||
coef * gtl::FindWithDefault(variable_values.raw_map(), variable_id);
|
||||
}
|
||||
for (const auto& [variable_ids, coef] : quadratic_terms_.raw_map()) {
|
||||
result +=
|
||||
coef *
|
||||
gtl::FindWithDefault(variable_values.raw_map(), variable_ids.first) *
|
||||
gtl::FindWithDefault(variable_values.raw_map(), variable_ids.second);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostr, const QuadraticExpression& expr) {
|
||||
// TODO(b/169415597): improve quadratic expression formatting.
|
||||
bool first = true;
|
||||
for (const auto v : expr.quadratic_terms().SortedKeys()) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
ostr << " + ";
|
||||
}
|
||||
ostr << expr.quadratic_terms().at(v) << "*";
|
||||
const Variable first_variable(expr.quadratic_terms().storage(),
|
||||
v.typed_id().first);
|
||||
const Variable second_variable(expr.quadratic_terms().storage(),
|
||||
v.typed_id().second);
|
||||
if (first_variable == second_variable) {
|
||||
ostr << first_variable << "²";
|
||||
} else {
|
||||
ostr << first_variable << "*" << second_variable;
|
||||
}
|
||||
}
|
||||
for (const auto v : expr.linear_terms().SortedKeys()) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
ostr << " + ";
|
||||
}
|
||||
ostr << expr.linear_terms().at(v) << "*" << v;
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
ostr << " + ";
|
||||
}
|
||||
ostr << expr.offset();
|
||||
|
||||
return ostr;
|
||||
}
|
||||
|
||||
#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS
|
||||
QuadraticExpression::QuadraticExpression() { ++num_calls_default_constructor_; }
|
||||
|
||||
QuadraticExpression::QuadraticExpression(const QuadraticExpression& other)
|
||||
: quadratic_terms_(other.quadratic_terms_),
|
||||
linear_terms_(other.linear_terms_),
|
||||
offset_(other.offset_) {
|
||||
++num_calls_copy_constructor_;
|
||||
}
|
||||
|
||||
QuadraticExpression::QuadraticExpression(QuadraticExpression&& other)
|
||||
: quadratic_terms_(std::move(other.quadratic_terms_)),
|
||||
linear_terms_(std::move(other.linear_terms_)),
|
||||
offset_(std::exchange(other.offset_, 0.0)) {
|
||||
++num_calls_move_constructor_;
|
||||
}
|
||||
|
||||
QuadraticExpression& QuadraticExpression::operator=(
|
||||
const QuadraticExpression& other) {
|
||||
quadratic_terms_ = other.quadratic_terms_;
|
||||
linear_terms_ = other.linear_terms_;
|
||||
offset_ = other.offset_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ABSL_CONST_INIT thread_local int
|
||||
QuadraticExpression::num_calls_default_constructor_ = 0;
|
||||
ABSL_CONST_INIT thread_local int
|
||||
QuadraticExpression::num_calls_copy_constructor_ = 0;
|
||||
ABSL_CONST_INIT thread_local int
|
||||
QuadraticExpression::num_calls_move_constructor_ = 0;
|
||||
ABSL_CONST_INIT thread_local int
|
||||
QuadraticExpression::num_calls_initializer_list_constructor_ = 0;
|
||||
ABSL_CONST_INIT thread_local int
|
||||
QuadraticExpression::num_calls_linear_expression_constructor_ = 0;
|
||||
|
||||
void QuadraticExpression::ResetCounters() {
|
||||
num_calls_default_constructor_ = 0;
|
||||
num_calls_copy_constructor_ = 0;
|
||||
num_calls_move_constructor_ = 0;
|
||||
num_calls_initializer_list_constructor_ = 0;
|
||||
num_calls_linear_expression_constructor_ = 0;
|
||||
}
|
||||
#endif // MATH_OPT_USE_EXPRESSION_COUNTERS
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,9 +44,6 @@ absl::Status IsSupported(const MPModelProto& model) {
|
||||
if (model.general_constraint_size() > 0) {
|
||||
return absl::InvalidArgumentError("General constraints are not supported");
|
||||
}
|
||||
if (model.quadratic_objective().coefficient_size() > 0) {
|
||||
return absl::InvalidArgumentError("Quadratic objectives not supported");
|
||||
}
|
||||
if (model.solution_hint().var_index_size() > 0) {
|
||||
return absl::InvalidArgumentError("Solution Hint not supported");
|
||||
}
|
||||
@@ -85,7 +82,7 @@ MPModelProtoToMathOptModel(const ::operations_research::MPModelProto& model) {
|
||||
output.set_name(model.name());
|
||||
|
||||
math_opt::VariablesProto* const vars = output.mutable_variables();
|
||||
int objective_non_zeros = 0;
|
||||
int linear_objective_non_zeros = 0;
|
||||
const int num_vars = model.variable_size();
|
||||
const bool vars_have_name = AnyVarNamed(model);
|
||||
vars->mutable_lower_bounds()->Reserve(num_vars);
|
||||
@@ -97,7 +94,7 @@ MPModelProtoToMathOptModel(const ::operations_research::MPModelProto& model) {
|
||||
for (int i = 0; i < model.variable_size(); ++i) {
|
||||
const MPVariableProto& var = model.variable(i);
|
||||
if (var.objective_coefficient() != 0.0) {
|
||||
++objective_non_zeros;
|
||||
++linear_objective_non_zeros;
|
||||
}
|
||||
vars->add_ids(i);
|
||||
vars->add_lower_bounds(var.lower_bound());
|
||||
@@ -109,11 +106,11 @@ MPModelProtoToMathOptModel(const ::operations_research::MPModelProto& model) {
|
||||
}
|
||||
|
||||
math_opt::ObjectiveProto* const objective = output.mutable_objective();
|
||||
if (objective_non_zeros > 0) {
|
||||
if (linear_objective_non_zeros > 0) {
|
||||
objective->mutable_linear_coefficients()->mutable_ids()->Reserve(
|
||||
objective_non_zeros);
|
||||
linear_objective_non_zeros);
|
||||
objective->mutable_linear_coefficients()->mutable_values()->Reserve(
|
||||
objective_non_zeros);
|
||||
linear_objective_non_zeros);
|
||||
for (int j = 0; j < num_vars; ++j) {
|
||||
const double value = model.variable(j).objective_coefficient();
|
||||
if (value == 0.0) continue;
|
||||
@@ -121,6 +118,39 @@ MPModelProtoToMathOptModel(const ::operations_research::MPModelProto& model) {
|
||||
objective->mutable_linear_coefficients()->add_values(value);
|
||||
}
|
||||
}
|
||||
const MPQuadraticObjective& origin_qp_terms = model.quadratic_objective();
|
||||
const int num_qp_terms = origin_qp_terms.coefficient().size();
|
||||
if (num_qp_terms > 0) {
|
||||
// ObjectiveProto requires three things that may not be satisfied by
|
||||
// MPQuadraticObjective:
|
||||
// 1. No duplicate entries
|
||||
// 2. No lower triangular entries
|
||||
// 3. Lexicographic sortedness of (row_id, column_id) keys
|
||||
std::vector<std::pair<std::pair<int, int>, double>> qp_terms_in_order;
|
||||
for (int k = 0; k < num_qp_terms; ++k) {
|
||||
int first_index = origin_qp_terms.qvar1_index(k);
|
||||
int second_index = origin_qp_terms.qvar2_index(k);
|
||||
if (first_index > second_index) {
|
||||
std::swap(first_index, second_index);
|
||||
}
|
||||
qp_terms_in_order.emplace_back(std::make_pair(first_index, second_index),
|
||||
origin_qp_terms.coefficient(k));
|
||||
}
|
||||
std::sort(qp_terms_in_order.begin(), qp_terms_in_order.end());
|
||||
SparseDoubleMatrixProto& destination_qp_terms =
|
||||
*objective->mutable_quadratic_coefficients();
|
||||
std::pair<int, int> previous = {-1, -1};
|
||||
for (const auto& [indices, coeff] : qp_terms_in_order) {
|
||||
if (indices == previous) {
|
||||
*destination_qp_terms.mutable_coefficients()->rbegin() += coeff;
|
||||
} else {
|
||||
destination_qp_terms.add_row_ids(indices.first);
|
||||
destination_qp_terms.add_column_ids(indices.second);
|
||||
destination_qp_terms.add_coefficients(coeff);
|
||||
previous = indices;
|
||||
}
|
||||
}
|
||||
}
|
||||
objective->set_maximize(model.maximize());
|
||||
objective->set_offset(model.objective_offset());
|
||||
|
||||
@@ -164,8 +194,8 @@ MPModelProtoToMathOptModel(const ::operations_research::MPModelProto& model) {
|
||||
}
|
||||
std::sort(terms_in_order.begin(), terms_in_order.end());
|
||||
for (const auto& term : terms_in_order) {
|
||||
matrix->add_column_ids(i);
|
||||
matrix->add_row_ids(term.first);
|
||||
matrix->add_row_ids(i);
|
||||
matrix->add_column_ids(term.first);
|
||||
matrix->add_coefficients(term.second);
|
||||
}
|
||||
terms_in_order.clear();
|
||||
@@ -221,6 +251,17 @@ absl::StatusOr<::operations_research::MPModelProto> MathOptModelToMPModelProto(
|
||||
MPVariableProto* const variable = output.mutable_variable(var_position);
|
||||
variable->set_objective_coefficient(coef);
|
||||
}
|
||||
const SparseDoubleMatrixProto& origin_qp_terms =
|
||||
model.objective().quadratic_coefficients();
|
||||
MPQuadraticObjective& destination_qp_terms =
|
||||
*output.mutable_quadratic_objective();
|
||||
for (int k = 0; k < origin_qp_terms.coefficients().size(); ++k) {
|
||||
destination_qp_terms.add_qvar1_index(
|
||||
variable_id_to_mp_position[origin_qp_terms.row_ids(k)]);
|
||||
destination_qp_terms.add_qvar2_index(
|
||||
variable_id_to_mp_position[origin_qp_terms.column_ids(k)]);
|
||||
destination_qp_terms.add_coefficient(origin_qp_terms.coefficients(k));
|
||||
}
|
||||
|
||||
// TODO(user): use the constraint iterator from scip_solver.cc here.
|
||||
const int constraint_non_zeros =
|
||||
|
||||
@@ -18,21 +18,20 @@
|
||||
#include "ortools/linear_solver/linear_solver.pb.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
namespace operations_research::math_opt {
|
||||
|
||||
absl::StatusOr<::operations_research::math_opt::ModelProto>
|
||||
MPModelProtoToMathOptModel(const ::operations_research::MPModelProto& model);
|
||||
// Returns a ModelProto equivalent to the input linear_solver Model.
|
||||
absl::StatusOr<ModelProto> MPModelProtoToMathOptModel(
|
||||
const MPModelProto& model);
|
||||
|
||||
// Returns a MPModelProto equivalent to the input math_opt Model.
|
||||
// Returns a linear_solver MPModelProto equivalent to the input math_opt Model.
|
||||
//
|
||||
// Variables are created in the same order as they appear in
|
||||
// `model.variables`. Hence the returned `.variable(i)` corresponds to input
|
||||
// `model.variables.ids(i)`.
|
||||
absl::StatusOr<::operations_research::MPModelProto> MathOptModelToMPModelProto(
|
||||
const ::operations_research::math_opt::ModelProto& model);
|
||||
absl::StatusOr<MPModelProto> MathOptModelToMPModelProto(
|
||||
const ModelProto& model);
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
} // namespace operations_research::math_opt
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_IO_PROTO_CONVERTER_H_
|
||||
|
||||
@@ -18,6 +18,9 @@ package operations_research.math_opt;
|
||||
|
||||
import "ortools/math_opt/sparse_containers.proto";
|
||||
|
||||
option java_package = "com.google.ortools.mathopt";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// As used below, we define "#variables" = size(VariablesProto.ids).
|
||||
message VariablesProto {
|
||||
// Must be nonnegative and strictly increasing.
|
||||
@@ -50,7 +53,21 @@ message ObjectiveProto {
|
||||
// * linear_coefficients.values can be zero, but this just wastes space.
|
||||
SparseDoubleVectorProto linear_coefficients = 3;
|
||||
|
||||
// TODO(user): add support for a quadratic objective term.
|
||||
// Objective terms that are quadratic in the decision variables.
|
||||
//
|
||||
// Requirements in addition to those on SparseDoubleMatrixProto messages:
|
||||
// * Each element of quadratic_coefficients.row_ids and each element of
|
||||
// quadratic_coefficients.column_ids must be an element of
|
||||
// VariablesProto.ids.
|
||||
// * The matrix must be upper triangular: for each i,
|
||||
// quadratic_coefficients.row_ids[i] <=
|
||||
// quadratic_coefficients.column_ids[i].
|
||||
//
|
||||
// Notes:
|
||||
// * Terms not explicitly stored have zero coefficient.
|
||||
// * Elements of quadratic_coefficients.coefficients can be zero, but this
|
||||
// just wastes space.
|
||||
SparseDoubleMatrixProto quadratic_coefficients = 4;
|
||||
}
|
||||
|
||||
// As used below, we define "#linear constraints" =
|
||||
|
||||
@@ -20,6 +20,25 @@ package operations_research.math_opt;
|
||||
import "ortools/math_opt/solution.proto";
|
||||
import "ortools/math_opt/sparse_containers.proto";
|
||||
|
||||
option java_package = "com.google.ortools.mathopt";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// TODO(b/183616124): Add dual_values/reduced_cost hints and hint-priorities
|
||||
// to variable_values.
|
||||
|
||||
// Initial solution hint for warm starting a solver. This can be a full solution
|
||||
// (all values specified) or a partial solution (only some values specified). In
|
||||
// addition, a full solution does not need to be feasible. The solver may try to
|
||||
// complete a partial solution or to repair a full solution that is infeasible.
|
||||
message SolutionHintProto {
|
||||
// A possibly partial assignment of values to the primal variables of the
|
||||
// problem. The solver-independent requirements for this sub-message are:
|
||||
// * variable_values.ids are elements of VariablesProto.ids.
|
||||
// * variable_values.values must all be finite.
|
||||
SparseDoubleVectorProto variable_values = 1;
|
||||
}
|
||||
|
||||
// TODO(b/183628247): follow naming convention in fields below.
|
||||
// Parameters to control a single solve that that are specific to the input
|
||||
// model (see SolveParametersProto for model independent parameters).
|
||||
message ModelSolveParametersProto {
|
||||
@@ -29,7 +48,7 @@ message ModelSolveParametersProto {
|
||||
//
|
||||
// Requirements:
|
||||
// * filtered_ids are elements of VariablesProto.ids.
|
||||
SparseVectorFilterProto primal_variables_filter = 1;
|
||||
SparseVectorFilterProto variable_values_filter = 1;
|
||||
|
||||
// Filter that is applied to all returned sparse containers keyed by linear
|
||||
// constraints in DualSolutionProto and DualRay
|
||||
@@ -37,7 +56,7 @@ message ModelSolveParametersProto {
|
||||
//
|
||||
// Requirements:
|
||||
// * filtered_ids are elements of LinearConstraints.ids.
|
||||
SparseVectorFilterProto dual_linear_constraints_filter = 2;
|
||||
SparseVectorFilterProto dual_values_filter = 2;
|
||||
|
||||
// Filter that is applied to all returned sparse containers keyed by variables
|
||||
// in DualSolutionProto and DualRay (DualSolutionProto.reduced_costs,
|
||||
@@ -45,12 +64,28 @@ message ModelSolveParametersProto {
|
||||
//
|
||||
// Requirements:
|
||||
// * filtered_ids are elements of VariablesProto.ids.
|
||||
SparseVectorFilterProto dual_variables_filter = 3;
|
||||
SparseVectorFilterProto reduced_costs_filter = 3;
|
||||
|
||||
// Optional initial basis for warm starting simplex LP solvers. If set, it is
|
||||
// expected to be valid according to `ValidateBasis` in
|
||||
// `validators/solution_validator.h` for the current `ModelSummary`.
|
||||
BasisProto initial_basis = 4;
|
||||
|
||||
// TODO(b/183616124): Support hint and branching priorities.
|
||||
// TODO(b/183616124): Add multiple solution start support for Gurobi/GSCIP and
|
||||
// add associated tests.
|
||||
|
||||
// Optional solution hints. If set, they are expected to be valid according to
|
||||
// the message description above or equivalently according to
|
||||
// `ValidateSolutionHint` in `validators/model_parameters_validator.cc` for
|
||||
// the current `ModelSummary`.
|
||||
repeated SolutionHintProto solution_hints = 5;
|
||||
|
||||
// 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 (usualy zero).
|
||||
//
|
||||
// Requirements:
|
||||
// * branching_priorities.values must be finite.
|
||||
// * branching_priorities.ids must be elements of VariablesProto.ids.
|
||||
SparseInt32VectorProto branching_priorities = 6;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ package operations_research.math_opt;
|
||||
import "ortools/math_opt/model.proto";
|
||||
import "ortools/math_opt/sparse_containers.proto";
|
||||
|
||||
option java_package = "com.google.ortools.mathopt";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// Updates to existing variables in a ModelProto.
|
||||
//
|
||||
// Applies only to existing variables in a model, for new variables, see
|
||||
@@ -66,6 +69,24 @@ message ObjectiveUpdatesProto {
|
||||
// * The value 0.0 removes a variable from the linear objective. This
|
||||
// value should only be used for existing variables.
|
||||
SparseDoubleVectorProto linear_coefficients = 3;
|
||||
|
||||
// Updates ModelProto.objective.quadratic_coefficients
|
||||
//
|
||||
// Requirements in addition to those on SparseDoubleMatrixProto messages:
|
||||
// * Each element of quadratic_coefficients.row_ids and each element of
|
||||
// quadratic_coefficients.column_ids must be a variable id, either an
|
||||
// existing one (from ModelProto.variables.ids) or a new one (from
|
||||
// ModelUpdateProto.new_variables.ids).
|
||||
// * The matrix must be upper triangular: for each i,
|
||||
// quadratic_coefficients.row_ids[i] <=
|
||||
// quadratic_coefficients.column_ids[i].
|
||||
//
|
||||
// Notes:
|
||||
// * Unset values are unchanged.
|
||||
// * The value 0.0 removes a quadratic term (i.e. product of two variables)
|
||||
// from the quadratic objective. This value should only be used for
|
||||
// existing quadratic terms appearing in the objective.
|
||||
SparseDoubleMatrixProto quadratic_coefficients = 4;
|
||||
}
|
||||
|
||||
// Updates to existing linear constraints in a ModelProto.
|
||||
@@ -113,14 +134,13 @@ message ModelUpdateProto {
|
||||
LinearConstraintUpdatesProto linear_constraint_updates = 4;
|
||||
|
||||
// Add new variables to the model. All new_variables.ids must be greater than
|
||||
// the existing model's largest variable id. All nonempty names should be
|
||||
// distinct from existing names. TODO(b/169575522): we may relax this.
|
||||
// any ids used in the initial model and previous updates. All nonempty names
|
||||
// should be distinct from existing names.
|
||||
VariablesProto new_variables = 5;
|
||||
|
||||
// Add new linear constraints to the model. All new_linear_constraints.ids
|
||||
// must be greater than the existing model's largest linear constraints id.
|
||||
// All nonempty names should be distinct from existing names.
|
||||
// TODO(b/169575522): we may relax this.
|
||||
// must be greater than any ids used in the initial model and previous
|
||||
// updates. All nonempty names should be distinct from existing names.
|
||||
LinearConstraintsProto new_linear_constraints = 6;
|
||||
|
||||
// Updates the objective, both for existing and new variables.
|
||||
|
||||
@@ -17,28 +17,82 @@ syntax = "proto3";
|
||||
package operations_research.math_opt;
|
||||
|
||||
import "google/protobuf/duration.proto";
|
||||
import "ortools/math_opt/solvers/gurobi.proto";
|
||||
|
||||
|
||||
option java_package = "com.google.ortools.mathopt";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "ortools/glop/parameters.proto";
|
||||
import "ortools/gscip/gscip.proto";
|
||||
import "ortools/sat/sat_parameters.proto";
|
||||
|
||||
enum SolverType {
|
||||
enum SolverTypeProto {
|
||||
SOLVER_TYPE_UNSPECIFIED = 0;
|
||||
|
||||
// Solving Constraint Integer Programs (SCIP) solver.
|
||||
//
|
||||
// It supports both MIPs and LPs. No dual data for LPs is returned though. To
|
||||
// solve LPs, SOLVER_TYPE_GLOP should be preferred.
|
||||
SOLVER_TYPE_GSCIP = 1;
|
||||
|
||||
// Gurobi solver.
|
||||
//
|
||||
// It supports both MIPs and LPs.
|
||||
SOLVER_TYPE_GUROBI = 2;
|
||||
|
||||
// Google's Glop linear solver.
|
||||
//
|
||||
// It only solves LPs.
|
||||
SOLVER_TYPE_GLOP = 3;
|
||||
|
||||
// Google's CP-SAT solver.
|
||||
//
|
||||
// It supports solving IPs and can scale MIPs to solve them as IPs.
|
||||
SOLVER_TYPE_CP_SAT = 4;
|
||||
|
||||
|
||||
// GNU Linear Programming Kit (GLPK).
|
||||
//
|
||||
// It supports both MIPs and LPs.
|
||||
//
|
||||
// Thread-safety: GLPK use thread-local storage for memory allocations. As a
|
||||
// consequence Solver instances must be destroyed on the same thread as they
|
||||
// are created or GLPK will crash. It seems OK to call Solver::Solve() from
|
||||
// another thread than the one used to create the Solver but it is not
|
||||
// documented by GLPK and should be avoided.
|
||||
//
|
||||
// When solving a LP with the presolver, a solution (and the unbound rays) are
|
||||
// only returned if an optimal solution has been found. Else nothing is
|
||||
// returned. See glpk-5.0/doc/glpk.pdf page #40 available from glpk-5.0.tar.gz
|
||||
// for details.
|
||||
SOLVER_TYPE_GLPK = 6;
|
||||
|
||||
}
|
||||
|
||||
enum LPAlgorithm {
|
||||
// Selects an algorithm for solving linear programs.
|
||||
enum LPAlgorithmProto {
|
||||
LP_ALGORITHM_UNSPECIFIED = 0;
|
||||
|
||||
// The (primal) simplex method. Typically can provide primal and dual
|
||||
// solutions, primal/dual rays on primal/dual unbounded problems, and a basis.
|
||||
LP_ALGORITHM_PRIMAL_SIMPLEX = 1;
|
||||
|
||||
// The dual simplex method. Typically can provide primal and dual
|
||||
// solutions, primal/dual rays on primal/dual unbounded problems, and a basis.
|
||||
LP_ALGORITHM_DUAL_SIMPLEX = 2;
|
||||
|
||||
// The barrier method, also commonly called an interior point method (IPM).
|
||||
// Can typically give both primal and dual solutions. Some implementations can
|
||||
// also produce rays on unbounded/infeasible problems. A basis is not given
|
||||
// unless the underlying solver does "crossover" and finishes with simplex.
|
||||
LP_ALGORITHM_BARRIER = 3;
|
||||
}
|
||||
|
||||
// How these are mapped onto underlying solvers:
|
||||
// Effort level applied to an optional task while solving (see
|
||||
// SolveParametersProto for use).
|
||||
//
|
||||
// Emphasis is used to configure a solver feature as follows:
|
||||
// * If a solver doesn't support the feature, only UNSPECIFIED and OFF are
|
||||
// valid, any other setting will give either a warning or error (as
|
||||
// configured for Strictness).
|
||||
@@ -49,7 +103,7 @@ enum LPAlgorithm {
|
||||
// mapped to MEDIUM.
|
||||
// - If the feature is supported, LOW, MEDIUM, HIGH, and VERY HIGH will never
|
||||
// give a warning or error, and will map onto their best match.
|
||||
enum Emphasis {
|
||||
enum EmphasisProto {
|
||||
EMPHASIS_UNSPECIFIED = 0;
|
||||
EMPHASIS_OFF = 1;
|
||||
EMPHASIS_LOW = 2;
|
||||
@@ -58,30 +112,67 @@ enum Emphasis {
|
||||
EMPHASIS_VERY_HIGH = 5;
|
||||
}
|
||||
|
||||
// Configures if potentially bad solver input is a warning or an error.
|
||||
message StrictnessProto {
|
||||
// If true, warnings on bad parameters are converted to Status errors.
|
||||
bool bad_parameter = 1;
|
||||
}
|
||||
|
||||
message CommonSolveParametersProto {
|
||||
StrictnessProto strictness = 1;
|
||||
// This message contains solver specific data that are used when the solver is
|
||||
// instantiated.
|
||||
message SolverInitializerProto {
|
||||
GurobiInitializerProto gurobi = 1;
|
||||
}
|
||||
|
||||
// Parameters to control a single solve.
|
||||
//
|
||||
// Contains both parameters common to all solvers e.g. time_limit, and
|
||||
// parameters for a specific solver, e.g. gscip. If a value is set in both
|
||||
// common and solver specific field, the solver specific setting is used.
|
||||
//
|
||||
// The common parameters that are optional and unset or an enum with value
|
||||
// unspecified indicate that the solver default is used.
|
||||
//
|
||||
// Solver specific parameters for solvers other than the one in use are ignored.
|
||||
//
|
||||
// Parameters that depends on the model (e.g. branching priority is set for
|
||||
// each variable) are passed in ModelSolveParametersProto.
|
||||
message SolveParametersProto {
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Parameters common to all solvers.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Maximum time a solver should spend on the problem (or infinite if not set).
|
||||
//
|
||||
// This value is not a hard limit, solve time may slightly exceed this value.
|
||||
// This parameter is always passed to the underlying solver, the solver
|
||||
// default is not used.
|
||||
google.protobuf.Duration time_limit = 1;
|
||||
|
||||
// Limit on the iterations of the underlying algorithm (e.g. simplex pivots).
|
||||
// The specific behavior is dependent on the solver and algorithm used, but
|
||||
// should result in a deterministic solve limit.
|
||||
// TODO(b/195295177): suggest node_limit as an alternative when it's added
|
||||
optional int64 iteration_limit = 2;
|
||||
|
||||
// Optimality tolerances (primarily) for MIP solvers. The absolute GAP of a
|
||||
// feasible solution is the distance between its objective value and a dual
|
||||
// bound (e.g. an upper bound on the optimal value for maximization problems).
|
||||
// The relative GAP is a solver-dependent scaled version of the absolute GAP
|
||||
// (e.g. it could be the relative GAP divided by the objective value of the
|
||||
// feasible solution if this is non-zero). Solvers consider a solution optimal
|
||||
// if its GAPs are below these limits (most solvers use both versions).
|
||||
optional double relative_gap_limit = 17;
|
||||
optional double absolute_gap_limit = 18;
|
||||
|
||||
// Enables printing the solver implementation traces. The location of those
|
||||
// traces depend on the solver. For SCIP and Gurobi this will be the standard
|
||||
// output streams. For Glop and CP-SAT this will LOG(INFO).
|
||||
//
|
||||
// When not set, the default solver behavior is used, which can be enabled or
|
||||
// disabled.
|
||||
//
|
||||
// Note that if the solver supports CALLBACK_EVENT_MESSAGE and the user
|
||||
// registers a callback for it, then this parameter value is ignored and no
|
||||
// traces are printed. The traces are only available through the
|
||||
// CallbackDataProto.
|
||||
optional bool enable_output = 2;
|
||||
|
||||
// If not set, the time limit is infinite. This parameter is always passed
|
||||
// to the underlying solver.
|
||||
google.protobuf.Duration time_limit = 3;
|
||||
// Note that if the solver supports message callback and the user registers a
|
||||
// callback for it, then this parameter value is ignored and no traces are
|
||||
// printed.
|
||||
bool enable_output = 3;
|
||||
|
||||
// If set, it must be >= 1.
|
||||
optional int32 threads = 4;
|
||||
@@ -101,58 +192,45 @@ message CommonSolveParametersProto {
|
||||
// MAX(0, MIN(MAX_VALID_VALUE_FOR_SOLVER, random_seed)).
|
||||
optional int32 random_seed = 5;
|
||||
|
||||
// If unspecified, used the solver default algorithm.
|
||||
LPAlgorithm lp_algorithm = 6;
|
||||
// The algorithm for solving a linear program. If LP_ALGORITHM_UNSPECIFIED,
|
||||
// use the solver default algorithm.
|
||||
//
|
||||
// For problems that are not linear programs but where linear programming is
|
||||
// a subroutine, solvers may use this value. E.g. MIP solvers will typically
|
||||
// use this for the root LP solve only (and use dual simplex otherwise).
|
||||
LPAlgorithmProto lp_algorithm = 6;
|
||||
|
||||
Emphasis presolve = 7;
|
||||
// Effort on simplifying the problem before starting the main algorithm, or
|
||||
// the solver default effort level if EMPHASIS_UNSPECIFIED.
|
||||
EmphasisProto presolve = 7;
|
||||
|
||||
// Effort on getting a stronger LP relaxation (MIP only), or the solver
|
||||
// default effort level if EMPHASIS_UNSPECIFIED.
|
||||
//
|
||||
// NOTE: disabling cuts may prevent callbacks from having a chance to add cuts
|
||||
// at MIP_NODE, this behavior is solver specific.
|
||||
Emphasis cuts = 8;
|
||||
Emphasis heuristics = 9;
|
||||
Emphasis scaling = 10;
|
||||
}
|
||||
EmphasisProto cuts = 8;
|
||||
|
||||
// This message contains solver specific data that are used when the solver is
|
||||
// instantiated.
|
||||
message SolverInitializerProto {}
|
||||
// Effort in finding feasible solutions beyond those encountered in the
|
||||
// complete search procedure (MIP only), or the solver default effort level if
|
||||
// EMPHASIS_UNSPECIFIED.
|
||||
EmphasisProto heuristics = 9;
|
||||
|
||||
// Gurobi's parameters have types (int, double, string), but they also support
|
||||
// a simpler interface through
|
||||
// `GRBsetparam(GRBenv* env,
|
||||
// const char* paramname,
|
||||
// const char* value)`
|
||||
//
|
||||
// Moreover, Gurobi also has a long list of `private` and `extended`
|
||||
// parameters, which are better handled through this generic interface. Given
|
||||
// these constraints, we store parameter changes as a sequence of strings of
|
||||
// the form "paramname=value".
|
||||
//
|
||||
// Note that final behavior is order-dependent of the sequence of parameters
|
||||
// used, so we apply parameter changes one at a time. Note that when merging
|
||||
// Gurobi parameters with common solver parameters, the common parameters will
|
||||
// be pre-pended to the list of Gurobi parameters.
|
||||
message GurobiParametersProto {
|
||||
message Parameter {
|
||||
string name = 1;
|
||||
string value = 2;
|
||||
}
|
||||
repeated Parameter parameters = 1;
|
||||
}
|
||||
// Effort in rescaling the problem to improve numerical stability, or the
|
||||
// solver default effort level if EMPHASIS_UNSPECIFIED.
|
||||
EmphasisProto scaling = 10;
|
||||
|
||||
// Parameters to control a single solve.
|
||||
//
|
||||
// Parameters that depends on the model (parameters about variables, ...) are
|
||||
// passed in ModelSolveParametersProto proto.
|
||||
message SolveParametersProto {
|
||||
CommonSolveParametersProto common_parameters = 1;
|
||||
// Values in solver_specific_parameters may overlap with values in
|
||||
// common_parameters. In that case, the value in solver_specific_parameters is
|
||||
// the one taken into account.
|
||||
oneof solver_specific_parameters {
|
||||
GScipParameters gscip_parameters = 2;
|
||||
GurobiParametersProto gurobi_parameters = 3;
|
||||
glop.GlopParameters glop_parameters = 4;
|
||||
sat.SatParameters cp_sat_parameters = 5;
|
||||
}
|
||||
reserved 6;
|
||||
// TODO(b/196132970): this needs to move into SolverInitializerProto.
|
||||
StrictnessProto strictness = 11;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Solver specific parameters
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
GScipParameters gscip = 12;
|
||||
GurobiParametersProto gurobi = 13;
|
||||
glop.GlopParameters glop = 14;
|
||||
sat.SatParameters cp_sat = 15;
|
||||
reserved 16;
|
||||
|
||||
reserved 19;
|
||||
}
|
||||
|
||||
@@ -20,37 +20,208 @@ import "google/protobuf/duration.proto";
|
||||
import "ortools/gscip/gscip.proto";
|
||||
import "ortools/math_opt/solution.proto";
|
||||
|
||||
// best_dual_bound should always be better (e.g. smaller for minimization) than
|
||||
// best_primal_bound.
|
||||
option java_package = "com.google.ortools.mathopt";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// Problem feasibility status as claimed by the solver (solver is not required
|
||||
// to return a certificate for the claim).
|
||||
enum FeasibilityStatusProto {
|
||||
// Guard value representing no status.
|
||||
FEASIBILITY_STATUS_UNSPECIFIED = 0;
|
||||
|
||||
// Solver does not claim a status.
|
||||
FEASIBILITY_STATUS_UNDETERMINED = 1;
|
||||
|
||||
// Solver claims the problem is feasible.
|
||||
FEASIBILITY_STATUS_FEASIBLE = 2;
|
||||
|
||||
// Solver claims the problem is infeasible.
|
||||
FEASIBILITY_STATUS_INFEASIBLE = 3;
|
||||
}
|
||||
|
||||
// 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
|
||||
// status gives a comprehensive description of a solver's claims about
|
||||
// feasibility and unboundedness of the solved problem. For instance,
|
||||
//
|
||||
// We only report nontrivial (finite) values for best_dual_bound and
|
||||
// best_primal_bound if the underlying solver claims to have such a bound. Some
|
||||
// solvers (e.g. bisco, typically continuous solvers) do not claim a bound
|
||||
// even when returning optimal (these methods can terminate without a basis and
|
||||
// slightly infeasible primal and dual solutions). MIP solvers will typically
|
||||
// report a bound even if their LP solutions are imprecise.
|
||||
// * 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
|
||||
// feasible status may imply the existence of an optimal solution, it does not
|
||||
// guarantee the solver has actually found such optimal solution.
|
||||
message ProblemStatusProto {
|
||||
// Status for the primal problem.
|
||||
FeasibilityStatusProto primal_status = 1;
|
||||
|
||||
// Status for the dual problem (or for the dual of a continuous relaxation).
|
||||
FeasibilityStatusProto dual_status = 2;
|
||||
|
||||
// If true, the solver claims the primal or dual problem is infeasible, but
|
||||
// it does not know which (or if both are infeasible). Can be true only when
|
||||
// primal_problem_status = dual_problem_status = kUndetermined. This extra
|
||||
// information is often needed when preprocessing determines there is no
|
||||
// optimal solution to the problem (but can't determine if it is due to
|
||||
// infeasibility, unboundedness, or both).
|
||||
bool primal_or_dual_infeasible = 3;
|
||||
}
|
||||
|
||||
message SolveStatsProto {
|
||||
// Elapsed wall clock time as measured by math_opt, roughly the time inside
|
||||
// Solver::Solve(). Note: this does not include work done building the model.
|
||||
google.protobuf.Duration solve_time = 1;
|
||||
|
||||
// When no bound is found by the solver, the trivial bound (+inf for
|
||||
// minimization and -inf maximizaiton) is given.
|
||||
//
|
||||
// Note that we can have a primal bound even when we have no feasible
|
||||
// solution, and that the primal bound can better than the best feasible
|
||||
// solution.
|
||||
// Solver claims the optimal value is equal or better (smaller for
|
||||
// minimization and larger for maximization) than best_primal_bound:
|
||||
// * best_primal_bound is trivial (+inf for minimization and -inf
|
||||
// maximization) when the solver does not claim to have such bound. This
|
||||
// may happen for some solvers (e.g. bisco, typically continuous solvers)
|
||||
// even when returning optimal (solver could terminate with slightly
|
||||
// infeasible primal solutions).
|
||||
// * 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.
|
||||
double best_primal_bound = 2;
|
||||
|
||||
// The best proven bound on the object (e.g. through the LP relaxation). When
|
||||
// no bound is found, the trivial bound (-inf minimization and +inf for
|
||||
// maximization) is given.
|
||||
//
|
||||
// Always better than (e.g. for minimization, smaller than) best_primal_bound.
|
||||
// Solver claims the optimal value is equal or worse (larger for
|
||||
// minimization and smaller for maximization) than best_dual_bound:
|
||||
// * best_dual_bound is always better (smaller for minimization and larger
|
||||
// for maximization) than best_primal_bound.
|
||||
// * 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.
|
||||
double best_dual_bound = 3;
|
||||
int64 simplex_iterations = 4;
|
||||
int64 barrier_iterations = 5;
|
||||
int64 node_count = 6;
|
||||
|
||||
// Feasibility statuses for primal and dual problems.
|
||||
ProblemStatusProto problem_status = 4;
|
||||
|
||||
int64 simplex_iterations = 5;
|
||||
|
||||
int64 barrier_iterations = 6;
|
||||
|
||||
int64 node_count = 7;
|
||||
}
|
||||
|
||||
// The reason a call to Solve() terminates.
|
||||
enum TerminationReasonProto {
|
||||
TERMINATION_REASON_UNSPECIFIED = 0;
|
||||
|
||||
// A provably optimal solution (up to numerical tolerances) has been found.
|
||||
TERMINATION_REASON_OPTIMAL = 1;
|
||||
|
||||
// The primal problem has no feasible solutions.
|
||||
TERMINATION_REASON_INFEASIBLE = 2;
|
||||
|
||||
// The primal problem is feasible and arbitrarily good solutions can be
|
||||
// found along a primal ray.
|
||||
TERMINATION_REASON_UNBOUNDED = 3;
|
||||
|
||||
// The primal problem is either infeasible or unbounded. More details on the
|
||||
// problem status may be available in solve_stats.problem_status. Note that
|
||||
// Gurobi's unbounded status may be mapped here as explained in
|
||||
// go/mathopt-solver-specific#gurobi-inf-or-unb.
|
||||
TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED = 4;
|
||||
|
||||
// The problem was solved to one of the criteria above (Optimal, Infeasible,
|
||||
// Unbounded, or InfeasibleOrUnbounded), but one or more tolerances was not
|
||||
// met. Some primal/dual solutions/rays be present, but either they will be
|
||||
// slightly infeasible, or (if the problem was nearly optimal) their may be
|
||||
// a gap between the best solution objective and best objective bound.
|
||||
//
|
||||
// Users can still query primal/dual solutions/rays and solution stats, but
|
||||
// they are responsible for dealing with the numerical imprecision.
|
||||
TERMINATION_REASON_IMPRECISE = 5;
|
||||
|
||||
// The optimizer reached some kind of limit. Partial solution information
|
||||
// may be available. See SolveResultProto.limit_detail for more detail.
|
||||
TERMINATION_REASON_LIMIT_REACHED = 6;
|
||||
|
||||
// The algorithm stopped because it encountered unrecoverable numerical
|
||||
// error. No solution information is available.
|
||||
TERMINATION_REASON_NUMERICAL_ERROR = 7;
|
||||
|
||||
// The algorithm stopped because of an error not covered by one of the
|
||||
// statuses defined above. No solution information is available.
|
||||
TERMINATION_REASON_OTHER_ERROR = 8;
|
||||
}
|
||||
|
||||
// When a Solve() stops early with TerminationReasonProto LIMIT_REACHED, the
|
||||
// specific limit that was hit.
|
||||
enum LimitProto {
|
||||
// Used as a null value when we terminated not from a limit (e.g.
|
||||
// TERMINATION_REASON_OPTIMAL).
|
||||
LIMIT_UNSPECIFIED = 0;
|
||||
|
||||
// The underlying solver does not expose which limit was reached.
|
||||
LIMIT_UNDETERMINED = 1;
|
||||
|
||||
// An iterative algorithm stopped after conducting the maximum number of
|
||||
// iterations (e.g. simplex or barrier iterations).
|
||||
LIMIT_ITERATION = 2;
|
||||
|
||||
// The algorithm stopped after a user-specified computation time.
|
||||
LIMIT_TIME = 3;
|
||||
|
||||
// A branch-and-bound algorithm stopped because it explored a maximum number
|
||||
// of nodes in the branch-and-bound tree.
|
||||
LIMIT_NODE = 4;
|
||||
|
||||
// The algorithm stopped because it found the required number of solutions.
|
||||
// This is often used in MIPs to get the solver to return the first feasible
|
||||
// solution it encounters.
|
||||
LIMIT_SOLUTION = 5;
|
||||
|
||||
// The algorithm stopped because it ran out of memory.
|
||||
LIMIT_MEMORY = 6;
|
||||
|
||||
// The algorithm stopped because it found a solution better than a minimum
|
||||
// limit set by the user.
|
||||
LIMIT_OBJECTIVE = 7;
|
||||
|
||||
// The algorithm stopped because the norm of an iterate became too large.
|
||||
LIMIT_NORM = 8;
|
||||
|
||||
// The algorithm stopped because of an interrupt signal or a user interrupt
|
||||
// request.
|
||||
LIMIT_INTERRUPTED = 9;
|
||||
|
||||
// The algorithm stopped because it was unable to continue making progress
|
||||
// towards the solution.
|
||||
LIMIT_SLOW_PROGRESS = 10;
|
||||
|
||||
// The algorithm stopped due to a limit not covered by one of the above. Note
|
||||
// that LIMIT_UNDETERMINED is used when the reason cannot be determined, and
|
||||
// LIMIT_OTHER is used when the reason is known but does not fit into any of
|
||||
// the above alternatives.
|
||||
//
|
||||
// TerminationProto.detail may contain additional information about the limit.
|
||||
LIMIT_OTHER = 11;
|
||||
}
|
||||
|
||||
// All information regarding why a call to Solve() terminated.
|
||||
message TerminationProto {
|
||||
TerminationReasonProto reason = 1;
|
||||
|
||||
// Is LIMIT_UNSPECIFIED unless reason is TERMINATION_REASON_LIMIT_REACHED.
|
||||
// Not all solvers can always determine the limit which caused termination,
|
||||
// LIMIT_UNDETERMINED is used when the cause cannot be determined.
|
||||
LimitProto limit = 2;
|
||||
|
||||
// Additional typically solver specific information about termination.
|
||||
string detail = 3;
|
||||
}
|
||||
|
||||
// The contract of when primal/dual solutions/rays is complex, see
|
||||
@@ -59,103 +230,43 @@ message SolveStatsProto {
|
||||
// Until an exact contract is finalized, it is safest to simply check if a
|
||||
// solution/ray is present rather than relying on the termination reason.
|
||||
message SolveResultProto {
|
||||
enum TerminationReason {
|
||||
TERMINATION_REASON_UNSPECIFIED = 0;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// The optimizer successfully ran to completion.
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// A provably optimal solution (up to numerical tolerances) has been found.
|
||||
OPTIMAL = 1;
|
||||
|
||||
// The primal problem has no feasible solutions.
|
||||
INFEASIBLE = 2;
|
||||
|
||||
// The primal problem is feasible and arbitrarily good solutions can be
|
||||
// found along a primal ray.
|
||||
UNBOUNDED = 3;
|
||||
|
||||
// A dual problem has been shown to be infeasible. The primal problem is
|
||||
// either infeasible or unbounded, but we do not know which.
|
||||
DUAL_INFEASIBLE = 4;
|
||||
|
||||
// The problem was solved to one of the criteria above (optimal, infeasible,
|
||||
// unbounded, or dual infeasible), but one or more tolerances was not met.
|
||||
// Some primal/dual solutions/rays be present, but either they will be
|
||||
// slightly infeasible, or (if the problem was nearly optimal) their may be
|
||||
// a gap between the best solution objective and best objective bound.
|
||||
//
|
||||
// Users can still query primal/dual solutions/rays and solution stats, but
|
||||
// they are responsible for dealing with the numerical imprecision.
|
||||
IMPRECISE = 5;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// The optimizer reached some kind of limit. Partial solution information
|
||||
// may be available.
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// An iterative algorithm stopped after conducting the maximum number of
|
||||
// iterations (e.g. simplex or barrier iterations).
|
||||
ITERATION_LIMIT = 10;
|
||||
// The algorithm stopped after a user-specified computation time.
|
||||
TIME_LIMIT = 11;
|
||||
// A branch-and-bound algorithm stopped because it explored a maximum number
|
||||
// of nodes in the branch-and-bound tree.
|
||||
NODE_LIMIT = 12;
|
||||
// The algorithm stopped because it found the required number of solutions.
|
||||
// This is often used in MIPs to get the solver to return the first feasible
|
||||
// solution it encounters.
|
||||
SOLUTION_LIMIT = 13;
|
||||
// The algorithm stopped because it ran out of memory.
|
||||
MEMORY_LIMIT = 14;
|
||||
// The algorithm stopped because it found a solution better than a minimum
|
||||
// limit set by the user.
|
||||
OBJECTIVE_LIMIT = 15;
|
||||
// The algorithm stopped because the norm of an iterate became too large.
|
||||
NORM_LIMIT = 16;
|
||||
// The algorithm stopped because of an interrupt signal or a user interrupt
|
||||
// request.
|
||||
INTERRUPTED = 17;
|
||||
// The algorithm stopped because it was unable to continue making progress
|
||||
// towards the solution.
|
||||
SLOW_PROGRESS = 18;
|
||||
// Either the algorithm stopped due to a limit not covered by one of the
|
||||
// above or the solver does not provide enough information in its output to
|
||||
// identify the limit.
|
||||
OTHER_LIMIT = 19;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// The optimizer had a problem while optimizing. No solution information is
|
||||
// available.
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// The algorithm stopped because it encountered unrecoverable numerical
|
||||
// error.
|
||||
NUMERICAL_ERROR = 30;
|
||||
|
||||
// The algorithm stopped because of an error not covered by one of the
|
||||
// statuses defined above.
|
||||
OTHER_ERROR = 40;
|
||||
}
|
||||
|
||||
// Non-fatal errors, e.g. an unsupported parameter that was skipped.
|
||||
repeated string warnings = 1;
|
||||
TerminationReason termination_reason = 2;
|
||||
string termination_detail = 3;
|
||||
// Solutions should be ordered best objective value first.
|
||||
repeated PrimalSolutionProto primal_solutions = 4;
|
||||
// Solutions should be ordered best objective value first.
|
||||
repeated DualSolutionProto dual_solutions = 5;
|
||||
repeated PrimalRayProto primal_rays = 6;
|
||||
repeated DualRayProto dual_rays = 7;
|
||||
// basis[i] corresponds to the primal dual pair:
|
||||
// {primal_solutions[i], dual_solutions[i]}. These fields must have at least
|
||||
// as many elements as basis. Basis will only be populated for LPs, and may
|
||||
// not be populated.
|
||||
// TODO(b/183631989): rename to bases.
|
||||
repeated BasisProto basis = 8;
|
||||
SolveStatsProto solve_stats = 9;
|
||||
|
||||
// The reason the solver stopped.
|
||||
TerminationProto termination = 2;
|
||||
|
||||
// Basic solutions use, as of Nov 2021:
|
||||
// * All convex optimization solvers (LP, convex QP) return only one
|
||||
// solution as a primal dual pair.
|
||||
// * Only MI(Q)P solvers return more than one solution. MIP solvers do not
|
||||
// return any dual information, or primal infeasible solutions. Solutions
|
||||
// are returned in order of best primal objective first. Gurobi solves
|
||||
// nonconvex QP (integer or continuous) as MIQP.
|
||||
|
||||
// The general contract for the order of solutions that future solvers should
|
||||
// implement is to order by:
|
||||
// 1. The solutions with a primal feasible solution, ordered by best primal
|
||||
// objective first.
|
||||
// 2. The solutions with a dual feasible solution, ordered by best dual
|
||||
// objective (unknown dual objective is worst)
|
||||
// 3. All remaining solutions can be returned in any order.
|
||||
repeated SolutionProto solutions = 3;
|
||||
|
||||
// Directions of unbounded primal improvement, or equivalently, dual
|
||||
// infeasibility certificates. Typically provided for TerminationReasonProtos
|
||||
// UNBOUNDED and DUAL_INFEASIBLE
|
||||
repeated PrimalRayProto primal_rays = 4;
|
||||
|
||||
// Directions of unbounded dual improvement, or equivalently, primal
|
||||
// infeasibility certificates. Typically provided for TerminationReasonProto
|
||||
// INFEASIBLE.
|
||||
repeated DualRayProto dual_rays = 5;
|
||||
|
||||
// Statistics on the solve process, e.g. running time, iterations.
|
||||
SolveStatsProto solve_stats = 6;
|
||||
|
||||
oneof solver_specific_output {
|
||||
GScipOutput gscip_output = 10;
|
||||
GScipOutput gscip_output = 7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,21 @@ cc_binary(
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "cocktail_hour",
|
||||
srcs = ["cocktail_hour.cc"],
|
||||
deps = [
|
||||
"//ortools/base",
|
||||
"//ortools/math_opt/cpp:math_opt",
|
||||
"//ortools/math_opt/solvers:cp_sat_solver",
|
||||
"//ortools/math_opt/solvers:gscip_solver",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/strings",
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "linear_programming",
|
||||
srcs = ["linear_programming.cc"],
|
||||
@@ -36,6 +51,19 @@ cc_binary(
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "cutting_stock",
|
||||
srcs = ["cutting_stock.cc"],
|
||||
deps = [
|
||||
"//ortools/base",
|
||||
"//ortools/math_opt/cpp:math_opt",
|
||||
"//ortools/math_opt/solvers:cp_sat_solver",
|
||||
"//ortools/math_opt/solvers:glop_solver",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "facility_lp_benders",
|
||||
srcs = ["facility_lp_benders.cc"],
|
||||
|
||||
@@ -33,31 +33,28 @@ namespace {
|
||||
|
||||
void SolveVersion1() {
|
||||
using ::operations_research::math_opt::LinearConstraint;
|
||||
using ::operations_research::math_opt::MathOpt;
|
||||
using ::operations_research::math_opt::Objective;
|
||||
using ::operations_research::math_opt::Result;
|
||||
using ::operations_research::math_opt::SolveParametersProto;
|
||||
using ::operations_research::math_opt::SolveResultProto;
|
||||
using ::operations_research::math_opt::Model;
|
||||
using ::operations_research::math_opt::SolveResult;
|
||||
using ::operations_research::math_opt::SolverType;
|
||||
using ::operations_research::math_opt::TerminationReason;
|
||||
using ::operations_research::math_opt::Variable;
|
||||
|
||||
MathOpt optimizer(operations_research::math_opt::SOLVER_TYPE_GSCIP,
|
||||
"my_model");
|
||||
const Variable x = optimizer.AddBinaryVariable("x");
|
||||
const Variable y = optimizer.AddContinuousVariable(0.0, 2.5, "y");
|
||||
const LinearConstraint c = optimizer.AddLinearConstraint(
|
||||
Model model("my_model");
|
||||
const Variable x = model.AddBinaryVariable("x");
|
||||
const Variable y = model.AddContinuousVariable(0.0, 2.5, "y");
|
||||
const LinearConstraint c = model.AddLinearConstraint(
|
||||
-std::numeric_limits<double>::infinity(), 1.5, "c");
|
||||
c.set_coefficient(x, 1.0);
|
||||
c.set_coefficient(y, 1.0);
|
||||
const Objective obj = optimizer.objective();
|
||||
obj.set_linear_coefficient(x, 2.0);
|
||||
obj.set_linear_coefficient(y, 1.0);
|
||||
obj.set_maximize();
|
||||
const Result result = optimizer.Solve(SolveParametersProto()).value();
|
||||
model.set_coefficient(c, x, 1.0);
|
||||
model.set_coefficient(c, y, 1.0);
|
||||
model.set_objective_coefficient(x, 2.0);
|
||||
model.set_objective_coefficient(y, 1.0);
|
||||
model.set_maximize();
|
||||
const SolveResult result = Solve(model, SolverType::kGscip).value();
|
||||
for (const auto& warning : result.warnings) {
|
||||
std::cerr << "Solver warning: " << warning << std::endl;
|
||||
}
|
||||
CHECK_EQ(result.termination_reason, SolveResultProto::OPTIMAL)
|
||||
<< result.termination_detail;
|
||||
CHECK_EQ(result.termination.reason, TerminationReason::kOptimal)
|
||||
<< result.termination;
|
||||
// The following code will print:
|
||||
// objective value: 2.5
|
||||
// value for variable x: 1
|
||||
@@ -68,29 +65,28 @@ void SolveVersion1() {
|
||||
|
||||
void SolveVersion2() {
|
||||
using ::operations_research::math_opt::LinearExpression;
|
||||
using ::operations_research::math_opt::MathOpt;
|
||||
using ::operations_research::math_opt::Result;
|
||||
using ::operations_research::math_opt::SolveParametersProto;
|
||||
using ::operations_research::math_opt::SolveResultProto;
|
||||
using ::operations_research::math_opt::Model;
|
||||
using ::operations_research::math_opt::SolveResult;
|
||||
using ::operations_research::math_opt::SolverType;
|
||||
using ::operations_research::math_opt::TerminationReason;
|
||||
using ::operations_research::math_opt::Variable;
|
||||
|
||||
MathOpt optimizer(operations_research::math_opt::SOLVER_TYPE_GSCIP,
|
||||
"my_model");
|
||||
const Variable x = optimizer.AddBinaryVariable("x");
|
||||
const Variable y = optimizer.AddContinuousVariable(0.0, 2.5, "y");
|
||||
Model model("my_model");
|
||||
const Variable x = model.AddBinaryVariable("x");
|
||||
const Variable y = model.AddContinuousVariable(0.0, 2.5, "y");
|
||||
// We can directly use linear combinations of variables ...
|
||||
optimizer.AddLinearConstraint(x + y <= 1.5, "c");
|
||||
model.AddLinearConstraint(x + y <= 1.5, "c");
|
||||
// ... or build them incrementally.
|
||||
LinearExpression objective_expression;
|
||||
objective_expression += 2 * x;
|
||||
objective_expression += y;
|
||||
optimizer.objective().Maximize(objective_expression);
|
||||
const Result result = optimizer.Solve(SolveParametersProto()).value();
|
||||
model.Maximize(objective_expression);
|
||||
const SolveResult result = Solve(model, SolverType::kGscip).value();
|
||||
for (const auto& warning : result.warnings) {
|
||||
std::cerr << "Solver warning: " << warning << std::endl;
|
||||
}
|
||||
CHECK_EQ(result.termination_reason, SolveResultProto::OPTIMAL)
|
||||
<< result.termination_detail;
|
||||
CHECK_EQ(result.termination.reason, TerminationReason::kOptimal)
|
||||
<< result.termination;
|
||||
// The following code will print:
|
||||
// objective value: 2.5
|
||||
// value for variable x: 1
|
||||
|
||||
376
ortools/math_opt/samples/cocktail_hour.cc
Normal file
376
ortools/math_opt/samples/cocktail_hour.cc
Normal file
@@ -0,0 +1,376 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Pick ingredients to buy to make the maximum number of cocktails.
|
||||
//
|
||||
// Given a list of cocktails, each of which is made from a list of ingredients,
|
||||
// and a budget of how many ingredients you can buy, solve a MIP to pick a
|
||||
// subset of the ingredients so that you can make the largest number of
|
||||
// cocktails.
|
||||
//
|
||||
// This program can be run in three modes:
|
||||
// text: Outputs the optimal set of ingredients and cocktails that can be
|
||||
// produced as plain text to standard out.
|
||||
// latex: Outputs a menu of the cocktails that can be made as LaTeX code to
|
||||
// standard out.
|
||||
// analysis: Computes the number of cocktails that can be made as a function
|
||||
// of the number of ingredients for all values.
|
||||
//
|
||||
// In latex mode, the output can be piped directly to pdflatex, e.g.
|
||||
// blaze run -c opt \
|
||||
// ortools/math_opt/examples/cocktail_hour \
|
||||
// -- --num_ingredients 10 --mode latex | pdflatex -output-directory /tmp
|
||||
// will create a PDF in /tmp.
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/flags/parse.h"
|
||||
#include "absl/flags/usage.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/strings/str_replace.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/map_util.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
#include "ortools/math_opt/cpp/math_opt.h"
|
||||
|
||||
ABSL_FLAG(std::string, mode, "text",
|
||||
"One of \"text\", \"latex\", or \"analysis\".");
|
||||
ABSL_FLAG(int, num_ingredients, 10,
|
||||
"How many ingredients to buy (ignored in analysis mode).");
|
||||
ABSL_FLAG(std::vector<std::string>, existing_ingredients, {},
|
||||
"Ingredients you already have (ignored in analysis mode).");
|
||||
ABSL_FLAG(std::vector<std::string>, unavailable_ingredients, {},
|
||||
"Ingredients you cannot get (ignored in analysis mode).");
|
||||
ABSL_FLAG(std::vector<std::string>, required_cocktails, {},
|
||||
"Cocktails you must be able to make (ignored in analysis mode).");
|
||||
ABSL_FLAG(std::vector<std::string>, blocked_cocktails, {},
|
||||
"Cocktails to exclude from the menu (ignored in analysis mode).");
|
||||
|
||||
namespace {
|
||||
|
||||
namespace math_opt = ::operations_research::math_opt;
|
||||
|
||||
constexpr absl::string_view kIngredients[] = {"Amaro Nonino",
|
||||
"All Spice Dram",
|
||||
"Aperol",
|
||||
"Bitters",
|
||||
"Bourbon",
|
||||
"Brandy",
|
||||
"Campari",
|
||||
"Cinnamon",
|
||||
"Chambord",
|
||||
"Cherry",
|
||||
"Cloves",
|
||||
"Cointreau",
|
||||
"Coke",
|
||||
"Cranberry",
|
||||
"Creme de Cacao",
|
||||
"Creme de Violette",
|
||||
"Cucumber",
|
||||
"Egg",
|
||||
"Gin",
|
||||
"Green Chartreuse",
|
||||
"Heavy Cream",
|
||||
"Lemon",
|
||||
"Lillet Blanc",
|
||||
"Lime",
|
||||
"Luxardo",
|
||||
"Mint",
|
||||
"Orange",
|
||||
"Orange Flower Water Extract",
|
||||
"Orgeat",
|
||||
"Pickle",
|
||||
"Pineapple Juice",
|
||||
"Pisco",
|
||||
"Prosecco",
|
||||
"Raspberry Vodka",
|
||||
"Ruby Port",
|
||||
"Rum",
|
||||
"Seltzer",
|
||||
"Simple Syrup",
|
||||
"Sugar",
|
||||
"Sweet Vermouth",
|
||||
"Tequila",
|
||||
"Tonic Water",
|
||||
"Vodka"};
|
||||
|
||||
constexpr std::size_t kIngredientsSize =
|
||||
sizeof(kIngredients) / sizeof(kIngredients[0]);
|
||||
|
||||
struct Cocktail {
|
||||
std::string name;
|
||||
std::vector<std::string> ingredients;
|
||||
};
|
||||
|
||||
std::vector<Cocktail> AllCocktails() {
|
||||
return {
|
||||
// Aperitifs
|
||||
{.name = "Prosecco glass", .ingredients = {"Prosecco"}},
|
||||
{.name = "Aperol Spritz", .ingredients = {"Prosecco", "Aperol"}},
|
||||
{.name = "Chambord Spritz", .ingredients = {"Prosecco", "Chambord"}},
|
||||
{.name = "Improved French 75",
|
||||
.ingredients = {"Prosecco", "Vodka", "Lemon", "Simple Syrup"}},
|
||||
// Quick and Simple
|
||||
{.name = "Gin and Tonic", .ingredients = {"Gin", "Tonic Water", "Lime"}},
|
||||
{.name = "Rum and Coke", .ingredients = {"Rum", "Coke"}},
|
||||
{.name = "Improved Manhattan",
|
||||
.ingredients = {"Bourbon", "Sweet Vermouth", "Bitters"}},
|
||||
// Vodka
|
||||
|
||||
// Serve with a sugared rim
|
||||
{.name = "Lemon Drop",
|
||||
.ingredients = {"Vodka", "Cointreau", "Lemon", "Simple Syrup"}},
|
||||
// Shake, then float 2oz Prosecco after pouring
|
||||
{.name = "Big Crush",
|
||||
.ingredients = {"Raspberry Vodka", "Cointreau", "Lemon", "Chambord",
|
||||
"Prosecco"}},
|
||||
{.name = "Cosmopolitan",
|
||||
.ingredients = {"Vodka", "Cranberry", "Cointreau", "Lime"}},
|
||||
// A shot, chase with 1/3 of pickle spear
|
||||
{.name = "Vodka/Pickle", .ingredients = {"Vodka", "Pickle"}},
|
||||
|
||||
// Gin
|
||||
{.name = "Last Word",
|
||||
.ingredients = {"Gin", "Green Chartreuse", "Luxardo", "Lime"}},
|
||||
{.name = "Corpse Reviver #2 (Lite)",
|
||||
.ingredients = {"Gin", "Cointreau", "Lillet Blanc", "Lemon"}},
|
||||
{.name = "Negroni", .ingredients = {"Gin", "Sweet Vermouth", "Campari"}},
|
||||
// "Float" Creme de Violette (it will sink)
|
||||
{.name = "Aviation",
|
||||
.ingredients = {"Gin", "Luxardo", "Lemon", "Creme de Violette"}},
|
||||
|
||||
// Bourbon
|
||||
{.name = "Paper Plane",
|
||||
.ingredients = {"Bourbon", "Aperol", "Amaro Nonino", "Lemon"}},
|
||||
{.name = "Derby",
|
||||
.ingredients = {"Bourbon", "Sweet Vermouth", "Lime", "Cointreau"}},
|
||||
// Muddle sugar, water, bitters, and orange peel. Garnish with a Luxardo
|
||||
// cherry (do not cheap out), spill cherry syrup generously in drink
|
||||
{.name = "Old Fashioned",
|
||||
.ingredients = {"Bourbon", "Sugar", "Bitters", "Orange", "Cherry"}},
|
||||
{.name = "Boulevardier",
|
||||
.ingredients = {"Bourbon", "Sweet Vermouth", "Campari"}},
|
||||
|
||||
// Tequila
|
||||
{.name = "Margarita", .ingredients = {"Tequila", "Cointreau", "Lime"}},
|
||||
// Shake with chopped cucumber and strain. Garnish with cucumber.
|
||||
{.name = "Midnight Cruiser",
|
||||
.ingredients = {"Tequila", "Aperol", "Lime", "Pineapple Juice",
|
||||
"Cucumber", "Simple Syrup"}},
|
||||
|
||||
{.name = "Tequila shot", .ingredients = {"Tequila"}},
|
||||
// Rum
|
||||
|
||||
// Shake with light rum, float a dark rum on top.
|
||||
{.name = "Pineapple Mai Tai",
|
||||
.ingredients = {"Rum", "Lime", "Orgeat", "Cointreau",
|
||||
"Pineapple Juice"}},
|
||||
{.name = "Daiquiri", .ingredients = {"Rum", "Lime", "Simple Syrup"}},
|
||||
{.name = "Mojito",
|
||||
.ingredients = {"Rum", "Lime", "Simple Syrup", "Mint", "Seltzer"}},
|
||||
// Add bitters generously. Invert half lime to form a cup, fill with
|
||||
// Green Chartreuse and cloves. Float lime cup on drink and ignite.
|
||||
{.name = "Kennedy",
|
||||
.ingredients = {"Rum", "All Spice Dram", "Bitters", "Lime",
|
||||
"Simple Syrup", "Cloves", "Green Chartreuse"}},
|
||||
|
||||
// Egg
|
||||
|
||||
{.name = "Pisco Sour",
|
||||
.ingredients = {"Pisco", "Lime", "Simple Syrup", "Egg", "Bitters"}},
|
||||
{.name = "Viana",
|
||||
.ingredients = {"Ruby Port", "Brandy", "Creme de Cacao", "Sugar", "Egg",
|
||||
"Cinnamon"}},
|
||||
// Add cream last before shaking (and seltzer after shaking). Shake for 10
|
||||
// minutes, no less.
|
||||
{.name = "Ramos gin fizz",
|
||||
.ingredients = {"Gin", "Seltzer", "Heavy Cream",
|
||||
"Orange Flower Water Extract", "Egg", "Lemon", "Lime",
|
||||
"Simple Syrup"}}};
|
||||
}
|
||||
|
||||
struct Menu {
|
||||
std::vector<std::string> ingredients;
|
||||
std::vector<Cocktail> cocktails;
|
||||
};
|
||||
|
||||
absl::StatusOr<Menu> SolveForMenu(
|
||||
const int max_new_ingredients, const bool enable_solver_output,
|
||||
const absl::flat_hash_set<std::string>& existing_ingredients,
|
||||
const absl::flat_hash_set<std::string>& unavailable_ingredients,
|
||||
const absl::flat_hash_set<std::string>& required_cocktails,
|
||||
const absl::flat_hash_set<std::string>& blocked_cocktails) {
|
||||
const std::vector<Cocktail> all_cocktails = AllCocktails();
|
||||
math_opt::Model model("Cocktail hour");
|
||||
absl::flat_hash_map<std::string, math_opt::Variable> ingredient_vars;
|
||||
for (const absl::string_view ingredient : kIngredients) {
|
||||
const double lb = existing_ingredients.contains(ingredient) ? 1.0 : 0.0;
|
||||
const double ub = unavailable_ingredients.contains(ingredient) ? 0.0 : 1.0;
|
||||
const math_opt::Variable v = model.AddIntegerVariable(lb, ub, ingredient);
|
||||
gtl::InsertOrDie(&ingredient_vars, std::string(ingredient), v);
|
||||
}
|
||||
math_opt::LinearExpression ingredients_used;
|
||||
for (const auto& [name, ingredient_var] : ingredient_vars) {
|
||||
ingredients_used += ingredient_var;
|
||||
}
|
||||
model.AddLinearConstraint(ingredients_used <=
|
||||
max_new_ingredients + existing_ingredients.size());
|
||||
|
||||
absl::flat_hash_map<std::string, math_opt::Variable> cocktail_vars;
|
||||
for (const Cocktail& cocktail : all_cocktails) {
|
||||
const double lb = required_cocktails.contains(cocktail.name) ? 1.0 : 0.0;
|
||||
const double ub = blocked_cocktails.contains(cocktail.name) ? 0.0 : 1.0;
|
||||
const math_opt::Variable v =
|
||||
model.AddIntegerVariable(lb, ub, cocktail.name);
|
||||
for (const std::string& ingredient : cocktail.ingredients) {
|
||||
model.AddLinearConstraint(v <=
|
||||
gtl::FindOrDie(ingredient_vars, ingredient));
|
||||
}
|
||||
gtl::InsertOrDie(&cocktail_vars, cocktail.name, v);
|
||||
}
|
||||
math_opt::LinearExpression cocktails_made;
|
||||
for (const auto& [name, cocktail_var] : cocktail_vars) {
|
||||
cocktails_made += cocktail_var;
|
||||
}
|
||||
model.Maximize(cocktails_made);
|
||||
const math_opt::SolveArguments args = {
|
||||
.parameters = {.enable_output = enable_solver_output}};
|
||||
ASSIGN_OR_RETURN(const math_opt::SolveResult result,
|
||||
math_opt::Solve(model, math_opt::SolverType::kGscip, args));
|
||||
|
||||
// Check that the problem has an optimal solution.
|
||||
QCHECK_EQ(result.termination.reason, math_opt::TerminationReason::kOptimal)
|
||||
<< "Failed to find an optimal solution: " << result.termination;
|
||||
|
||||
Menu menu;
|
||||
for (const absl::string_view ingredient : kIngredients) {
|
||||
if (result.variable_values().at(ingredient_vars.at(ingredient)) > 0.5) {
|
||||
menu.ingredients.push_back(std::string(ingredient));
|
||||
}
|
||||
}
|
||||
for (const Cocktail& cocktail : all_cocktails) {
|
||||
if (result.variable_values().at(cocktail_vars.at(cocktail.name)) > 0.5) {
|
||||
menu.cocktails.push_back(cocktail);
|
||||
}
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
absl::flat_hash_set<std::string> SetFromVec(
|
||||
const std::vector<std::string>& vec) {
|
||||
return {vec.begin(), vec.end()};
|
||||
}
|
||||
|
||||
absl::Status AnalysisMode() {
|
||||
std::cout << "Considering " << AllCocktails().size() << " cocktails and "
|
||||
<< kIngredientsSize << " ingredients." << std::endl;
|
||||
std::cout << "Solving for number of cocktails that can be made as a function "
|
||||
"of number of ingredients"
|
||||
<< std::endl;
|
||||
|
||||
std::cout << "ingredients | cocktails" << std::endl;
|
||||
for (int i = 1; i <= kIngredientsSize; ++i) {
|
||||
const absl::StatusOr<Menu> menu = SolveForMenu(
|
||||
i, false, /*existing_ingredients=*/{}, /*unavailable_ingredients=*/{},
|
||||
/*required_cocktails=*/{}, /*blocked_cocktails=*/{});
|
||||
RETURN_IF_ERROR(menu.status())
|
||||
<< "Failure when solving for " << i << " ingredients";
|
||||
std::cout << i << " | " << menu->cocktails.size() << std::endl;
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
std::string ExportToLaTeX(const std::vector<Cocktail>& cocktails,
|
||||
const std::string& title = "Cocktail Hour") {
|
||||
std::vector<std::string> lines;
|
||||
lines.push_back("\\documentclass{article}");
|
||||
lines.push_back("\\usepackage{fullpage}");
|
||||
lines.push_back("\\linespread{2}");
|
||||
lines.push_back("\\begin{document}");
|
||||
lines.push_back("\\begin{center}");
|
||||
lines.push_back(absl::StrCat("\\begin{Huge}", title, "\\end{Huge}"));
|
||||
lines.push_back("");
|
||||
for (const Cocktail& cocktail : cocktails) {
|
||||
lines.push_back(absl::StrCat(cocktail.name, "---{\\em ",
|
||||
absl::StrJoin(cocktail.ingredients, ", "),
|
||||
"}"));
|
||||
lines.push_back("");
|
||||
}
|
||||
lines.push_back("\\end{center}");
|
||||
lines.push_back("\\end{document}");
|
||||
|
||||
return absl::StrReplaceAll(absl::StrJoin(lines, "\n"), {{"#", "\\#"}});
|
||||
}
|
||||
|
||||
void RealMain() {
|
||||
const std::string mode = absl::GetFlag(FLAGS_mode);
|
||||
CHECK(absl::flat_hash_set<std::string>({"text", "latex", "analysis"})
|
||||
.contains(mode))
|
||||
<< "Unexpected mode: " << mode;
|
||||
|
||||
// We are in analysis mode.
|
||||
if (mode == "analysis") {
|
||||
const absl::Status status = AnalysisMode();
|
||||
if (!status.ok()) {
|
||||
LOG(QFATAL) << status;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
absl::StatusOr<Menu> menu =
|
||||
SolveForMenu(absl::GetFlag(FLAGS_num_ingredients), mode == "text",
|
||||
SetFromVec(absl::GetFlag(FLAGS_existing_ingredients)),
|
||||
SetFromVec(absl::GetFlag(FLAGS_unavailable_ingredients)),
|
||||
SetFromVec(absl::GetFlag(FLAGS_required_cocktails)),
|
||||
SetFromVec(absl::GetFlag(FLAGS_blocked_cocktails)));
|
||||
if (!menu.ok()) {
|
||||
LOG(QFATAL) << "Error when solving for optimal set of ingredients: "
|
||||
<< menu.status();
|
||||
}
|
||||
|
||||
// We are in latex mode.
|
||||
if (mode == "latex") {
|
||||
std::cout << ExportToLaTeX(menu->cocktails) << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
// We are in text mode
|
||||
std::cout << "Considered " << AllCocktails().size() << " cocktails and "
|
||||
<< kIngredientsSize << " ingredients." << std::endl;
|
||||
std::cout << "Solution has " << menu->ingredients.size()
|
||||
<< " ingredients to make " << menu->cocktails.size()
|
||||
<< " cocktails." << std::endl
|
||||
<< std::endl;
|
||||
|
||||
std::cout << "Ingredients:" << std::endl;
|
||||
for (const std::string& ingredient : menu->ingredients) {
|
||||
std::cout << " " << ingredient << std::endl;
|
||||
}
|
||||
std::cout << "Cocktails:" << std::endl;
|
||||
for (const Cocktail& cocktail : menu->cocktails) {
|
||||
std::cout << " " << cocktail.name << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
absl::ParseCommandLine(argc, argv);
|
||||
RealMain();
|
||||
return 0;
|
||||
}
|
||||
267
ortools/math_opt/samples/cutting_stock.cc
Normal file
267
ortools/math_opt/samples/cutting_stock.cc
Normal file
@@ -0,0 +1,267 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// The Cutting Stock problem is as follows. You begin with unlimited boards, all
|
||||
// of the same length. You are also given a list of smaller pieces to cut out,
|
||||
// each with a length and a demanded quantity. You want to cut out all these
|
||||
// pieces using as few of your starting boards as possible.
|
||||
//
|
||||
// E.g. you begin with boards that are 20 feet long, and you must cut out 3
|
||||
// pieces that are 6 feet long and 5 pieces that are 8 feet long. An optimal
|
||||
// solution is:
|
||||
// [(6,), (8, 8) (8, 8), (6, 6, 8)]
|
||||
// (We cut a 6 foot piece from the first board, two 8 foot pieces from
|
||||
// the second board, and so on.)
|
||||
//
|
||||
// This example approximately solves the problem with a column generation
|
||||
// heuristic. The leader problem is a set cover problem, and the worker is a
|
||||
// knapsack problem. We alternate between solving the LP relaxation of the
|
||||
// leader incrementally, and solving the worker to generate new a configuration
|
||||
// (a column) for the leader. When the worker can no longer find a column
|
||||
// improving the LP cost, we convert the leader problem to a MIP and solve
|
||||
// again. We now give precise statements of the leader and worker.
|
||||
//
|
||||
// Problem data:
|
||||
// * l_i: the length of each piece we need to cut out.
|
||||
// * d_i: how many copies each piece we need.
|
||||
// * L: the length of our initial boards.
|
||||
// * q_ci: for configuration c, the quantity of piece i produced.
|
||||
//
|
||||
// Leader problem variables:
|
||||
// * x_c: how many copies of configuration c to produce.
|
||||
//
|
||||
// Leader problem formulation:
|
||||
// min sum_c x_c
|
||||
// s.t. sum_c q_ci * x_c = d_i for all i
|
||||
// x_c >= 0, integer for all c.
|
||||
//
|
||||
// The worker problem is to generate new configurations for the leader problem
|
||||
// based on the dual variables of the demand constraints in the LP relaxation.
|
||||
// Worker problem data:
|
||||
// * p_i: The "price" of piece i (dual value from leader's demand constraint)
|
||||
//
|
||||
// Worker decision variables:
|
||||
// * y_i: How many copies of piece i should be in the configuration.
|
||||
//
|
||||
// Worker formulation
|
||||
// max sum_i p_i * y_i
|
||||
// s.t. sum_i l_i * y_i <= L
|
||||
// y_i >= 0, integer for all i
|
||||
//
|
||||
// An optimal solution y* defines a new configuration c with q_ci = y_i* for all
|
||||
// i. If the solution has objective value <= 1, no further improvement on the LP
|
||||
// is possible. For additional background and proofs see:
|
||||
// https://people.orie.cornell.edu/shmoys/or630/notes-06/lec16.pdf
|
||||
// or any other reference on the "Cutting Stock Problem".
|
||||
//
|
||||
// Note: this problem is equivalent to symmetric bin packing:
|
||||
// https://en.wikipedia.org/wiki/Bin_packing_problem#Formal_statement
|
||||
// but typically in bin packing it is not assumed that you should exploit having
|
||||
// multiple items of the same size.
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/flags/parse.h"
|
||||
#include "absl/flags/usage.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/status_builder.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
#include "ortools/math_opt/cpp/math_opt.h"
|
||||
|
||||
namespace {
|
||||
|
||||
namespace math_opt = operations_research::math_opt;
|
||||
constexpr double kInf = std::numeric_limits<double>::infinity();
|
||||
|
||||
// piece_sizes and piece_demands must have equal length.
|
||||
// every piece must have 0 < size <= board_length.
|
||||
// every piece must have demand > 0.
|
||||
struct CuttingStockInstance {
|
||||
std::vector<int> piece_sizes;
|
||||
std::vector<int> piece_demands;
|
||||
int board_length;
|
||||
};
|
||||
|
||||
// pieces and quantity must have equal size.
|
||||
// Defined for a related CuttingStockInstance, the total length all pieces
|
||||
// weighted by their quantity must not exceed board_length.
|
||||
struct Configuration {
|
||||
std::vector<int> pieces;
|
||||
std::vector<int> quantity;
|
||||
};
|
||||
|
||||
// configurations and quantity must have equal size.
|
||||
// objective_value is the sum of the vales in quantity (how many total boards
|
||||
// are used).
|
||||
// To be feasible, the demand for each piece type must be met by the produced
|
||||
// configurations.
|
||||
struct CuttingStockSolution {
|
||||
std::vector<Configuration> configurations;
|
||||
std::vector<int> quantity;
|
||||
int objective_value = 0;
|
||||
};
|
||||
|
||||
// Solves the worker problem.
|
||||
//
|
||||
// Solves the problem on finding the configuration (with its objective value) to
|
||||
// add the to model that will give the greatest improvement in the LP
|
||||
// relaxation. This is equivalent to a knapsack problem.
|
||||
absl::StatusOr<std::pair<Configuration, double>> BestConfiguration(
|
||||
const std::vector<double>& piece_prices,
|
||||
const std::vector<int>& piece_sizes, const int board_size) {
|
||||
int num_pieces = piece_prices.size();
|
||||
CHECK_EQ(piece_sizes.size(), num_pieces);
|
||||
math_opt::Model model("knapsack");
|
||||
std::vector<math_opt::Variable> pieces;
|
||||
for (int i = 0; i < num_pieces; ++i) {
|
||||
pieces.push_back(
|
||||
model.AddIntegerVariable(0, kInf, absl::StrCat("item_", i)));
|
||||
}
|
||||
model.Maximize(math_opt::InnerProduct(pieces, piece_prices));
|
||||
model.AddLinearConstraint(math_opt::InnerProduct(pieces, piece_sizes) <=
|
||||
board_size);
|
||||
ASSIGN_OR_RETURN(const math_opt::SolveResult solve_result,
|
||||
math_opt::Solve(model, math_opt::SolverType::kCpSat));
|
||||
if (solve_result.termination.reason !=
|
||||
math_opt::TerminationReason::kOptimal) {
|
||||
return util::InvalidArgumentErrorBuilder()
|
||||
<< "Failed to solve knapsack pricing problem: "
|
||||
<< solve_result.termination;
|
||||
}
|
||||
Configuration config;
|
||||
for (int i = 0; i < num_pieces; ++i) {
|
||||
const int use = static_cast<int>(
|
||||
std::round(solve_result.variable_values().at(pieces[i])));
|
||||
if (use > 0) {
|
||||
config.pieces.push_back(i);
|
||||
config.quantity.push_back(use);
|
||||
}
|
||||
}
|
||||
return std::make_pair(config, solve_result.objective_value());
|
||||
}
|
||||
|
||||
// Solves the full cutting stock problem by decomposition.
|
||||
absl::StatusOr<CuttingStockSolution> SolveCuttingStock(
|
||||
const CuttingStockInstance& instance) {
|
||||
math_opt::Model model("cutting_stock");
|
||||
model.set_minimize();
|
||||
const int n = instance.piece_sizes.size();
|
||||
std::vector<math_opt::LinearConstraint> demand_met;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const int d = instance.piece_demands[i];
|
||||
demand_met.push_back(model.AddLinearConstraint(d, d));
|
||||
}
|
||||
std::vector<std::pair<Configuration, math_opt::Variable>> configs;
|
||||
auto add_config = [&](const Configuration& config) {
|
||||
const math_opt::Variable v = model.AddContinuousVariable(0.0, kInf);
|
||||
model.set_objective_coefficient(v, 1);
|
||||
for (int i = 0; i < config.pieces.size(); ++i) {
|
||||
const int item = config.pieces[i];
|
||||
const int use = config.quantity[i];
|
||||
if (use >= 1) {
|
||||
model.set_coefficient(demand_met[item], v, use);
|
||||
}
|
||||
}
|
||||
configs.push_back({config, v});
|
||||
};
|
||||
|
||||
// To ensure the leader problem is always feasible, begin a configuration for
|
||||
// every item that has a single copy of the item.
|
||||
for (int i = 0; i < n; ++i) {
|
||||
add_config(Configuration{.pieces = {i}, .quantity = {1}});
|
||||
}
|
||||
|
||||
ASSIGN_OR_RETURN(auto solver, math_opt::IncrementalSolver::New(
|
||||
model, math_opt::SolverType::kGlop));
|
||||
int pricing_round = 0;
|
||||
while (true) {
|
||||
ASSIGN_OR_RETURN(math_opt::SolveResult solve_result, solver->Solve());
|
||||
if (solve_result.termination.reason !=
|
||||
math_opt::TerminationReason::kOptimal) {
|
||||
return absl::InternalErrorBuilder()
|
||||
<< "Failed to solve leader LP problem at iteration "
|
||||
<< pricing_round << " termination: " << solve_result.termination;
|
||||
}
|
||||
// GLOP always returns a dual solution on optimal
|
||||
CHECK(solve_result.has_dual_feasible_solution());
|
||||
std::vector<double> prices;
|
||||
for (const math_opt::LinearConstraint d : demand_met) {
|
||||
prices.push_back(solve_result.dual_values().at(d));
|
||||
}
|
||||
ASSIGN_OR_RETURN(
|
||||
(const auto [config, value]),
|
||||
BestConfiguration(prices, instance.piece_sizes, instance.board_length));
|
||||
if (value <= 1 + 1e-3) {
|
||||
// The LP relaxation is solved, we can stop adding columns.
|
||||
break;
|
||||
}
|
||||
add_config(config);
|
||||
LOG(INFO) << "round: " << pricing_round
|
||||
<< " lp objective: " << solve_result.objective_value();
|
||||
pricing_round++;
|
||||
}
|
||||
LOG(INFO) << "Done adding columns, switching to MIP";
|
||||
for (const auto& [config, var] : configs) {
|
||||
model.set_integer(var);
|
||||
}
|
||||
ASSIGN_OR_RETURN(const math_opt::SolveResult solve_result,
|
||||
math_opt::Solve(model, math_opt::SolverType::kCpSat));
|
||||
if (solve_result.termination.reason !=
|
||||
math_opt::TerminationReason::kOptimal) {
|
||||
return absl::InternalErrorBuilder()
|
||||
<< "Failed to solve final cutting stock MIP, termination: "
|
||||
<< solve_result.termination;
|
||||
}
|
||||
CuttingStockSolution solution;
|
||||
for (const auto& [config, var] : configs) {
|
||||
int use =
|
||||
static_cast<int>(std::round(solve_result.variable_values().at(var)));
|
||||
if (use > 0) {
|
||||
solution.configurations.push_back(config);
|
||||
solution.quantity.push_back(use);
|
||||
solution.objective_value += use;
|
||||
}
|
||||
}
|
||||
return solution;
|
||||
}
|
||||
|
||||
absl::Status RealMain() {
|
||||
// Data from https://en.wikipedia.org/wiki/Cutting_stock_problem
|
||||
CuttingStockInstance instance;
|
||||
instance.board_length = 5600;
|
||||
instance.piece_sizes = {1380, 1520, 1560, 1710, 1820, 1880, 1930,
|
||||
2000, 2050, 2100, 2140, 2150, 2200};
|
||||
instance.piece_demands = {22, 25, 12, 14, 18, 18, 20, 10, 12, 14, 16, 18, 20};
|
||||
ASSIGN_OR_RETURN(CuttingStockSolution solution, SolveCuttingStock(instance));
|
||||
std::cout << "Best known solution uses 73 rolls." << std::endl;
|
||||
std::cout << "Total rolls used in actual solution found: "
|
||||
<< solution.objective_value << std::endl;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
absl::ParseCommandLine(argc, argv);
|
||||
absl::Status result = RealMain();
|
||||
if (!result.ok()) {
|
||||
std::cout << result;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -44,15 +44,15 @@ ABSL_FLAG(
|
||||
"Fraction of a facility's capacity that can be used by each location.");
|
||||
|
||||
namespace {
|
||||
using ::operations_research::math_opt::GurobiParametersProto;
|
||||
using ::operations_research::math_opt::IncrementalSolver;
|
||||
using ::operations_research::math_opt::LinearConstraint;
|
||||
using ::operations_research::math_opt::LinearExpression;
|
||||
using ::operations_research::math_opt::MathOpt;
|
||||
using ::operations_research::math_opt::Objective;
|
||||
using ::operations_research::math_opt::Result;
|
||||
using ::operations_research::math_opt::SolveParametersProto;
|
||||
using ::operations_research::math_opt::SolveResultProto;
|
||||
using ::operations_research::math_opt::Model;
|
||||
using ::operations_research::math_opt::SolveArguments;
|
||||
using ::operations_research::math_opt::SolveResult;
|
||||
using ::operations_research::math_opt::SolverType;
|
||||
using ::operations_research::math_opt::Sum;
|
||||
using ::operations_research::math_opt::TerminationReason;
|
||||
using ::operations_research::math_opt::Variable;
|
||||
|
||||
// First element is a facility and second is a location.
|
||||
@@ -175,17 +175,15 @@ void FullProblem(const Network& network, const double location_demand,
|
||||
const int num_facilities = network.num_facilities();
|
||||
const int num_locations = network.num_locations();
|
||||
|
||||
MathOpt model(operations_research::math_opt::SOLVER_TYPE_GUROBI,
|
||||
"Full network design problem");
|
||||
const Objective objective = model.objective();
|
||||
objective.set_minimize();
|
||||
Model model("Full network design problem");
|
||||
model.set_minimize();
|
||||
|
||||
// Capacity variables
|
||||
std::vector<Variable> z;
|
||||
for (int j = 0; j < num_facilities; j++) {
|
||||
const Variable z_j = model.AddContinuousVariable(0.0, kInf);
|
||||
z.push_back(z_j);
|
||||
objective.set_linear_coefficient(z_j, facility_cost);
|
||||
model.set_objective_coefficient(z_j, facility_cost);
|
||||
}
|
||||
|
||||
// Flow variables
|
||||
@@ -193,7 +191,7 @@ void FullProblem(const Network& network, const double location_demand,
|
||||
for (const auto& edge : network.edges()) {
|
||||
const Variable x_edge = model.AddContinuousVariable(0.0, kInf);
|
||||
x.insert({edge, x_edge});
|
||||
objective.set_linear_coefficient(x_edge, network.edge_cost(edge));
|
||||
model.set_objective_coefficient(x_edge, network.edge_cost(edge));
|
||||
}
|
||||
|
||||
// Demand constraints
|
||||
@@ -220,12 +218,12 @@ void FullProblem(const Network& network, const double location_demand,
|
||||
model.AddLinearConstraint(x.at(edge) <= location_fraction * z[facility]);
|
||||
}
|
||||
}
|
||||
const Result result = model.Solve(SolveParametersProto()).value();
|
||||
const SolveResult result = Solve(model, SolverType::kGurobi).value();
|
||||
for (const auto& warning : result.warnings) {
|
||||
LOG(WARNING) << "Solver warning: " << warning << std::endl;
|
||||
}
|
||||
QCHECK_EQ(result.termination_reason, SolveResultProto::OPTIMAL)
|
||||
<< "Failed to find an optimal solution: " << result.termination_detail;
|
||||
QCHECK_EQ(result.termination.reason, TerminationReason::kOptimal)
|
||||
<< "Failed to find an optimal solution: " << result.termination;
|
||||
std::cout << "Full problem optimal objective: "
|
||||
<< absl::StrFormat("%.9f", result.objective_value()) << std::endl;
|
||||
}
|
||||
@@ -244,8 +242,7 @@ void Benders(const Network network, const double location_demand,
|
||||
// z_f >= 0 for all f in F
|
||||
// sum(fcut_f^i z_f) + fcut_const^i <= 0 for i = 1,...
|
||||
// sum(ocut_f^j z_f) + ocut_const^j <= w for j = 1,...
|
||||
MathOpt first_stage_model(operations_research::math_opt::SOLVER_TYPE_GUROBI,
|
||||
"First stage problem");
|
||||
Model first_stage_model("First stage problem");
|
||||
std::vector<Variable> z;
|
||||
for (int j = 0; j < num_facilities; j++) {
|
||||
z.push_back(first_stage_model.AddContinuousVariable(0.0, kInf));
|
||||
@@ -253,10 +250,7 @@ void Benders(const Network network, const double location_demand,
|
||||
|
||||
const Variable w = first_stage_model.AddContinuousVariable(0.0, kInf);
|
||||
|
||||
first_stage_model.objective().Minimize(facility_cost * Sum(z) + w);
|
||||
|
||||
SolveParametersProto first_stage_params;
|
||||
first_stage_params.mutable_common_parameters()->set_enable_output(false);
|
||||
first_stage_model.Minimize(facility_cost * Sum(z) + w);
|
||||
|
||||
// Setup second stage model.
|
||||
// min sum(h_e * x_e : e in E)
|
||||
@@ -267,16 +261,14 @@ void Benders(const Network network, const double location_demand,
|
||||
// x_e >= 0 for all e in E
|
||||
//
|
||||
// where zz_f are fixed values for z_f from the first stage model.
|
||||
MathOpt second_stage_model(operations_research::math_opt::SOLVER_TYPE_GUROBI,
|
||||
"Second stage model");
|
||||
const Objective second_stage_objective = second_stage_model.objective();
|
||||
second_stage_objective.set_minimize();
|
||||
Model second_stage_model("Second stage model");
|
||||
second_stage_model.set_minimize();
|
||||
absl::flat_hash_map<Edge, Variable> x;
|
||||
for (const auto& edge : network.edges()) {
|
||||
const Variable x_edge = second_stage_model.AddContinuousVariable(0.0, kInf);
|
||||
x.insert({edge, x_edge});
|
||||
second_stage_objective.set_linear_coefficient(x_edge,
|
||||
network.edge_cost(edge));
|
||||
second_stage_model.set_objective_coefficient(x_edge,
|
||||
network.edge_cost(edge));
|
||||
}
|
||||
std::vector<LinearConstraint> demand_constraints;
|
||||
|
||||
@@ -298,25 +290,26 @@ void Benders(const Network network, const double location_demand,
|
||||
second_stage_model.AddLinearConstraint(linear_expression <= kInf));
|
||||
}
|
||||
|
||||
SolveParametersProto second_stage_params;
|
||||
second_stage_params.mutable_common_parameters()->set_enable_output(false);
|
||||
GurobiParametersProto::Parameter* param1 =
|
||||
second_stage_params.mutable_gurobi_parameters()->add_parameters();
|
||||
param1->set_name("InfUnbdInfo");
|
||||
param1->set_value("1");
|
||||
SolveArguments second_stage_args;
|
||||
second_stage_args.parameters.gurobi.param_values["InfUnbdInfo"] = "1";
|
||||
|
||||
// Start Benders
|
||||
int iteration = 0;
|
||||
double best_upper_bound = kInf;
|
||||
const std::unique_ptr<IncrementalSolver> first_stage_solver =
|
||||
IncrementalSolver::New(first_stage_model, SolverType::kGurobi).value();
|
||||
const std::unique_ptr<IncrementalSolver> second_stage_solver =
|
||||
IncrementalSolver::New(second_stage_model, SolverType::kGurobi).value();
|
||||
while (true) {
|
||||
LOG(INFO) << "Iteration: " << iteration;
|
||||
// Solve and process first stage.
|
||||
const Result first_stage_result =
|
||||
first_stage_model.Solve(first_stage_params).value();
|
||||
const SolveResult first_stage_result = first_stage_solver->Solve().value();
|
||||
for (const auto& warning : first_stage_result.warnings) {
|
||||
LOG(WARNING) << "Solver warning: " << warning << std::endl;
|
||||
}
|
||||
QCHECK_EQ(first_stage_result.termination_reason, SolveResultProto::OPTIMAL);
|
||||
QCHECK_EQ(first_stage_result.termination.reason,
|
||||
TerminationReason::kOptimal)
|
||||
<< first_stage_result.termination;
|
||||
const double lower_bound = first_stage_result.objective_value();
|
||||
LOG(INFO) << "LB = " << lower_bound;
|
||||
|
||||
@@ -325,19 +318,21 @@ void Benders(const Network network, const double location_demand,
|
||||
const double capacity_value =
|
||||
first_stage_result.variable_values().at(z[facility]);
|
||||
for (const auto& edge : network.edges_incident_to_facility(facility)) {
|
||||
x.at(edge).set_upper_bound(location_fraction * capacity_value);
|
||||
second_stage_model.set_upper_bound(x.at(edge),
|
||||
location_fraction * capacity_value);
|
||||
}
|
||||
supply_constraints[facility].set_upper_bound(capacity_value);
|
||||
second_stage_model.set_upper_bound(supply_constraints[facility],
|
||||
capacity_value);
|
||||
}
|
||||
|
||||
// Solve and process second stage.
|
||||
const Result second_stage_result =
|
||||
second_stage_model.Solve(second_stage_params).value();
|
||||
const SolveResult second_stage_result =
|
||||
second_stage_solver->Solve(second_stage_args).value();
|
||||
for (const auto& warning : second_stage_result.warnings) {
|
||||
LOG(WARNING) << "Solver warning: " << warning << std::endl;
|
||||
}
|
||||
if (second_stage_result.termination_reason ==
|
||||
SolveResultProto::INFEASIBLE) {
|
||||
if (second_stage_result.termination.reason ==
|
||||
TerminationReason::kInfeasible) {
|
||||
// If the second stage problem is infeasible we will get a dual ray
|
||||
// (r, y) such that
|
||||
//
|
||||
@@ -397,8 +392,9 @@ void Benders(const Network network, const double location_demand,
|
||||
// ocut_f = sum(r_(f,l)*a : (f,l) in E, r_(f,l) < 0)
|
||||
// + min{y_f, 0}
|
||||
// ocut_const = sum*(y_l*d : l in L, y_l > 0)
|
||||
QCHECK_EQ(second_stage_result.termination_reason,
|
||||
SolveResultProto::OPTIMAL);
|
||||
QCHECK_EQ(second_stage_result.termination.reason,
|
||||
TerminationReason::kOptimal)
|
||||
<< second_stage_result.termination;
|
||||
LOG(INFO) << "Adding optimality cut...";
|
||||
LinearExpression optimality_cut_expression;
|
||||
double upper_bound = 0.0;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Simple linear programming example
|
||||
// Simple integer programming example
|
||||
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
@@ -24,12 +24,10 @@
|
||||
#include "ortools/math_opt/cpp/math_opt.h"
|
||||
|
||||
namespace {
|
||||
using ::operations_research::math_opt::MathOpt;
|
||||
using ::operations_research::math_opt::Result;
|
||||
using ::operations_research::math_opt::SolveParametersProto;
|
||||
using ::operations_research::math_opt::SOLVER_TYPE_GSCIP;
|
||||
using ::operations_research::math_opt::SolveResultProto;
|
||||
using ::operations_research::math_opt::SolveStatsProto;
|
||||
using ::operations_research::math_opt::Model;
|
||||
using ::operations_research::math_opt::SolveResult;
|
||||
using ::operations_research::math_opt::SolverType;
|
||||
using ::operations_research::math_opt::TerminationReason;
|
||||
using ::operations_research::math_opt::Variable;
|
||||
using ::operations_research::math_opt::VariableMap;
|
||||
|
||||
@@ -43,32 +41,32 @@ constexpr double kInf = std::numeric_limits<double>::infinity();
|
||||
// y in {0.0, 1.0, 2.0, ...,
|
||||
//
|
||||
void SolveSimpleMIP() {
|
||||
MathOpt optimizer(SOLVER_TYPE_GSCIP, "Integer programming example");
|
||||
Model model("Integer programming example");
|
||||
|
||||
// Variables
|
||||
const Variable x = optimizer.AddIntegerVariable(0.0, kInf, "x");
|
||||
const Variable y = optimizer.AddIntegerVariable(0.0, kInf, "y");
|
||||
const Variable x = model.AddIntegerVariable(0.0, kInf, "x");
|
||||
const Variable y = model.AddIntegerVariable(0.0, kInf, "y");
|
||||
|
||||
// Constraints
|
||||
optimizer.AddLinearConstraint(x + 7 * y <= 17.5, "c1");
|
||||
optimizer.AddLinearConstraint(x <= 3.5, "c2");
|
||||
model.AddLinearConstraint(x + 7 * y <= 17.5, "c1");
|
||||
model.AddLinearConstraint(x <= 3.5, "c2");
|
||||
|
||||
// Objective
|
||||
optimizer.objective().Maximize(x + 10 * y);
|
||||
model.Maximize(x + 10 * y);
|
||||
|
||||
std::cout << "Num variables: " << optimizer.num_variables() << std::endl;
|
||||
std::cout << "Num constraints: " << optimizer.num_linear_constraints()
|
||||
std::cout << "Num variables: " << model.num_variables() << std::endl;
|
||||
std::cout << "Num constraints: " << model.num_linear_constraints()
|
||||
<< std::endl;
|
||||
|
||||
const Result result = optimizer.Solve(SolveParametersProto()).value();
|
||||
const SolveResult result = Solve(model, SolverType::kGscip).value();
|
||||
|
||||
// Check for warnings.
|
||||
for (const auto& warning : result.warnings) {
|
||||
LOG(ERROR) << "Solver warning: " << warning << std::endl;
|
||||
}
|
||||
// Check that the problem has an optimal solution.
|
||||
QCHECK_EQ(result.termination_reason, SolveResultProto::OPTIMAL)
|
||||
<< "Failed to find an optimal solution: " << result.termination_detail;
|
||||
QCHECK_EQ(result.termination.reason, TerminationReason::kOptimal)
|
||||
<< "Failed to find an optimal solution: " << result.termination;
|
||||
|
||||
std::cout << "Problem solved in " << result.solve_time() << std::endl;
|
||||
std::cout << "Objective value: " << result.objective_value() << std::endl;
|
||||
@@ -78,10 +76,6 @@ void SolveSimpleMIP() {
|
||||
|
||||
std::cout << "Variable values: [x=" << x_val << ", y=" << y_val << "]"
|
||||
<< std::endl;
|
||||
const SolveStatsProto& stat = result.solve_stats;
|
||||
std::cout << "Simplex iterations: " << stat.simplex_iterations() << std::endl;
|
||||
std::cout << "Barrier iterations: " << stat.barrier_iterations() << std::endl;
|
||||
std::cout << "Branch and bound nodes: " << stat.node_count() << std::endl;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -115,10 +115,11 @@ constexpr double kZeroTol = 1.0e-8;
|
||||
namespace {
|
||||
using ::operations_research::MathUtil;
|
||||
using ::operations_research::math_opt::LinearExpression;
|
||||
using ::operations_research::math_opt::MathOpt;
|
||||
using ::operations_research::math_opt::Result;
|
||||
using ::operations_research::math_opt::SolveParametersProto;
|
||||
using ::operations_research::math_opt::Model;
|
||||
using ::operations_research::math_opt::SolveArguments;
|
||||
using ::operations_research::math_opt::SolveResult;
|
||||
using ::operations_research::math_opt::SolverType;
|
||||
using ::operations_research::math_opt::TerminationReason;
|
||||
using ::operations_research::math_opt::Variable;
|
||||
using ::operations_research::math_opt::VariableMap;
|
||||
|
||||
@@ -138,10 +139,8 @@ struct Graph {
|
||||
};
|
||||
|
||||
struct FlowModel {
|
||||
explicit FlowModel(SolverType solver_type) {
|
||||
model = std::make_unique<MathOpt>(solver_type, "LagrangianProblem");
|
||||
}
|
||||
std::unique_ptr<MathOpt> model;
|
||||
FlowModel() : model(std::make_unique<Model>("LagrangianProblem")) {}
|
||||
std::unique_ptr<Model> model;
|
||||
LinearExpression cost;
|
||||
LinearExpression resource_1;
|
||||
LinearExpression resource_2;
|
||||
@@ -150,8 +149,8 @@ struct FlowModel {
|
||||
|
||||
// Populates `model` with variables and constraints of a shortest path problem.
|
||||
FlowModel CreateShortestPathModel(const Graph graph) {
|
||||
FlowModel flow_model(operations_research::math_opt::SOLVER_TYPE_GSCIP);
|
||||
MathOpt& model = *flow_model.model;
|
||||
FlowModel flow_model;
|
||||
Model& model = *flow_model.model;
|
||||
for (const Arc& arc : graph.arcs) {
|
||||
Variable var = model.AddContinuousVariable(
|
||||
/*lower_bound=*/0, /*upper_bound=*/1,
|
||||
@@ -210,8 +209,8 @@ Graph CreateSampleNetwork() {
|
||||
// Solves the constrained shortest path as an MIP.
|
||||
FlowModel SolveMip(const Graph graph, const double max_resource_1,
|
||||
const double max_resource_2) {
|
||||
FlowModel flow_model(operations_research::math_opt::SOLVER_TYPE_GSCIP);
|
||||
MathOpt& model = *flow_model.model;
|
||||
FlowModel flow_model;
|
||||
Model& model = *flow_model.model;
|
||||
for (const Arc& arc : graph.arcs) {
|
||||
Variable var = model.AddBinaryVariable(
|
||||
/*name=*/absl::StrFormat("x_%d_%d", arc.i, arc.j));
|
||||
@@ -240,10 +239,8 @@ FlowModel SolveMip(const Graph graph, const double max_resource_1,
|
||||
"resource_ctr_1");
|
||||
model.AddLinearConstraint(flow_model.resource_2 <= max_resource_2,
|
||||
"resource_ctr_2");
|
||||
model.objective().Minimize(flow_model.cost);
|
||||
SolveParametersProto params;
|
||||
params.mutable_common_parameters()->set_enable_output(false);
|
||||
const Result result = model.Solve(params).value();
|
||||
model.Minimize(flow_model.cost);
|
||||
const SolveResult result = Solve(model, SolverType::kGscip).value();
|
||||
const VariableMap<double>& variable_values = result.variable_values();
|
||||
std::cout << "MIP Solution with 2 side constraints" << std::endl;
|
||||
std::cout << absl::StrFormat("MIP objective value: %6.3f",
|
||||
@@ -262,10 +259,8 @@ FlowModel SolveMip(const Graph graph, const double max_resource_1,
|
||||
void SolveLinearRelaxation(FlowModel& flow_model, const Graph& graph,
|
||||
const double max_resource_1,
|
||||
const double max_resource_2) {
|
||||
MathOpt& model = *flow_model.model;
|
||||
SolveParametersProto params;
|
||||
params.mutable_common_parameters()->set_enable_output(false);
|
||||
const Result result = model.Solve(params).value();
|
||||
Model& model = *flow_model.model;
|
||||
const SolveResult result = Solve(model, SolverType::kGscip).value();
|
||||
const VariableMap<double>& variable_values = result.variable_values();
|
||||
std::cout << "LP relaxation with 2 side constraints" << std::endl;
|
||||
std::cout << absl::StrFormat("LP objective value: %6.3f",
|
||||
@@ -282,12 +277,10 @@ void SolveLagrangianRelaxation(const Graph graph, const double max_resource_1,
|
||||
const double max_resource_2) {
|
||||
// Model, variables, and linear expressions.
|
||||
FlowModel flow_model = CreateShortestPathModel(graph);
|
||||
MathOpt& model = *flow_model.model;
|
||||
Model& model = *flow_model.model;
|
||||
LinearExpression& cost = flow_model.cost;
|
||||
LinearExpression& resource_1 = flow_model.resource_1;
|
||||
LinearExpression& resource_2 = flow_model.resource_2;
|
||||
SolveParametersProto params;
|
||||
params.mutable_common_parameters()->set_enable_output(false);
|
||||
|
||||
// Dualized constraints and dual variable iterates.
|
||||
std::vector<double> mu;
|
||||
@@ -307,14 +300,14 @@ void SolveLagrangianRelaxation(const Graph graph, const double max_resource_1,
|
||||
grad_mu.push_back(max_resource_1 - resource_1);
|
||||
model.AddLinearConstraint(resource_2 <= max_resource_2);
|
||||
for (Variable& var : flow_model.flow_vars) {
|
||||
var.set_integer();
|
||||
model.set_integer(var);
|
||||
}
|
||||
} else if (!dualized_resource_1 && dualized_resource_2) {
|
||||
mu.push_back(initial_dual_value);
|
||||
grad_mu.push_back(max_resource_2 - resource_2);
|
||||
model.AddLinearConstraint(resource_1 <= max_resource_1);
|
||||
for (Variable& var : flow_model.flow_vars) {
|
||||
var.set_integer();
|
||||
model.set_integer(var);
|
||||
}
|
||||
} else {
|
||||
mu.push_back(initial_dual_value);
|
||||
@@ -334,8 +327,8 @@ void SolveLagrangianRelaxation(const Graph graph, const double max_resource_1,
|
||||
<< "Number of iterations must be strictly positive.";
|
||||
|
||||
// Upper and lower bounds on the full problem.
|
||||
double upper_bound = std::numeric_limits<double>().infinity();
|
||||
double lower_bound = -std::numeric_limits<double>().infinity();
|
||||
double upper_bound = std::numeric_limits<double>::infinity();
|
||||
double lower_bound = -std::numeric_limits<double>::infinity();
|
||||
double best_solution_resource_1 = 0;
|
||||
double best_solution_resource_2 = 0;
|
||||
|
||||
@@ -352,8 +345,10 @@ void SolveLagrangianRelaxation(const Graph graph, const double max_resource_1,
|
||||
for (int k = 0; k < mu.size(); ++k) {
|
||||
lagrangian_function += mu[k] * grad_mu[k];
|
||||
}
|
||||
model.objective().Minimize(lagrangian_function);
|
||||
Result result = model.Solve(params).value();
|
||||
model.Minimize(lagrangian_function);
|
||||
SolveResult result = Solve(model, SolverType::kGscip).value();
|
||||
CHECK_EQ(result.termination.reason, TerminationReason::kOptimal)
|
||||
<< result.termination;
|
||||
const VariableMap<double>& vars_val = result.variable_values();
|
||||
bool feasible = true;
|
||||
|
||||
@@ -430,9 +425,9 @@ void SolveLagrangianRelaxation(const Graph graph, const double max_resource_1,
|
||||
|
||||
void RelaxModel(FlowModel& flow_model) {
|
||||
for (Variable& var : flow_model.flow_vars) {
|
||||
var.set_continuous();
|
||||
var.set_lower_bound(0.0);
|
||||
var.set_upper_bound(1.0);
|
||||
flow_model.model->set_continuous(var);
|
||||
flow_model.model->set_lower_bound(var, 0.0);
|
||||
flow_model.model->set_upper_bound(var, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,13 +30,11 @@
|
||||
namespace {
|
||||
using ::operations_research::math_opt::LinearConstraint;
|
||||
using ::operations_research::math_opt::LinearExpression;
|
||||
using ::operations_research::math_opt::MathOpt;
|
||||
using ::operations_research::math_opt::Result;
|
||||
using ::operations_research::math_opt::SolveParametersProto;
|
||||
using ::operations_research::math_opt::SOLVER_TYPE_GLOP;
|
||||
using ::operations_research::math_opt::SolveResultProto;
|
||||
using ::operations_research::math_opt::SolveStatsProto;
|
||||
using ::operations_research::math_opt::Model;
|
||||
using ::operations_research::math_opt::SolveResult;
|
||||
using ::operations_research::math_opt::SolverType;
|
||||
using ::operations_research::math_opt::Sum;
|
||||
using ::operations_research::math_opt::TerminationReason;
|
||||
using ::operations_research::math_opt::Variable;
|
||||
|
||||
constexpr double kInf = std::numeric_limits<double>::infinity();
|
||||
@@ -51,40 +49,39 @@ constexpr double kInf = std::numeric_limits<double>::infinity();
|
||||
// x2 in [0, infinity)
|
||||
//
|
||||
void SolveSimpleLp() {
|
||||
MathOpt optimizer(SOLVER_TYPE_GLOP, "Linear programming example");
|
||||
Model model("Linear programming example");
|
||||
|
||||
// Variables
|
||||
std::vector<Variable> x;
|
||||
for (int j = 0; j < 3; j++) {
|
||||
x.push_back(
|
||||
optimizer.AddContinuousVariable(0.0, kInf, absl::StrCat("x", j)));
|
||||
x.push_back(model.AddContinuousVariable(0.0, kInf, absl::StrCat("x", j)));
|
||||
}
|
||||
|
||||
// Constraints
|
||||
std::vector<LinearConstraint> constraints;
|
||||
constraints.push_back(optimizer.AddLinearConstraint(
|
||||
10 * x[0] + 4 * x[1] + 5 * x[2] <= 600, "c1"));
|
||||
constraints.push_back(optimizer.AddLinearConstraint(
|
||||
2 * x[0] + 2 * x[1] + 6 * x[2] <= 300, "c2"));
|
||||
constraints.push_back(
|
||||
model.AddLinearConstraint(10 * x[0] + 4 * x[1] + 5 * x[2] <= 600, "c1"));
|
||||
constraints.push_back(
|
||||
model.AddLinearConstraint(2 * x[0] + 2 * x[1] + 6 * x[2] <= 300, "c2"));
|
||||
// sum(x[i]) <= 100
|
||||
constraints.push_back(optimizer.AddLinearConstraint(Sum(x) <= 100, "c3"));
|
||||
constraints.push_back(model.AddLinearConstraint(Sum(x) <= 100, "c3"));
|
||||
|
||||
// Objective
|
||||
optimizer.objective().Maximize(10 * x[0] + 6 * x[1] + 4 * x[2]);
|
||||
model.Maximize(10 * x[0] + 6 * x[1] + 4 * x[2]);
|
||||
|
||||
std::cout << "Num variables: " << optimizer.num_variables() << std::endl;
|
||||
std::cout << "Num constraints: " << optimizer.num_linear_constraints()
|
||||
std::cout << "Num variables: " << model.num_variables() << std::endl;
|
||||
std::cout << "Num constraints: " << model.num_linear_constraints()
|
||||
<< std::endl;
|
||||
|
||||
const Result result = optimizer.Solve(SolveParametersProto()).value();
|
||||
const SolveResult result = Solve(model, SolverType::kGlop).value();
|
||||
|
||||
// Check for warnings.
|
||||
for (const auto& warning : result.warnings) {
|
||||
LOG(ERROR) << "Solver warning: " << warning << std::endl;
|
||||
}
|
||||
// Check that the problem has an optimal solution.
|
||||
QCHECK_EQ(result.termination_reason, SolveResultProto::OPTIMAL)
|
||||
<< "Failed to find an optimal solution: " << result.termination_detail;
|
||||
QCHECK_EQ(result.termination.reason, TerminationReason::kOptimal)
|
||||
<< "Failed to find an optimal solution: " << result.termination;
|
||||
|
||||
std::cout << "Problem solved in " << result.solve_time() << std::endl;
|
||||
std::cout << "Objective value: " << result.objective_value() << std::endl;
|
||||
@@ -98,11 +95,8 @@ void SolveSimpleLp() {
|
||||
std::cout << "Reduced costs: ["
|
||||
<< absl::StrJoin(result.reduced_costs().Values(x), ", ") << "]"
|
||||
<< std::endl;
|
||||
const SolveStatsProto& stat = result.solve_stats;
|
||||
std::cout << "Simplex iterations: " << stat.simplex_iterations() << std::endl;
|
||||
std::cout << "Barrier iterations: " << stat.barrier_iterations() << std::endl;
|
||||
|
||||
// TODO(user): add basis statuses when they are included in Result
|
||||
// TODO(user): add basis statuses when they are included in SolveResult
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -18,6 +18,21 @@ package operations_research.math_opt;
|
||||
|
||||
import "ortools/math_opt/sparse_containers.proto";
|
||||
|
||||
option java_package = "com.google.ortools.mathopt";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// Feasibility of a primal or dual solution as claimed by the solver.
|
||||
enum SolutionStatusProto {
|
||||
// Guard value representing no status.
|
||||
SOLUTION_STATUS_UNSPECIFIED = 0;
|
||||
// Solver does not claim a feasibility status.
|
||||
SOLUTION_STATUS_UNDETERMINED = 1;
|
||||
// Solver claims the solution is feasible.
|
||||
SOLUTION_STATUS_FEASIBLE = 2;
|
||||
// Solver claims the solution is infeasible.
|
||||
SOLUTION_STATUS_INFEASIBLE = 3;
|
||||
}
|
||||
|
||||
// A solution to an optimization problem.
|
||||
//
|
||||
// E.g. consider a simple linear program:
|
||||
@@ -37,9 +52,10 @@ message PrimalSolutionProto {
|
||||
SparseDoubleVectorProto variable_values = 1;
|
||||
|
||||
// Objective value as computed by the underlying solver.
|
||||
optional double objective_value = 2;
|
||||
double objective_value = 2;
|
||||
|
||||
// TODO(b/185365397): indicate if the solution is feasible.
|
||||
// Feasibility status of the solution according to the underlying solver.
|
||||
SolutionStatusProto feasibility_status = 3;
|
||||
}
|
||||
|
||||
// A direction of unbounded improvement to an optimization problem;
|
||||
@@ -97,10 +113,12 @@ message DualSolutionProto {
|
||||
// * reduced_costs.values must all be finite.
|
||||
SparseDoubleVectorProto reduced_costs = 2;
|
||||
|
||||
// TODO(b/195295177): consider making this non-optional
|
||||
// Objective value as computed by the underlying solver.
|
||||
optional double objective_value = 3;
|
||||
|
||||
// TODO(b/185365397): indicate if the solution is feasible.
|
||||
// Feasibility status of the solution according to the underlying solver.
|
||||
SolutionStatusProto feasibility_status = 4;
|
||||
}
|
||||
|
||||
// A direction of unbounded improvement to the dual of an optimization,
|
||||
@@ -137,13 +155,25 @@ message DualRayProto {
|
||||
// TODO(b/185365397): indicate if the ray is feasible.
|
||||
}
|
||||
|
||||
enum BasisStatus {
|
||||
INVALID = 0;
|
||||
FREE = 1;
|
||||
AT_LOWER_BOUND = 2;
|
||||
AT_UPPER_BOUND = 3;
|
||||
FIXED_VALUE = 4;
|
||||
BASIC = 5;
|
||||
// Status of a variable/constraint in a LP basis.
|
||||
enum BasisStatusProto {
|
||||
// Guard value representing no status.
|
||||
BASIS_STATUS_UNSPECIFIED = 0;
|
||||
|
||||
// The variable/constraint is free (it has no finite bounds).
|
||||
BASIS_STATUS_FREE = 1;
|
||||
|
||||
// The variable/constraint is at its lower bound (which must be finite).
|
||||
BASIS_STATUS_AT_LOWER_BOUND = 2;
|
||||
|
||||
// The variable/constraint is at its upper bound (which must be finite).
|
||||
BASIS_STATUS_AT_UPPER_BOUND = 3;
|
||||
|
||||
// The variable/constraint has identical finite lower and upper bounds.
|
||||
BASIS_STATUS_FIXED_VALUE = 4;
|
||||
|
||||
// The variable/constraint is basic.
|
||||
BASIS_STATUS_BASIC = 5;
|
||||
}
|
||||
|
||||
// A sparse representation of a vector of basis statuses.
|
||||
@@ -152,14 +182,14 @@ message SparseBasisStatusVector {
|
||||
repeated int64 ids = 1;
|
||||
|
||||
// Must have equal length to ids.
|
||||
repeated BasisStatus values = 2;
|
||||
repeated BasisStatusProto values = 2;
|
||||
}
|
||||
|
||||
// A combinatorial characterization for a solution to a linear program.
|
||||
//
|
||||
// The simplex method for solving linear programs always returns a "basic
|
||||
// feasible solution" which can be described combinatorially by a Basis. A basis
|
||||
// assigns a BasisStatus for every variable and linear constraint.
|
||||
// assigns a BasisStatusProto for every variable and linear constraint.
|
||||
//
|
||||
// E.g. consider a standard form LP:
|
||||
// min c * x
|
||||
@@ -193,4 +223,26 @@ message BasisProto {
|
||||
// Requirements:
|
||||
// * constraint_status.ids is equal to VariablesProto.ids.
|
||||
SparseBasisStatusVector variable_status = 2;
|
||||
|
||||
// This is an advanced status. For single-sided LPs it should be equal to the
|
||||
// feasibility status of the associated dual solution. For two-sided LPs it
|
||||
// may be different in some edge cases (e.g. incomplete solves with primal
|
||||
// simplex). For more details see go/mathopt-basis-advanced#dualfeasibility.
|
||||
SolutionStatusProto basic_dual_feasibility = 3;
|
||||
}
|
||||
|
||||
// What is included in a solution depends on the kind of problem and solver.
|
||||
// The current common patterns are
|
||||
// 1. MIP solvers return only a primal solution.
|
||||
// 2. Simplex LP solvers often return a basis and the primal and dual
|
||||
// solutions associated to this basis.
|
||||
// 3. Other continuous solvers often return a primal and dual solution
|
||||
// solution that are connected in a solver-dependent form.
|
||||
//
|
||||
// Requirements:
|
||||
// * at least one field must be set; a solution can't be empty.
|
||||
message SolutionProto {
|
||||
optional PrimalSolutionProto primal_solution = 1;
|
||||
optional DualSolutionProto dual_solution = 2;
|
||||
optional BasisProto basis = 3;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
|
||||
package(default_visibility = ["//ortools/math_opt:__subpackages__"])
|
||||
|
||||
cc_library(
|
||||
@@ -9,25 +11,29 @@ cc_library(
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":gscip_solver_callback",
|
||||
":gscip_solver_message_callback_handler",
|
||||
"//ortools/base",
|
||||
"//ortools/base:cleanup",
|
||||
"//ortools/base:map_util",
|
||||
"//ortools/base:protoutil",
|
||||
"//ortools/base:status_macros",
|
||||
"//ortools/gscip",
|
||||
"//ortools/gscip:gscip_cc_proto",
|
||||
"//ortools/gscip:gscip_event_handler",
|
||||
"//ortools/gscip:gscip_parameters",
|
||||
"//ortools/linear_solver:scip_with_glop",
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
"//ortools/math_opt/core:math_opt_proto_utils",
|
||||
"//ortools/math_opt:model_cc_proto",
|
||||
"//ortools/math_opt:model_parameters_cc_proto",
|
||||
"//ortools/math_opt:model_update_cc_proto",
|
||||
"//ortools/math_opt:parameters_cc_proto",
|
||||
"//ortools/math_opt:result_cc_proto",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"//ortools/math_opt/core:math_opt_proto_utils",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"//ortools/math_opt/core:sparse_vector_view",
|
||||
"//ortools/math_opt/validators:callback_validator",
|
||||
"//ortools/port:proto_utils",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
@@ -54,11 +60,12 @@ cc_library(
|
||||
"//ortools/base:status_macros",
|
||||
"//ortools/gurobi:environment",
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
"//ortools/math_opt/core:math_opt_proto_utils",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"//ortools/math_opt/core:math_opt_proto_utils",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"//ortools/math_opt/core:sparse_vector_view",
|
||||
"//ortools/math_opt/solvers/gurobi:g_gurobi",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
@@ -72,30 +79,38 @@ cc_library(
|
||||
cc_library(
|
||||
name = "gurobi_solver",
|
||||
srcs = [
|
||||
"gurobi_init_arguments.cc",
|
||||
"gurobi_solver.cc",
|
||||
"gurobi_solver.h",
|
||||
],
|
||||
hdrs = [
|
||||
"gurobi_init_arguments.h",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":gurobi_callback",
|
||||
":gurobi_cc_proto",
|
||||
":message_callback_data",
|
||||
"//ortools/base",
|
||||
"//ortools/base:cleanup",
|
||||
"//ortools/base:linked_hash_map",
|
||||
"//ortools/base:map_util",
|
||||
"//ortools/base:protoutil",
|
||||
"//ortools/base:status_macros",
|
||||
"//ortools/gurobi:environment",
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
"//ortools/math_opt/core:math_opt_proto_utils",
|
||||
"//ortools/math_opt:model_cc_proto",
|
||||
"//ortools/math_opt:model_parameters_cc_proto",
|
||||
"//ortools/math_opt:model_update_cc_proto",
|
||||
"//ortools/math_opt:parameters_cc_proto",
|
||||
"//ortools/math_opt:result_cc_proto",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"//ortools/math_opt/core:math_opt_proto_utils",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"//ortools/math_opt/core:sparse_vector_view",
|
||||
"//ortools/math_opt/solvers/gurobi:g_gurobi",
|
||||
"//ortools/math_opt/validators:callback_validator",
|
||||
"//ortools/port:proto_utils",
|
||||
"@com_google_absl//absl/cleanup",
|
||||
"@com_google_absl//absl/memory",
|
||||
@@ -118,6 +133,7 @@ cc_library(
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//ortools/base",
|
||||
"//ortools/base:cleanup",
|
||||
"//ortools/base:int_type",
|
||||
"//ortools/base:map_util",
|
||||
"//ortools/base:protoutil",
|
||||
@@ -128,16 +144,17 @@ cc_library(
|
||||
"//ortools/lp_data",
|
||||
"//ortools/lp_data:base",
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
"//ortools/math_opt/core:math_opt_proto_utils",
|
||||
"//ortools/math_opt:model_cc_proto",
|
||||
"//ortools/math_opt:model_parameters_cc_proto",
|
||||
"//ortools/math_opt:model_update_cc_proto",
|
||||
"//ortools/math_opt:parameters_cc_proto",
|
||||
"//ortools/math_opt:result_cc_proto",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"//ortools/math_opt/core:math_opt_proto_utils",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"//ortools/math_opt/core:sparse_vector_view",
|
||||
"//ortools/math_opt/validators:callback_validator",
|
||||
"//ortools/port:proto_utils",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/memory",
|
||||
@@ -165,16 +182,19 @@ cc_library(
|
||||
# For sat_proto_solver.h/cc, this needs to be broken up.
|
||||
"//ortools/linear_solver",
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
"//ortools/math_opt/core:math_opt_proto_utils",
|
||||
"//ortools/math_opt:model_cc_proto",
|
||||
"//ortools/math_opt:model_parameters_cc_proto",
|
||||
"//ortools/math_opt:model_update_cc_proto",
|
||||
"//ortools/math_opt:parameters_cc_proto",
|
||||
"//ortools/math_opt/io:proto_converter",
|
||||
"//ortools/math_opt:result_cc_proto",
|
||||
"//ortools/math_opt:solution_cc_proto",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"//ortools/math_opt:sparse_containers_cc_proto",
|
||||
"//ortools/math_opt/core:math_opt_proto_utils",
|
||||
"//ortools/math_opt/core:solve_interrupter",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"//ortools/math_opt/core:sparse_vector_view",
|
||||
"//ortools/math_opt/io:proto_converter",
|
||||
"//ortools/math_opt/validators:callback_validator",
|
||||
"//ortools/port:proto_utils",
|
||||
"//ortools/sat:sat_parameters_cc_proto",
|
||||
"@com_google_absl//absl/memory",
|
||||
@@ -191,8 +211,6 @@ cc_library(
|
||||
srcs = ["message_callback_data.cc"],
|
||||
hdrs = ["message_callback_data.h"],
|
||||
deps = [
|
||||
"//ortools/math_opt:callback_cc_proto",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/types:optional",
|
||||
],
|
||||
)
|
||||
@@ -202,7 +220,6 @@ cc_library(
|
||||
srcs = ["gscip_solver_callback.cc"],
|
||||
hdrs = ["gscip_solver_callback.h"],
|
||||
deps = [
|
||||
":message_callback_data",
|
||||
"//ortools/base",
|
||||
"//ortools/base:protoutil",
|
||||
"//ortools/base:status_macros",
|
||||
@@ -218,9 +235,32 @@ cc_library(
|
||||
"@com_google_absl//absl/memory",
|
||||
"@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/time",
|
||||
"@com_google_absl//absl/types:optional",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "gscip_solver_message_callback_handler",
|
||||
srcs = ["gscip_solver_message_callback_handler.cc"],
|
||||
hdrs = ["gscip_solver_message_callback_handler.h"],
|
||||
deps = [
|
||||
":message_callback_data",
|
||||
"//ortools/gscip",
|
||||
"//ortools/gscip:gscip_message_handler",
|
||||
"//ortools/math_opt/core:solver_interface",
|
||||
"@com_google_absl//absl/base:core_headers",
|
||||
"@com_google_absl//absl/strings",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "gurobi_proto",
|
||||
srcs = ["gurobi.proto"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "gurobi_cc_proto",
|
||||
deps = [":gurobi_proto"],
|
||||
)
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
|
||||
#include "ortools/math_opt/solvers/cp_sat_solver.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -25,14 +28,19 @@
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/match.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "ortools/linear_solver/linear_solver.pb.h"
|
||||
#include "ortools/linear_solver/sat_proto_solver.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/math_opt_proto_utils.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/io/proto_converter.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_parameters.pb.h"
|
||||
@@ -41,8 +49,10 @@
|
||||
#include "ortools/math_opt/result.pb.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
#include "ortools/math_opt/validators/callback_validator.h"
|
||||
#include "ortools/port/proto_utils.h"
|
||||
#include "ortools/sat/sat_parameters.pb.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
#include "ortools/base/protoutil.h"
|
||||
|
||||
@@ -53,26 +63,15 @@ namespace {
|
||||
|
||||
constexpr double kInf = std::numeric_limits<double>::infinity();
|
||||
|
||||
void SetTrivialBounds(const bool maximize, SolveStatsProto& stats) {
|
||||
stats.set_best_primal_bound(maximize ? -kInf : kInf);
|
||||
stats.set_best_dual_bound(maximize ? kInf : -kInf);
|
||||
}
|
||||
|
||||
// Returns a list of warnings from parameter settings that were
|
||||
// invalid/unsupported (specific to CP-SAT), one element per bad parameter.
|
||||
std::vector<std::string> SetSolveParameters(
|
||||
const SolveParametersProto& parameters, MPModelRequest& request) {
|
||||
const SolveParametersProto& parameters, const bool has_message_callback,
|
||||
MPModelRequest& request) {
|
||||
std::vector<std::string> warnings;
|
||||
const CommonSolveParametersProto& common_parameters =
|
||||
parameters.common_parameters();
|
||||
if (common_parameters.has_time_limit()) {
|
||||
if (parameters.has_time_limit()) {
|
||||
request.set_solver_time_limit_seconds(absl::ToDoubleSeconds(
|
||||
util_time::DecodeGoogleApiProto(common_parameters.time_limit())
|
||||
.value()));
|
||||
}
|
||||
if (common_parameters.has_enable_output()) {
|
||||
request.set_enable_internal_solver_output(
|
||||
common_parameters.enable_output());
|
||||
util_time::DecodeGoogleApiProto(parameters.time_limit()).value()));
|
||||
}
|
||||
|
||||
// Build CP SAT parameters by first initializing them from the common
|
||||
@@ -83,20 +82,33 @@ std::vector<std::string> SetSolveParameters(
|
||||
// `request.solver_time_limit_seconds`. The logic of `SatSolveProto()` will
|
||||
// apply the logic we want here.
|
||||
sat::SatParameters sat_parameters;
|
||||
if (common_parameters.has_random_seed()) {
|
||||
sat_parameters.set_random_seed(common_parameters.random_seed());
|
||||
|
||||
// By default CP-SAT catches SIGINT (Ctrl-C) to interrupt the solve but we
|
||||
// don't want this behavior when the users uses CP-SAT through MathOpt.
|
||||
sat_parameters.set_catch_sigint_signal(false);
|
||||
|
||||
if (parameters.has_random_seed()) {
|
||||
sat_parameters.set_random_seed(parameters.random_seed());
|
||||
}
|
||||
if (common_parameters.has_threads()) {
|
||||
sat_parameters.set_num_search_workers(common_parameters.threads());
|
||||
if (parameters.has_threads()) {
|
||||
sat_parameters.set_num_search_workers(parameters.threads());
|
||||
}
|
||||
if (common_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
|
||||
if (parameters.has_relative_gap_limit()) {
|
||||
sat_parameters.set_relative_gap_limit(parameters.relative_gap_limit());
|
||||
}
|
||||
|
||||
if (parameters.has_absolute_gap_limit()) {
|
||||
sat_parameters.set_absolute_gap_limit(parameters.absolute_gap_limit());
|
||||
}
|
||||
|
||||
if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
|
||||
warnings.push_back(
|
||||
absl::StrCat("Setting the LP Algorithm (was set to ",
|
||||
ProtoEnumToString(common_parameters.lp_algorithm()),
|
||||
ProtoEnumToString(parameters.lp_algorithm()),
|
||||
") is not supported for CP_SAT solver"));
|
||||
}
|
||||
if (common_parameters.presolve() != EMPHASIS_UNSPECIFIED) {
|
||||
switch (common_parameters.presolve()) {
|
||||
if (parameters.presolve() != EMPHASIS_UNSPECIFIED) {
|
||||
switch (parameters.presolve()) {
|
||||
case EMPHASIS_OFF:
|
||||
sat_parameters.set_cp_model_presolve(false);
|
||||
break;
|
||||
@@ -108,18 +120,17 @@ std::vector<std::string> SetSolveParameters(
|
||||
break;
|
||||
default:
|
||||
LOG(FATAL) << "Presolve emphasis: "
|
||||
<< ProtoEnumToString(common_parameters.presolve())
|
||||
<< ProtoEnumToString(parameters.presolve())
|
||||
<< " unknown, error setting CP-SAT parameters";
|
||||
}
|
||||
}
|
||||
if (common_parameters.scaling() != EMPHASIS_UNSPECIFIED) {
|
||||
warnings.push_back(
|
||||
absl::StrCat("Setting the scaling (was set to ",
|
||||
ProtoEnumToString(common_parameters.scaling()),
|
||||
") is not supported for CP_SAT solver"));
|
||||
if (parameters.scaling() != EMPHASIS_UNSPECIFIED) {
|
||||
warnings.push_back(absl::StrCat("Setting the scaling (was set to ",
|
||||
ProtoEnumToString(parameters.scaling()),
|
||||
") is not supported for CP_SAT solver"));
|
||||
}
|
||||
if (common_parameters.cuts() != EMPHASIS_UNSPECIFIED) {
|
||||
switch (common_parameters.cuts()) {
|
||||
if (parameters.cuts() != EMPHASIS_UNSPECIFIED) {
|
||||
switch (parameters.cuts()) {
|
||||
case EMPHASIS_OFF:
|
||||
// This is not very maintainable, but CP-SAT doesn't expose the
|
||||
// parameters we need.
|
||||
@@ -136,88 +147,112 @@ std::vector<std::string> SetSolveParameters(
|
||||
case EMPHASIS_VERY_HIGH:
|
||||
break;
|
||||
default:
|
||||
LOG(FATAL) << "Cut emphasis: "
|
||||
<< ProtoEnumToString(common_parameters.cuts())
|
||||
LOG(FATAL) << "Cut emphasis: " << ProtoEnumToString(parameters.cuts())
|
||||
<< " unknown, error setting CP-SAT parameters";
|
||||
}
|
||||
}
|
||||
if (common_parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
|
||||
warnings.push_back(
|
||||
absl::StrCat("Setting the heuristics (was set to ",
|
||||
ProtoEnumToString(common_parameters.heuristics()),
|
||||
") is not supported for CP_SAT solver"));
|
||||
if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
|
||||
warnings.push_back(absl::StrCat("Setting the heuristics (was set to ",
|
||||
ProtoEnumToString(parameters.heuristics()),
|
||||
") is not supported for CP_SAT solver"));
|
||||
}
|
||||
sat_parameters.MergeFrom(parameters.cp_sat_parameters());
|
||||
sat_parameters.MergeFrom(parameters.cp_sat());
|
||||
|
||||
// We want to override specifically SAT parameters independently from the user
|
||||
// input when a message callback is used to prevent wrongful writes to stdout
|
||||
// or disabling of messages via these parameters.
|
||||
if (has_message_callback) {
|
||||
// When enable_internal_solver_output is used, CP-SAT solver actually has
|
||||
// the same effect as setting log_search_progress to true.
|
||||
sat_parameters.set_log_search_progress(true);
|
||||
|
||||
// Default value of log_to_stdout is true; but even if it was not the case,
|
||||
// we don't want to write to stdout when a message callback is used.
|
||||
sat_parameters.set_log_to_stdout(false);
|
||||
} else {
|
||||
// We only set enable_internal_solver_output when we have no message
|
||||
// callback.
|
||||
request.set_enable_internal_solver_output(parameters.enable_output());
|
||||
}
|
||||
|
||||
request.set_solver_specific_parameters(
|
||||
EncodeSatParametersAsString(sat_parameters));
|
||||
return warnings;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
absl::StatusOr<std::pair<SolveStatsProto, TerminationProto>>
|
||||
GetTerminationAndStats(const bool is_interrupted, const bool maximize,
|
||||
const MPSolutionResponse& response) {
|
||||
SolveStatsProto solve_stats;
|
||||
TerminationProto termination;
|
||||
|
||||
absl::StatusOr<std::unique_ptr<SolverInterface>> CpSatSolver::New(
|
||||
const ModelProto& model, const SolverInitializerProto& initializer) {
|
||||
ASSIGN_OR_RETURN(MPModelProto cp_sat_model,
|
||||
MathOptModelToMPModelProto(model));
|
||||
std::vector variable_ids(model.variables().ids().begin(),
|
||||
model.variables().ids().end());
|
||||
// We must use WrapUnique here since the constructor is private.
|
||||
return absl::WrapUnique(
|
||||
new CpSatSolver(std::move(cp_sat_model), std::move(variable_ids)));
|
||||
}
|
||||
|
||||
absl::StatusOr<SolveResultProto> CpSatSolver::Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const CallbackRegistrationProto& callback_registration, const Callback cb) {
|
||||
SolveResultProto result;
|
||||
MPModelRequest req;
|
||||
// Here we must make a copy since Solve() can be called multiple times with
|
||||
// different parameters. Hence we can't move `cp_sat_model`.
|
||||
*req.mutable_model() = cp_sat_model_;
|
||||
req.set_solver_type(MPModelRequest::SAT_INTEGER_PROGRAMMING);
|
||||
{
|
||||
std::vector<std::string> param_warnings =
|
||||
SetSolveParameters(parameters, req);
|
||||
if (!param_warnings.empty()) {
|
||||
if (parameters.common_parameters().strictness().bad_parameter()) {
|
||||
return absl::InvalidArgumentError(absl::StrJoin(param_warnings, "; "));
|
||||
} else {
|
||||
for (std::string& warning : param_warnings) {
|
||||
result.add_warnings(std::move(warning));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The `response` is not const to be able to move out the solution values.
|
||||
ASSIGN_OR_RETURN(const MPSolutionResponse response,
|
||||
SatSolveProto(std::move(req)));
|
||||
// Set default status and bounds.
|
||||
solve_stats.mutable_problem_status()->set_primal_status(
|
||||
FEASIBILITY_STATUS_UNDETERMINED);
|
||||
solve_stats.set_best_primal_bound(maximize ? -kInf : kInf);
|
||||
solve_stats.mutable_problem_status()->set_dual_status(
|
||||
FEASIBILITY_STATUS_UNDETERMINED);
|
||||
solve_stats.set_best_dual_bound(maximize ? kInf : -kInf);
|
||||
|
||||
// Set terminations and update status and bounds as appropriate.
|
||||
switch (response.status()) {
|
||||
case MPSOLVER_FEASIBLE:
|
||||
case MPSOLVER_OPTIMAL: {
|
||||
result.set_termination_reason(response.status() == MPSOLVER_OPTIMAL
|
||||
? SolveResultProto::OPTIMAL
|
||||
: SolveResultProto::OTHER_LIMIT);
|
||||
result.set_termination_detail(response.status_str());
|
||||
result.mutable_solve_stats()->set_best_primal_bound(
|
||||
response.objective_value());
|
||||
result.mutable_solve_stats()->set_best_dual_bound(
|
||||
response.best_objective_bound());
|
||||
*result.add_primal_solutions() =
|
||||
ExtractSolution(response, model_parameters);
|
||||
case MPSOLVER_OPTIMAL:
|
||||
termination =
|
||||
TerminateForReason(TERMINATION_REASON_OPTIMAL, response.status_str());
|
||||
solve_stats.mutable_problem_status()->set_primal_status(
|
||||
FEASIBILITY_STATUS_FEASIBLE);
|
||||
solve_stats.set_best_primal_bound(response.objective_value());
|
||||
solve_stats.mutable_problem_status()->set_dual_status(
|
||||
FEASIBILITY_STATUS_FEASIBLE);
|
||||
solve_stats.set_best_dual_bound(response.best_objective_bound());
|
||||
break;
|
||||
}
|
||||
case MPSOLVER_INFEASIBLE:
|
||||
result.set_termination_reason(SolveResultProto::INFEASIBLE);
|
||||
result.set_termination_detail(response.status_str());
|
||||
SetTrivialBounds(cp_sat_model_.maximize(), *result.mutable_solve_stats());
|
||||
termination = TerminateForReason(TERMINATION_REASON_INFEASIBLE,
|
||||
response.status_str());
|
||||
solve_stats.mutable_problem_status()->set_primal_status(
|
||||
FEASIBILITY_STATUS_INFEASIBLE);
|
||||
break;
|
||||
case MPSOLVER_UNKNOWN_STATUS:
|
||||
// For a basic unbounded problem, CP-SAT internally returns
|
||||
// INFEASIBLE_OR_UNBOUNDED after presolve but MPSolver statuses don't
|
||||
// support that thus it get transformed in MPSOLVER_UNKNOWN_STATUS with
|
||||
// a status_str of
|
||||
//
|
||||
// "Problem proven infeasible or unbounded during MIP presolve"
|
||||
//
|
||||
// There may be some other cases where CP-SAT returns UNKNOWN here so we
|
||||
// only return TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED when the
|
||||
// status_str is detected. Otherwise we return OTHER_ERROR.
|
||||
//
|
||||
// TODO(b/202159173): A better solution would be to use CP-SAT API
|
||||
// directly which may help further improve the statuses.
|
||||
if (absl::StrContains(response.status_str(), "infeasible or unbounded")) {
|
||||
termination = TerminateForReason(
|
||||
TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, response.status_str());
|
||||
solve_stats.mutable_problem_status()->set_primal_or_dual_infeasible(
|
||||
true);
|
||||
} else {
|
||||
termination = TerminateForReason(TERMINATION_REASON_OTHER_ERROR,
|
||||
response.status_str());
|
||||
}
|
||||
break;
|
||||
case MPSOLVER_FEASIBLE:
|
||||
termination = TerminateForLimit(
|
||||
is_interrupted ? LIMIT_INTERRUPTED : LIMIT_UNDETERMINED,
|
||||
response.status_str());
|
||||
solve_stats.mutable_problem_status()->set_primal_status(
|
||||
FEASIBILITY_STATUS_FEASIBLE);
|
||||
solve_stats.set_best_primal_bound(response.objective_value());
|
||||
solve_stats.set_best_dual_bound(response.best_objective_bound());
|
||||
if (std::isfinite(response.best_objective_bound())) {
|
||||
solve_stats.mutable_problem_status()->set_dual_status(
|
||||
FEASIBILITY_STATUS_FEASIBLE);
|
||||
}
|
||||
break;
|
||||
case MPSOLVER_NOT_SOLVED:
|
||||
result.set_termination_reason(SolveResultProto::OTHER_LIMIT);
|
||||
result.set_termination_detail(response.status_str());
|
||||
SetTrivialBounds(cp_sat_model_.maximize(), *result.mutable_solve_stats());
|
||||
termination = TerminateForLimit(
|
||||
is_interrupted ? LIMIT_INTERRUPTED : LIMIT_UNDETERMINED,
|
||||
response.status_str());
|
||||
break;
|
||||
case MPSOLVER_MODEL_INVALID:
|
||||
return absl::InternalError(
|
||||
@@ -227,6 +262,147 @@ absl::StatusOr<SolveResultProto> CpSatSolver::Solve(
|
||||
return absl::InternalError(
|
||||
absl::StrCat("unexpected solve status: ", response.status()));
|
||||
}
|
||||
return std::make_pair(std::move(solve_stats), std::move(termination));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
absl::StatusOr<std::unique_ptr<SolverInterface>> CpSatSolver::New(
|
||||
const ModelProto& model, const InitArgs& init_args) {
|
||||
ASSIGN_OR_RETURN(MPModelProto cp_sat_model,
|
||||
MathOptModelToMPModelProto(model));
|
||||
std::vector variable_ids(model.variables().ids().begin(),
|
||||
model.variables().ids().end());
|
||||
// TODO(b/204083726): Remove this check if QP support is added
|
||||
if (!model.objective().quadratic_coefficients().row_ids().empty()) {
|
||||
return absl::InvalidArgumentError(
|
||||
"MathOpt does not currently support CP-SAT models with quadratic "
|
||||
"objectives");
|
||||
}
|
||||
// We must use WrapUnique here since the constructor is private.
|
||||
return absl::WrapUnique(
|
||||
new CpSatSolver(std::move(cp_sat_model), std::move(variable_ids)));
|
||||
}
|
||||
|
||||
absl::StatusOr<SolveResultProto> CpSatSolver::Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const MessageCallback message_cb,
|
||||
const CallbackRegistrationProto& callback_registration, const Callback cb,
|
||||
SolveInterrupter* const interrupter) {
|
||||
const absl::Time start = absl::Now();
|
||||
|
||||
RETURN_IF_ERROR(CheckRegisteredCallbackEvents(
|
||||
callback_registration,
|
||||
/*supported_events=*/{CALLBACK_EVENT_MIP_SOLUTION}));
|
||||
if (callback_registration.add_lazy_constraints()) {
|
||||
return absl::InvalidArgumentError(
|
||||
"CallbackRegistrationProto.add_lazy_constraints=true is not supported "
|
||||
"for CP-SAT.");
|
||||
}
|
||||
// We need not check callback_registration.add_cuts, as cuts can only be added
|
||||
// at event MIP_NODE which we have already validated is not supported.
|
||||
|
||||
SolveResultProto result;
|
||||
MPModelRequest req;
|
||||
// Here we must make a copy since Solve() can be called multiple times with
|
||||
// different parameters. Hence we can't move `cp_sat_model`.
|
||||
*req.mutable_model() = cp_sat_model_;
|
||||
req.set_solver_type(MPModelRequest::SAT_INTEGER_PROGRAMMING);
|
||||
{
|
||||
std::vector<std::string> param_warnings =
|
||||
SetSolveParameters(parameters,
|
||||
/*has_message_callback=*/message_cb != nullptr, req);
|
||||
if (!param_warnings.empty()) {
|
||||
if (parameters.strictness().bad_parameter()) {
|
||||
return absl::InvalidArgumentError(absl::StrJoin(param_warnings, "; "));
|
||||
} else {
|
||||
for (std::string& warning : param_warnings) {
|
||||
result.add_warnings(std::move(warning));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!model_parameters.solution_hints().empty()) {
|
||||
int i = 0;
|
||||
for (const auto [id, val] :
|
||||
MakeView(model_parameters.solution_hints(0).variable_values())) {
|
||||
while (variable_ids_[i] < id) {
|
||||
++i;
|
||||
}
|
||||
req.mutable_model()->mutable_solution_hint()->add_var_index(i);
|
||||
req.mutable_model()->mutable_solution_hint()->add_var_value(val);
|
||||
}
|
||||
}
|
||||
|
||||
// We need to chain the user interrupter through a local interrupter, because
|
||||
// if we termiante early from a callback request, we don't want to incorrectly
|
||||
// modify the input state.
|
||||
SolveInterrupter local_interrupter;
|
||||
std::atomic<bool> interrupt_solve = false;
|
||||
local_interrupter.AddInterruptionCallback([&]() { interrupt_solve = true; });
|
||||
|
||||
// Setup a callback on the user provided so that we interrupt the solver.
|
||||
const ScopedSolveInterrupterCallback scoped_interrupt_cb(
|
||||
interrupter, [&]() { local_interrupter.Interrupt(); });
|
||||
|
||||
std::function<void(const std::string&)> logging_callback;
|
||||
if (message_cb != nullptr) {
|
||||
logging_callback = [&](const std::string& message) {
|
||||
message_cb(absl::StrSplit(message, '\n'));
|
||||
};
|
||||
}
|
||||
|
||||
const absl::flat_hash_set<CallbackEventProto> events =
|
||||
EventSet(callback_registration);
|
||||
std::function<void(const MPSolution&)> solution_callback;
|
||||
absl::Status callback_error = absl::OkStatus();
|
||||
if (events.contains(CALLBACK_EVENT_MIP_SOLUTION)) {
|
||||
solution_callback = [this, &cb, &callback_error, &local_interrupter,
|
||||
&model_parameters](const MPSolution& mp_solution) {
|
||||
if (!callback_error.ok()) {
|
||||
// A previous callback failed.
|
||||
return;
|
||||
}
|
||||
CallbackDataProto cb_data;
|
||||
cb_data.set_event(CALLBACK_EVENT_MIP_SOLUTION);
|
||||
*cb_data.mutable_primal_solution_vector() =
|
||||
ExtractSolution(mp_solution.variable_value(), model_parameters);
|
||||
const absl::StatusOr<CallbackResultProto> cb_result = cb(cb_data);
|
||||
if (!cb_result.ok()) {
|
||||
callback_error = cb_result.status();
|
||||
// Note: we will be returning a status error, we do not need to worry
|
||||
// about interpreting this as TERMINATION_REASON_INTERRUPTED.
|
||||
local_interrupter.Interrupt();
|
||||
} else if (cb_result->terminate()) {
|
||||
local_interrupter.Interrupt();
|
||||
}
|
||||
// Note cb_result.cuts and cb_result.suggested solutions are not
|
||||
// supported by CP-SAT and we have validated they are empty.
|
||||
};
|
||||
}
|
||||
|
||||
ASSIGN_OR_RETURN(const MPSolutionResponse response,
|
||||
SatSolveProto(std::move(req), &interrupt_solve,
|
||||
logging_callback, solution_callback));
|
||||
RETURN_IF_ERROR(callback_error) << "error in callback";
|
||||
ASSIGN_OR_RETURN((auto [solve_stats, termination]),
|
||||
GetTerminationAndStats(local_interrupter.IsInterrupted(),
|
||||
cp_sat_model_.maximize(), response));
|
||||
*result.mutable_solve_stats() = std::move(solve_stats);
|
||||
*result.mutable_termination() = std::move(termination);
|
||||
if (response.status() == MPSOLVER_OPTIMAL ||
|
||||
response.status() == MPSOLVER_FEASIBLE) {
|
||||
PrimalSolutionProto& solution =
|
||||
*result.add_solutions()->mutable_primal_solution();
|
||||
*solution.mutable_variable_values() =
|
||||
ExtractSolution(response.variable_value(), model_parameters);
|
||||
solution.set_objective_value(response.objective_value());
|
||||
solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
|
||||
}
|
||||
|
||||
CHECK_OK(util_time::EncodeGoogleApiProto(
|
||||
absl::Now() - start, result.mutable_solve_stats()->mutable_solve_time()));
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -246,30 +422,25 @@ CpSatSolver::CpSatSolver(MPModelProto cp_sat_model,
|
||||
: cp_sat_model_(std::move(cp_sat_model)),
|
||||
variable_ids_(std::move(variable_ids)) {}
|
||||
|
||||
PrimalSolutionProto CpSatSolver::ExtractSolution(
|
||||
const MPSolutionResponse& response,
|
||||
SparseDoubleVectorProto CpSatSolver::ExtractSolution(
|
||||
const absl::Span<const double> cp_sat_variable_values,
|
||||
const ModelSolveParametersProto& model_parameters) const {
|
||||
PrimalSolutionProto solution;
|
||||
|
||||
solution.set_objective_value(response.objective_value());
|
||||
|
||||
// Pre-condition: we assume one-to-one correspondence of input variables to
|
||||
// solution's variables.
|
||||
CHECK_EQ(response.variable_value_size(), variable_ids_.size());
|
||||
CHECK_EQ(cp_sat_variable_values.size(), variable_ids_.size());
|
||||
|
||||
SparseVectorFilterPredicate predicate(
|
||||
model_parameters.primal_variables_filter());
|
||||
auto* const values = solution.mutable_variable_values();
|
||||
model_parameters.variable_values_filter());
|
||||
SparseDoubleVectorProto result;
|
||||
for (int i = 0; i < variable_ids_.size(); ++i) {
|
||||
const int64_t id = variable_ids_[i];
|
||||
const double value = response.variable_value(i);
|
||||
const double value = cp_sat_variable_values[i];
|
||||
if (predicate.AcceptsAndUpdate(id, value)) {
|
||||
values->add_ids(id);
|
||||
values->add_values(value);
|
||||
result.add_ids(id);
|
||||
result.add_values(value);
|
||||
}
|
||||
}
|
||||
|
||||
return solution;
|
||||
return result;
|
||||
}
|
||||
|
||||
MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_CP_SAT, CpSatSolver::New);
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/linear_solver/linear_solver.pb.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_parameters.pb.h"
|
||||
@@ -38,13 +40,14 @@ namespace math_opt {
|
||||
class CpSatSolver : public SolverInterface {
|
||||
public:
|
||||
static absl::StatusOr<std::unique_ptr<SolverInterface>> New(
|
||||
const ModelProto& model, const SolverInitializerProto& initializer);
|
||||
const ModelProto& model, const InitArgs& init_args);
|
||||
|
||||
absl::StatusOr<SolveResultProto> Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const CallbackRegistrationProto& callback_registration,
|
||||
Callback cb) override;
|
||||
MessageCallback message_cb,
|
||||
const CallbackRegistrationProto& callback_registration, Callback cb,
|
||||
SolveInterrupter* interrupter) override;
|
||||
absl::Status Update(const ModelUpdateProto& model_update) override;
|
||||
bool CanUpdate(const ModelUpdateProto& model_update) override;
|
||||
|
||||
@@ -52,11 +55,8 @@ class CpSatSolver : public SolverInterface {
|
||||
CpSatSolver(MPModelProto cp_sat_model, std::vector<int64_t> variable_ids);
|
||||
|
||||
// Extract the solution from CP-SAT's response.
|
||||
//
|
||||
// This function assumes it exists, i.e. that the input `response.status` is
|
||||
// feasible or optimal.
|
||||
PrimalSolutionProto ExtractSolution(
|
||||
const MPSolutionResponse& response,
|
||||
SparseDoubleVectorProto ExtractSolution(
|
||||
absl::Span<const double> cp_sat_variable_values,
|
||||
const ModelSolveParametersProto& model_parameters) const;
|
||||
|
||||
const MPModelProto cp_sat_model_;
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
#include "ortools/math_opt/solvers/glop_solver.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -22,12 +25,14 @@
|
||||
|
||||
#include "ortools/base/integral_types.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/cleanup.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
#include "absl/strings/str_split.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
@@ -41,6 +46,7 @@
|
||||
#include "ortools/lp_data/lp_types.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/math_opt_proto_utils.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
@@ -50,8 +56,11 @@
|
||||
#include "ortools/math_opt/result.pb.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
#include "ortools/math_opt/validators/callback_validator.h"
|
||||
#include "ortools/port/proto_utils.h"
|
||||
#include "ortools/util/time_limit.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
#include "ortools/base/protoutil.h"
|
||||
|
||||
namespace operations_research {
|
||||
@@ -74,10 +83,40 @@ absl::string_view SafeName(const LinearConstraintsProto& linear_constraints,
|
||||
return linear_constraints.names(index);
|
||||
}
|
||||
|
||||
glop::LinearProgram::VariableType GlopVarTypeFromIsInteger(
|
||||
const bool is_integer) {
|
||||
return is_integer ? glop::LinearProgram::VariableType::INTEGER
|
||||
: glop::LinearProgram::VariableType::CONTINUOUS;
|
||||
absl::StatusOr<TerminationProto> BuildTermination(
|
||||
const glop::ProblemStatus status,
|
||||
const SolveInterrupter* const interrupter) {
|
||||
switch (status) {
|
||||
case glop::ProblemStatus::OPTIMAL:
|
||||
return TerminateForReason(TERMINATION_REASON_OPTIMAL);
|
||||
case glop::ProblemStatus::PRIMAL_INFEASIBLE:
|
||||
case glop::ProblemStatus::DUAL_UNBOUNDED:
|
||||
return TerminateForReason(TERMINATION_REASON_INFEASIBLE);
|
||||
case glop::ProblemStatus::PRIMAL_UNBOUNDED:
|
||||
return TerminateForReason(TERMINATION_REASON_UNBOUNDED);
|
||||
case glop::ProblemStatus::DUAL_INFEASIBLE:
|
||||
case glop::ProblemStatus::INFEASIBLE_OR_UNBOUNDED:
|
||||
return TerminateForReason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED);
|
||||
case glop::ProblemStatus::INIT:
|
||||
case glop::ProblemStatus::PRIMAL_FEASIBLE:
|
||||
case glop::ProblemStatus::DUAL_FEASIBLE:
|
||||
// Glop may flip the `interrupt_solve` atomic when it is terminated for a
|
||||
// reason other than interruption so we should ignore its value. Instead
|
||||
// we use the interrupter.
|
||||
return TerminateForLimit(interrupter != nullptr &&
|
||||
interrupter->IsInterrupted()
|
||||
? LIMIT_INTERRUPTED
|
||||
: LIMIT_UNDETERMINED);
|
||||
case glop::ProblemStatus::IMPRECISE:
|
||||
return TerminateForReason(TERMINATION_REASON_IMPRECISE);
|
||||
case glop::ProblemStatus::ABNORMAL:
|
||||
case glop::ProblemStatus::INVALID_PROBLEM:
|
||||
return absl::InternalError(
|
||||
absl::StrCat("Unexpected GLOP termination reason: ",
|
||||
glop::GetProblemStatusString(status)));
|
||||
}
|
||||
LOG(FATAL) << "Unimplemented GLOP termination reason: "
|
||||
<< glop::GetProblemStatusString(status);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -90,8 +129,6 @@ void GlopSolver::AddVariables(const VariablesProto& variables) {
|
||||
linear_program_.SetVariableBounds(col_index, variables.lower_bounds(i),
|
||||
variables.upper_bounds(i));
|
||||
linear_program_.SetVariableName(col_index, SafeName(variables, i));
|
||||
linear_program_.SetVariableType(
|
||||
col_index, GlopVarTypeFromIsInteger(variables.integers(i)));
|
||||
gtl::InsertOrDie(&variables_, variables.ids(i), col_index);
|
||||
}
|
||||
}
|
||||
@@ -218,31 +255,44 @@ void GlopSolver::UpdateLinearConstraintBounds(
|
||||
}
|
||||
|
||||
std::pair<glop::GlopParameters, std::vector<std::string>>
|
||||
GlopSolver::MergeCommonParameters(
|
||||
const CommonSolveParametersProto& common_solver_parameters,
|
||||
const glop::GlopParameters& glop_parameters) {
|
||||
glop::GlopParameters result = glop_parameters;
|
||||
GlopSolver::MergeSolveParameters(const SolveParametersProto& solver_parameters,
|
||||
const bool setting_initial_basis,
|
||||
const bool has_message_callback) {
|
||||
glop::GlopParameters result = solver_parameters.glop();
|
||||
std::vector<std::string> warnings;
|
||||
if (!result.has_max_time_in_seconds() &&
|
||||
common_solver_parameters.has_time_limit()) {
|
||||
if (!result.has_max_time_in_seconds() && solver_parameters.has_time_limit()) {
|
||||
const absl::Duration time_limit =
|
||||
util_time::DecodeGoogleApiProto(common_solver_parameters.time_limit())
|
||||
.value();
|
||||
util_time::DecodeGoogleApiProto(solver_parameters.time_limit()).value();
|
||||
result.set_max_time_in_seconds(absl::ToDoubleSeconds(time_limit));
|
||||
}
|
||||
if (!result.has_log_search_progress()) {
|
||||
result.set_log_search_progress(common_solver_parameters.enable_output());
|
||||
if (has_message_callback) {
|
||||
// If we have a message callback, we must set log_search_progress to get any
|
||||
// logs. We ignore the user's input on specific solver parameters here since
|
||||
// it would be confusing to accept a callback but never call it.
|
||||
result.set_log_search_progress(true);
|
||||
|
||||
// We don't want the logs to be also printed to stdout when we have a
|
||||
// message callback. Here we ignore the user input since message callback
|
||||
// can be used in the context of a server and printing to stdout could be a
|
||||
// problem.
|
||||
result.set_log_to_stdout(false);
|
||||
} else if (!result.has_log_search_progress()) {
|
||||
result.set_log_search_progress(solver_parameters.enable_output());
|
||||
}
|
||||
if (!result.has_num_omp_threads() && common_solver_parameters.has_threads()) {
|
||||
result.set_num_omp_threads(common_solver_parameters.threads());
|
||||
if (!result.has_num_omp_threads() && solver_parameters.has_threads()) {
|
||||
result.set_num_omp_threads(solver_parameters.threads());
|
||||
}
|
||||
if (!result.has_random_seed() && common_solver_parameters.has_random_seed()) {
|
||||
const int random_seed = std::max(0, common_solver_parameters.random_seed());
|
||||
if (!result.has_random_seed() && solver_parameters.has_random_seed()) {
|
||||
const int random_seed = std::max(0, solver_parameters.random_seed());
|
||||
result.set_random_seed(random_seed);
|
||||
}
|
||||
if (!result.has_max_number_of_iterations() &&
|
||||
solver_parameters.iteration_limit()) {
|
||||
result.set_max_number_of_iterations(solver_parameters.iteration_limit());
|
||||
}
|
||||
if (!result.has_use_dual_simplex() &&
|
||||
common_solver_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
|
||||
switch (common_solver_parameters.lp_algorithm()) {
|
||||
solver_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
|
||||
switch (solver_parameters.lp_algorithm()) {
|
||||
case LP_ALGORITHM_PRIMAL_SIMPLEX:
|
||||
result.set_use_dual_simplex(false);
|
||||
break;
|
||||
@@ -256,13 +306,13 @@ GlopSolver::MergeCommonParameters(
|
||||
break;
|
||||
default:
|
||||
LOG(FATAL) << "LPAlgorithm: "
|
||||
<< ProtoEnumToString(common_solver_parameters.lp_algorithm())
|
||||
<< ProtoEnumToString(solver_parameters.lp_algorithm())
|
||||
<< " unknown, error setting GLOP parameters";
|
||||
}
|
||||
}
|
||||
if (!result.has_use_scaling() && !result.has_scaling_method() &&
|
||||
common_solver_parameters.scaling() != EMPHASIS_UNSPECIFIED) {
|
||||
switch (common_solver_parameters.scaling()) {
|
||||
solver_parameters.scaling() != EMPHASIS_UNSPECIFIED) {
|
||||
switch (solver_parameters.scaling()) {
|
||||
case EMPHASIS_OFF:
|
||||
result.set_use_scaling(false);
|
||||
break;
|
||||
@@ -278,13 +328,15 @@ GlopSolver::MergeCommonParameters(
|
||||
break;
|
||||
default:
|
||||
LOG(FATAL) << "Scaling emphasis: "
|
||||
<< ProtoEnumToString(common_solver_parameters.scaling())
|
||||
<< ProtoEnumToString(solver_parameters.scaling())
|
||||
<< " unknown, error setting GLOP parameters";
|
||||
}
|
||||
}
|
||||
if (!result.has_use_preprocessing() &&
|
||||
common_solver_parameters.presolve() != EMPHASIS_UNSPECIFIED) {
|
||||
switch (common_solver_parameters.presolve()) {
|
||||
if (setting_initial_basis) {
|
||||
result.set_use_preprocessing(false);
|
||||
} else if (!result.has_use_preprocessing() &&
|
||||
solver_parameters.presolve() != EMPHASIS_UNSPECIFIED) {
|
||||
switch (solver_parameters.presolve()) {
|
||||
case EMPHASIS_OFF:
|
||||
result.set_use_preprocessing(false);
|
||||
break;
|
||||
@@ -296,26 +348,29 @@ GlopSolver::MergeCommonParameters(
|
||||
break;
|
||||
default:
|
||||
LOG(FATAL) << "Presolve emphasis: "
|
||||
<< ProtoEnumToString(common_solver_parameters.presolve())
|
||||
<< ProtoEnumToString(solver_parameters.presolve())
|
||||
<< " unknown, error setting GLOP parameters";
|
||||
}
|
||||
}
|
||||
if (common_solver_parameters.cuts() != EMPHASIS_UNSPECIFIED) {
|
||||
if (solver_parameters.cuts() != EMPHASIS_UNSPECIFIED) {
|
||||
warnings.push_back(absl::StrCat(
|
||||
"GLOP does not support 'cuts' parameters, but cuts was set to: ",
|
||||
ProtoEnumToString(common_solver_parameters.cuts())));
|
||||
ProtoEnumToString(solver_parameters.cuts())));
|
||||
}
|
||||
if (common_solver_parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
|
||||
if (solver_parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
|
||||
warnings.push_back(
|
||||
absl::StrCat("GLOP does not support 'heuristics' parameter, but "
|
||||
"heuristics was set to: ",
|
||||
ProtoEnumToString(common_solver_parameters.heuristics())));
|
||||
ProtoEnumToString(solver_parameters.heuristics())));
|
||||
}
|
||||
return std::make_pair(std::move(result), std::move(warnings));
|
||||
}
|
||||
|
||||
bool GlopSolver::CanUpdate(const ModelUpdateProto& model_update) {
|
||||
return true;
|
||||
return model_update.objective_updates()
|
||||
.quadratic_coefficients()
|
||||
.row_ids()
|
||||
.empty();
|
||||
}
|
||||
|
||||
template <typename IndexType>
|
||||
@@ -336,6 +391,58 @@ SparseDoubleVectorProto FillSparseDoubleVector(
|
||||
return result;
|
||||
}
|
||||
|
||||
// ValueType should be glop's VariableStatus or ConstraintStatus.
|
||||
template <typename ValueType>
|
||||
BasisStatusProto FromGlopBasisStatus(const ValueType glop_basis_status) {
|
||||
switch (glop_basis_status) {
|
||||
case ValueType::BASIC:
|
||||
return BasisStatusProto::BASIS_STATUS_BASIC;
|
||||
case ValueType::FIXED_VALUE:
|
||||
return BasisStatusProto::BASIS_STATUS_FIXED_VALUE;
|
||||
case ValueType::AT_LOWER_BOUND:
|
||||
return BasisStatusProto::BASIS_STATUS_AT_LOWER_BOUND;
|
||||
case ValueType::AT_UPPER_BOUND:
|
||||
return BasisStatusProto::BASIS_STATUS_AT_UPPER_BOUND;
|
||||
case ValueType::FREE:
|
||||
return BasisStatusProto::BASIS_STATUS_FREE;
|
||||
}
|
||||
return BasisStatusProto::BASIS_STATUS_UNSPECIFIED;
|
||||
}
|
||||
|
||||
template <typename IndexType, typename ValueType>
|
||||
SparseBasisStatusVector FillSparseBasisStatusVector(
|
||||
const std::vector<int64_t>& ids_in_order,
|
||||
const absl::flat_hash_map<int64_t, IndexType>& id_map,
|
||||
const glop::StrictITIVector<IndexType, ValueType>& values) {
|
||||
SparseBasisStatusVector result;
|
||||
for (const int64_t variable_id : ids_in_order) {
|
||||
const ValueType value = values[id_map.at(variable_id)];
|
||||
result.add_ids(variable_id);
|
||||
result.add_values(FromGlopBasisStatus(value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ValueType should be glop's VariableStatus or ConstraintStatus.
|
||||
template <typename ValueType>
|
||||
ValueType ToGlopBasisStatus(const BasisStatusProto basis_status) {
|
||||
switch (basis_status) {
|
||||
case BASIS_STATUS_BASIC:
|
||||
return ValueType::BASIC;
|
||||
case BASIS_STATUS_FIXED_VALUE:
|
||||
return ValueType::FIXED_VALUE;
|
||||
case BASIS_STATUS_AT_LOWER_BOUND:
|
||||
return ValueType::AT_LOWER_BOUND;
|
||||
case BASIS_STATUS_AT_UPPER_BOUND:
|
||||
return ValueType::AT_UPPER_BOUND;
|
||||
case BASIS_STATUS_FREE:
|
||||
return ValueType::FREE;
|
||||
default:
|
||||
LOG(FATAL) << "Unexpected invalid initial_basis.";
|
||||
return ValueType::FREE;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::vector<int64_t> GetSortedIs(
|
||||
const absl::flat_hash_map<int64_t, T>& id_map) {
|
||||
@@ -348,74 +455,250 @@ std::vector<int64_t> GetSortedIs(
|
||||
return sorted;
|
||||
}
|
||||
|
||||
void GlopSolver::FillSolveResult(
|
||||
const glop::ProblemStatus status,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
SolveResultProto& solve_result) {
|
||||
solve_result.mutable_solve_stats()->set_simplex_iterations(
|
||||
lp_solver_.GetNumberOfSimplexIterations());
|
||||
// TODO(b/168374742): this needs to be properly filled in. In particular, we
|
||||
// can give better primal and dual bounds when the status is not OPTIMAL.
|
||||
void GlopSolver::FillSolution(const glop::ProblemStatus status,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
SolveResultProto& solve_result) {
|
||||
// Meaningfull solutions are available if optimality is proven in
|
||||
// preprocessing or after 1 simplex iteration.
|
||||
// TODO(b/195295177): Discuss what to do with glop::ProblemStatus::IMPRECISE
|
||||
// looks like it may be set also when rays are imprecise.
|
||||
const bool phase_I_solution_available =
|
||||
(status == glop::ProblemStatus::INIT) &&
|
||||
(lp_solver_.GetNumberOfSimplexIterations() > 0);
|
||||
if (status != glop::ProblemStatus::OPTIMAL &&
|
||||
status != glop::ProblemStatus::PRIMAL_FEASIBLE &&
|
||||
status != glop::ProblemStatus::DUAL_FEASIBLE &&
|
||||
status != glop::ProblemStatus::PRIMAL_UNBOUNDED &&
|
||||
status != glop::ProblemStatus::DUAL_UNBOUNDED &&
|
||||
!phase_I_solution_available) {
|
||||
return;
|
||||
}
|
||||
auto sorted_variables = GetSortedIs(variables_);
|
||||
auto sorted_constraints = GetSortedIs(linear_constraints_);
|
||||
SolutionProto* const solution = solve_result.add_solutions();
|
||||
BasisProto* const basis = solution->mutable_basis();
|
||||
PrimalSolutionProto* const primal_solution =
|
||||
solution->mutable_primal_solution();
|
||||
DualSolutionProto* const dual_solution = solution->mutable_dual_solution();
|
||||
|
||||
// Fill in feasibility statuses
|
||||
// Note: if we reach here and status != OPTIMAL, then at least 1 simplex
|
||||
// iteration has been executed.
|
||||
if (status == glop::ProblemStatus::OPTIMAL) {
|
||||
primal_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
|
||||
basis->set_basic_dual_feasibility(SOLUTION_STATUS_FEASIBLE);
|
||||
dual_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
|
||||
} else if (status == glop::ProblemStatus::PRIMAL_FEASIBLE) {
|
||||
// Solve reached phase II of primal simplex and current basis is not
|
||||
// optimal. Hence basis is primal feasible, but cannot be dual feasible.
|
||||
// Dual solution could still be feasible as noted in
|
||||
// go/mathopt-basis-advanced#dualfeasibility
|
||||
primal_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
|
||||
dual_solution->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED);
|
||||
basis->set_basic_dual_feasibility(SOLUTION_STATUS_INFEASIBLE);
|
||||
} else if (status == glop::ProblemStatus::DUAL_FEASIBLE) {
|
||||
// Solve reached phase II of dual simplex and current basis is not optimal.
|
||||
// Hence basis is dual feasible, but cannot be primal feasible. In addition,
|
||||
// glop applies dual feasibility correction in dual simplex so feasibility
|
||||
// of the dual solution matches dual feasibility of the basis (i.e the issue
|
||||
// described in go/mathopt-basis-advanced#dualfeasibility cannot happen).
|
||||
// TODO(b/195295177): confirm with fdid
|
||||
primal_solution->set_feasibility_status(SOLUTION_STATUS_INFEASIBLE);
|
||||
dual_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
|
||||
basis->set_basic_dual_feasibility(SOLUTION_STATUS_FEASIBLE);
|
||||
} else { // status == INIT
|
||||
// Phase I of primal or dual simplex ran for at least one iteration
|
||||
if (lp_solver_.GetParameters().use_dual_simplex()) {
|
||||
// Phase I did not finish so basis is not dual feasible. In addition,
|
||||
// glop applies dual feasibility correction so feasibility of the dual
|
||||
// solution matches dual feasibility of the basis (i.e the issue described
|
||||
// in go/mathopt-basis-advanced#dualfeasibility cannot happen).
|
||||
// TODO(b/195295177): confirm with fdid
|
||||
primal_solution->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED);
|
||||
dual_solution->set_feasibility_status(SOLUTION_STATUS_INFEASIBLE);
|
||||
basis->set_basic_dual_feasibility(SOLUTION_STATUS_INFEASIBLE);
|
||||
} else {
|
||||
// Phase I did not finish so basis is not primal feasible.
|
||||
primal_solution->set_feasibility_status(SOLUTION_STATUS_INFEASIBLE);
|
||||
dual_solution->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED);
|
||||
basis->set_basic_dual_feasibility(SOLUTION_STATUS_UNDETERMINED);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in objective values
|
||||
primal_solution->set_objective_value(lp_solver_.GetObjectiveValue());
|
||||
if (basis->basic_dual_feasibility() == SOLUTION_STATUS_FEASIBLE) {
|
||||
// Primal and dual objectives are the same for a dual feasible basis
|
||||
// see go/mathopt-basis-advanced#cs-obj-dual-feasible-dual-feasible-basis
|
||||
dual_solution->set_objective_value(primal_solution->objective_value());
|
||||
}
|
||||
|
||||
// Fill solution and basis
|
||||
*basis->mutable_constraint_status() = *basis->mutable_variable_status() =
|
||||
FillSparseBasisStatusVector(sorted_variables, variables_,
|
||||
lp_solver_.variable_statuses());
|
||||
*basis->mutable_constraint_status() =
|
||||
FillSparseBasisStatusVector(sorted_constraints, linear_constraints_,
|
||||
lp_solver_.constraint_statuses());
|
||||
|
||||
*primal_solution->mutable_variable_values() = FillSparseDoubleVector(
|
||||
sorted_variables, variables_, lp_solver_.variable_values(),
|
||||
model_parameters.variable_values_filter());
|
||||
|
||||
*dual_solution->mutable_dual_values() = FillSparseDoubleVector(
|
||||
sorted_constraints, linear_constraints_, lp_solver_.dual_values(),
|
||||
model_parameters.dual_values_filter());
|
||||
*dual_solution->mutable_reduced_costs() = FillSparseDoubleVector(
|
||||
sorted_variables, variables_, lp_solver_.reduced_costs(),
|
||||
model_parameters.reduced_costs_filter());
|
||||
|
||||
if (!lp_solver_.primal_ray().empty()) {
|
||||
PrimalRayProto* const primal_ray = solve_result.add_primal_rays();
|
||||
|
||||
*primal_ray->mutable_variable_values() = FillSparseDoubleVector(
|
||||
sorted_variables, variables_, lp_solver_.primal_ray(),
|
||||
model_parameters.variable_values_filter());
|
||||
}
|
||||
if (!lp_solver_.constraints_dual_ray().empty() &&
|
||||
!lp_solver_.variable_bounds_dual_ray().empty()) {
|
||||
DualRayProto* const dual_ray = solve_result.add_dual_rays();
|
||||
*dual_ray->mutable_dual_values() =
|
||||
FillSparseDoubleVector(sorted_constraints, linear_constraints_,
|
||||
lp_solver_.constraints_dual_ray(),
|
||||
model_parameters.dual_values_filter());
|
||||
*dual_ray->mutable_reduced_costs() = FillSparseDoubleVector(
|
||||
sorted_variables, variables_, lp_solver_.variable_bounds_dual_ray(),
|
||||
model_parameters.reduced_costs_filter());
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status GlopSolver::FillSolveStats(const glop::ProblemStatus status,
|
||||
const absl::Duration solve_time,
|
||||
SolveStatsProto& solve_stats) {
|
||||
const bool is_maximize = linear_program_.IsMaximizationProblem();
|
||||
constexpr double kInf = std::numeric_limits<double>::infinity();
|
||||
solve_result.mutable_solve_stats()->set_best_primal_bound(is_maximize ? -kInf
|
||||
: kInf);
|
||||
solve_result.mutable_solve_stats()->set_best_dual_bound(is_maximize ? kInf
|
||||
: -kInf);
|
||||
if (status == glop::ProblemStatus::OPTIMAL) {
|
||||
solve_result.set_termination_reason(SolveResultProto::OPTIMAL);
|
||||
solve_result.mutable_solve_stats()->set_best_primal_bound(
|
||||
lp_solver_.GetObjectiveValue());
|
||||
solve_result.mutable_solve_stats()->set_best_dual_bound(
|
||||
lp_solver_.GetObjectiveValue());
|
||||
|
||||
auto sorted_variables = GetSortedIs(variables_);
|
||||
auto sorted_constraints = GetSortedIs(linear_constraints_);
|
||||
// Set default status and bounds.
|
||||
solve_stats.mutable_problem_status()->set_primal_status(
|
||||
FEASIBILITY_STATUS_UNDETERMINED);
|
||||
solve_stats.set_best_primal_bound(is_maximize ? -kInf : kInf);
|
||||
solve_stats.mutable_problem_status()->set_dual_status(
|
||||
FEASIBILITY_STATUS_UNDETERMINED);
|
||||
solve_stats.set_best_dual_bound(is_maximize ? kInf : -kInf);
|
||||
|
||||
PrimalSolutionProto* const primal_solution =
|
||||
solve_result.add_primal_solutions();
|
||||
primal_solution->set_objective_value(lp_solver_.GetObjectiveValue());
|
||||
*primal_solution->mutable_variable_values() = FillSparseDoubleVector(
|
||||
sorted_variables, variables_, lp_solver_.variable_values(),
|
||||
model_parameters.primal_variables_filter());
|
||||
|
||||
DualSolutionProto* const dual_solution = solve_result.add_dual_solutions();
|
||||
dual_solution->set_objective_value(lp_solver_.GetObjectiveValue());
|
||||
*dual_solution->mutable_dual_values() = FillSparseDoubleVector(
|
||||
sorted_constraints, linear_constraints_, lp_solver_.dual_values(),
|
||||
model_parameters.dual_linear_constraints_filter());
|
||||
*dual_solution->mutable_reduced_costs() = FillSparseDoubleVector(
|
||||
sorted_variables, variables_, lp_solver_.reduced_costs(),
|
||||
model_parameters.dual_variables_filter());
|
||||
// TODO(user): consider pulling these out to a separate method once we
|
||||
// support all statuses
|
||||
} else if (status == glop::ProblemStatus::PRIMAL_INFEASIBLE ||
|
||||
status == glop::ProblemStatus::DUAL_UNBOUNDED) {
|
||||
solve_result.set_termination_reason(SolveResultProto::INFEASIBLE);
|
||||
} else if (status == glop::ProblemStatus::PRIMAL_UNBOUNDED) {
|
||||
solve_result.set_termination_reason(SolveResultProto::UNBOUNDED);
|
||||
} else if (status == glop::ProblemStatus::DUAL_INFEASIBLE ||
|
||||
status == glop::ProblemStatus::INFEASIBLE_OR_UNBOUNDED) {
|
||||
solve_result.set_termination_reason(SolveResultProto::DUAL_INFEASIBLE);
|
||||
} else {
|
||||
LOG(DFATAL) << "Termination not implemented.";
|
||||
solve_result.set_termination_reason(
|
||||
SolveResultProto::TERMINATION_REASON_UNSPECIFIED);
|
||||
solve_result.set_termination_detail(absl::StrCat("Glop status: ", status));
|
||||
// Update status and bounds as appropriate.
|
||||
switch (status) {
|
||||
case glop::ProblemStatus::OPTIMAL:
|
||||
solve_stats.mutable_problem_status()->set_primal_status(
|
||||
FEASIBILITY_STATUS_FEASIBLE);
|
||||
solve_stats.mutable_problem_status()->set_dual_status(
|
||||
FEASIBILITY_STATUS_FEASIBLE);
|
||||
solve_stats.set_best_primal_bound(lp_solver_.GetObjectiveValue());
|
||||
solve_stats.set_best_dual_bound(lp_solver_.GetObjectiveValue());
|
||||
break;
|
||||
case glop::ProblemStatus::PRIMAL_INFEASIBLE:
|
||||
solve_stats.mutable_problem_status()->set_primal_status(
|
||||
FEASIBILITY_STATUS_INFEASIBLE);
|
||||
break;
|
||||
case glop::ProblemStatus::DUAL_UNBOUNDED:
|
||||
solve_stats.mutable_problem_status()->set_primal_status(
|
||||
FEASIBILITY_STATUS_INFEASIBLE);
|
||||
solve_stats.mutable_problem_status()->set_dual_status(
|
||||
FEASIBILITY_STATUS_FEASIBLE);
|
||||
solve_stats.set_best_dual_bound(is_maximize ? -kInf : kInf);
|
||||
break;
|
||||
case glop::ProblemStatus::PRIMAL_UNBOUNDED:
|
||||
solve_stats.mutable_problem_status()->set_primal_status(
|
||||
FEASIBILITY_STATUS_FEASIBLE);
|
||||
solve_stats.mutable_problem_status()->set_dual_status(
|
||||
FEASIBILITY_STATUS_INFEASIBLE);
|
||||
solve_stats.set_best_primal_bound(is_maximize ? kInf : -kInf);
|
||||
break;
|
||||
case glop::ProblemStatus::DUAL_INFEASIBLE:
|
||||
solve_stats.mutable_problem_status()->set_dual_status(
|
||||
FEASIBILITY_STATUS_INFEASIBLE);
|
||||
break;
|
||||
case glop::ProblemStatus::INFEASIBLE_OR_UNBOUNDED:
|
||||
solve_stats.mutable_problem_status()->set_primal_or_dual_infeasible(true);
|
||||
break;
|
||||
case glop::ProblemStatus::PRIMAL_FEASIBLE:
|
||||
solve_stats.mutable_problem_status()->set_primal_status(
|
||||
FEASIBILITY_STATUS_FEASIBLE);
|
||||
solve_stats.set_best_primal_bound(lp_solver_.GetObjectiveValue());
|
||||
break;
|
||||
case glop::ProblemStatus::DUAL_FEASIBLE:
|
||||
solve_stats.mutable_problem_status()->set_dual_status(
|
||||
FEASIBILITY_STATUS_FEASIBLE);
|
||||
solve_stats.set_best_dual_bound(lp_solver_.GetObjectiveValue());
|
||||
break;
|
||||
case glop::ProblemStatus::INIT:
|
||||
case glop::ProblemStatus::IMPRECISE:
|
||||
// TODO(b/195295177): Discuss what to do with
|
||||
// glop::ProblemStatus::IMPRECISE
|
||||
break;
|
||||
case glop::ProblemStatus::ABNORMAL:
|
||||
case glop::ProblemStatus::INVALID_PROBLEM:
|
||||
return absl::InternalError(
|
||||
absl::StrCat("Unexpected GLOP termination reason: ",
|
||||
glop::GetProblemStatusString(status)));
|
||||
}
|
||||
|
||||
// Fill remaining stats
|
||||
solve_stats.set_simplex_iterations(lp_solver_.GetNumberOfSimplexIterations());
|
||||
RETURN_IF_ERROR(util_time::EncodeGoogleApiProto(
|
||||
solve_time, solve_stats.mutable_solve_time()));
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status GlopSolver::FillSolveResult(
|
||||
const glop::ProblemStatus status,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const SolveInterrupter* const interrupter, const absl::Duration solve_time,
|
||||
SolveResultProto& solve_result) {
|
||||
ASSIGN_OR_RETURN(*solve_result.mutable_termination(),
|
||||
BuildTermination(status, interrupter));
|
||||
FillSolution(status, model_parameters, solve_result);
|
||||
RETURN_IF_ERROR(
|
||||
FillSolveStats(status, solve_time, *solve_result.mutable_solve_stats()));
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
void GlopSolver::SetGlopBasis(const BasisProto& basis) {
|
||||
glop::VariableStatusRow variable_statuses(linear_program_.num_variables());
|
||||
for (const auto [id, value] : MakeView(basis.variable_status())) {
|
||||
variable_statuses[variables_.at(id)] =
|
||||
ToGlopBasisStatus<glop::VariableStatus>(
|
||||
static_cast<BasisStatusProto>(value));
|
||||
}
|
||||
glop::ConstraintStatusColumn constraint_statuses(
|
||||
linear_program_.num_constraints());
|
||||
for (const auto [id, value] : MakeView(basis.constraint_status())) {
|
||||
constraint_statuses[linear_constraints_.at(id)] =
|
||||
ToGlopBasisStatus<glop::ConstraintStatus>(
|
||||
static_cast<BasisStatusProto>(value));
|
||||
}
|
||||
lp_solver_.SetInitialBasis(variable_statuses, constraint_statuses);
|
||||
}
|
||||
|
||||
absl::StatusOr<SolveResultProto> GlopSolver::Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const CallbackRegistrationProto& callback_registration, const Callback cb) {
|
||||
const MessageCallback message_cb,
|
||||
const CallbackRegistrationProto& callback_registration, const Callback cb,
|
||||
SolveInterrupter* const interrupter) {
|
||||
RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration,
|
||||
/*supported_events=*/{}));
|
||||
|
||||
const absl::Time start = absl::Now();
|
||||
SolveResultProto result;
|
||||
{
|
||||
auto [glop_parameters, warnings] = MergeCommonParameters(
|
||||
parameters.common_parameters(), parameters.glop_parameters());
|
||||
auto [glop_parameters, warnings] = MergeSolveParameters(
|
||||
parameters,
|
||||
/*setting_initial_basis=*/model_parameters.has_initial_basis(),
|
||||
/*has_message_callback=*/message_cb != nullptr);
|
||||
if (!warnings.empty()) {
|
||||
if (parameters.common_parameters().strictness().bad_parameter()) {
|
||||
if (parameters.strictness().bad_parameter()) {
|
||||
return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
|
||||
} else {
|
||||
for (std::string& warning : warnings) {
|
||||
@@ -426,15 +709,58 @@ absl::StatusOr<SolveResultProto> GlopSolver::Solve(
|
||||
lp_solver_.SetParameters(glop_parameters);
|
||||
}
|
||||
|
||||
const glop::ProblemStatus status = lp_solver_.Solve(linear_program_);
|
||||
FillSolveResult(status, model_parameters, result);
|
||||
CHECK_OK(util_time::EncodeGoogleApiProto(
|
||||
absl::Now() - start, result.mutable_solve_stats()->mutable_solve_time()));
|
||||
if (model_parameters.has_initial_basis()) {
|
||||
SetGlopBasis(model_parameters.initial_basis());
|
||||
}
|
||||
|
||||
std::atomic<bool> interrupt_solve = false;
|
||||
const std::unique_ptr<TimeLimit> time_limit =
|
||||
TimeLimit::FromParameters(lp_solver_.GetParameters());
|
||||
time_limit->RegisterExternalBooleanAsLimit(&interrupt_solve);
|
||||
|
||||
const ScopedSolveInterrupterCallback scoped_interrupt_cb(interrupter, [&]() {
|
||||
CHECK_NE(interrupter, nullptr);
|
||||
interrupt_solve = true;
|
||||
});
|
||||
|
||||
if (message_cb != nullptr) {
|
||||
// Please note that the logging is enabled in MergeSolveParameters() where
|
||||
// we also disable logging to stdout. We can't modify the SolverLogger here
|
||||
// since the values are overwritten from the parameters at the beginning of
|
||||
// the solve.
|
||||
//
|
||||
// Here we test that there are no other callbacks since we will clear them
|
||||
// all in the cleanup below.
|
||||
CHECK_EQ(lp_solver_.GetSolverLogger().NumInfoLoggingCallbacks(), 0);
|
||||
lp_solver_.GetSolverLogger().AddInfoLoggingCallback(
|
||||
[&](const std::string& message) {
|
||||
message_cb(absl::StrSplit(message, '\n'));
|
||||
});
|
||||
}
|
||||
const auto message_cb_cleanup = absl::MakeCleanup([&]() {
|
||||
if (message_cb != nullptr) {
|
||||
// Check that no other callbacks have been added to the logger.
|
||||
CHECK_EQ(lp_solver_.GetSolverLogger().NumInfoLoggingCallbacks(), 1);
|
||||
lp_solver_.GetSolverLogger().ClearInfoLoggingCallbacks();
|
||||
}
|
||||
});
|
||||
|
||||
const glop::ProblemStatus status =
|
||||
lp_solver_.SolveWithTimeLimit(linear_program_, time_limit.get());
|
||||
const absl::Duration solve_time = absl::Now() - start;
|
||||
|
||||
RETURN_IF_ERROR(FillSolveResult(status, model_parameters, interrupter,
|
||||
solve_time, result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::unique_ptr<SolverInterface>> GlopSolver::New(
|
||||
const ModelProto& model, const SolverInitializerProto& initializer) {
|
||||
const ModelProto& model, const InitArgs& init_args) {
|
||||
if (!model.objective().quadratic_coefficients().row_ids().empty()) {
|
||||
return absl::InvalidArgumentError(
|
||||
"Glop does not support quadratic objectives");
|
||||
}
|
||||
auto solver = absl::WrapUnique(new GlopSolver);
|
||||
solver->linear_program_.SetName(model.name());
|
||||
solver->linear_program_.SetMaximizationProblem(model.objective().maximize());
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -30,12 +31,14 @@
|
||||
#include "ortools/lp_data/lp_data.h"
|
||||
#include "ortools/lp_data/lp_types.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_parameters.pb.h"
|
||||
#include "ortools/math_opt/model_update.pb.h"
|
||||
#include "ortools/math_opt/parameters.pb.h"
|
||||
#include "ortools/math_opt/result.pb.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
@@ -44,22 +47,22 @@ namespace math_opt {
|
||||
class GlopSolver : public SolverInterface {
|
||||
public:
|
||||
static absl::StatusOr<std::unique_ptr<SolverInterface>> New(
|
||||
const ModelProto& model, const SolverInitializerProto& initializer);
|
||||
const ModelProto& model, const InitArgs& init_args);
|
||||
|
||||
absl::StatusOr<SolveResultProto> Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const CallbackRegistrationProto& callback_registration,
|
||||
Callback cb) override;
|
||||
MessageCallback message_cb,
|
||||
const CallbackRegistrationProto& callback_registration, Callback cb,
|
||||
SolveInterrupter* interrupter) override;
|
||||
absl::Status Update(const ModelUpdateProto& model_update) override;
|
||||
bool CanUpdate(const ModelUpdateProto& model_update) override;
|
||||
|
||||
// Returns the merged parameters and a list of warnings from any parameter
|
||||
// settings that are invalid for this solver.
|
||||
static std::pair<glop::GlopParameters, std::vector<std::string>>
|
||||
MergeCommonParameters(
|
||||
const CommonSolveParametersProto& common_solver_parameters,
|
||||
const glop::GlopParameters& glop_parameters);
|
||||
MergeSolveParameters(const SolveParametersProto& solver_parameters,
|
||||
bool setting_initial_basis, bool has_message_callback);
|
||||
|
||||
private:
|
||||
GlopSolver();
|
||||
@@ -79,9 +82,20 @@ class GlopSolver : public SolverInterface {
|
||||
void UpdateLinearConstraintBounds(
|
||||
const LinearConstraintUpdatesProto& linear_constraint_updates);
|
||||
|
||||
void FillSolveResult(glop::ProblemStatus status,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
SolveResultProto& solve_result);
|
||||
void FillSolution(glop::ProblemStatus status,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
SolveResultProto& solve_result);
|
||||
absl::Status FillSolveResult(
|
||||
glop::ProblemStatus status,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const SolveInterrupter* interrupter, absl::Duration solve_time,
|
||||
SolveResultProto& solve_result);
|
||||
|
||||
absl::Status FillSolveStats(const glop::ProblemStatus status,
|
||||
absl::Duration solve_time,
|
||||
SolveStatsProto& solve_stats);
|
||||
|
||||
void SetGlopBasis(const BasisProto& basis);
|
||||
|
||||
glop::LinearProgram linear_program_;
|
||||
glop::LPSolver lp_solver_;
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/integral_types.h"
|
||||
#include "ortools/base/logging.h"
|
||||
#include "ortools/base/cleanup.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/memory/memory.h"
|
||||
@@ -32,16 +34,19 @@
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "absl/types/optional.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "scip/scip.h"
|
||||
#include "scip/type_cons.h"
|
||||
#include "scip/type_event.h"
|
||||
#include "scip/type_var.h"
|
||||
#include "ortools/base/map_util.h"
|
||||
#include "ortools/gscip/gscip.h"
|
||||
#include "ortools/gscip/gscip.pb.h"
|
||||
#include "ortools/gscip/gscip_event_handler.h"
|
||||
#include "ortools/gscip/gscip_parameters.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/math_opt_proto_utils.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
@@ -51,7 +56,9 @@
|
||||
#include "ortools/math_opt/result.pb.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
#include "ortools/math_opt/solvers/gscip_solver_callback.h"
|
||||
#include "ortools/math_opt/solvers/gscip_solver_message_callback_handler.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
#include "ortools/math_opt/validators/callback_validator.h"
|
||||
#include "ortools/port/proto_utils.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
@@ -261,7 +268,7 @@ class LazyInitialized {
|
||||
|
||||
private:
|
||||
const std::function<T()> initializer_;
|
||||
absl::optional<T> value_;
|
||||
std::optional<T> value_;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
@@ -372,7 +379,7 @@ absl::Status GScipSolver::UpdateLinearConstraints(
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
GScipParameters::MetaParamValue ConvertMathOptEmphasis(Emphasis emphasis) {
|
||||
GScipParameters::MetaParamValue ConvertMathOptEmphasis(EmphasisProto emphasis) {
|
||||
switch (emphasis) {
|
||||
case EMPHASIS_OFF:
|
||||
return GScipParameters::OFF;
|
||||
@@ -391,38 +398,53 @@ GScipParameters::MetaParamValue ConvertMathOptEmphasis(Emphasis emphasis) {
|
||||
}
|
||||
}
|
||||
|
||||
GScipParameters GScipSolver::MergeCommonParameters(
|
||||
const CommonSolveParametersProto& common_solver_parameters,
|
||||
const GScipParameters& gscip_parameters) {
|
||||
GScipParameters GScipSolver::MergeParameters(
|
||||
const SolveParametersProto& solve_parameters) {
|
||||
// First build the result by translating common parameters to a
|
||||
// GScipParameters, and then merging with user provided gscip_parameters.
|
||||
// This results in user provided solver specific parameters overwriting
|
||||
// common parameters should there be any conflict.
|
||||
GScipParameters result;
|
||||
if (common_solver_parameters.has_time_limit()) {
|
||||
|
||||
// By default SCIP catches Ctrl-C but we don't want this behavior when the
|
||||
// users uses SCIP through MathOpt.
|
||||
GScipSetCatchCtrlC(false, &result);
|
||||
|
||||
if (solve_parameters.has_time_limit()) {
|
||||
GScipSetTimeLimit(
|
||||
util_time::DecodeGoogleApiProto(common_solver_parameters.time_limit())
|
||||
.value(),
|
||||
util_time::DecodeGoogleApiProto(solve_parameters.time_limit()).value(),
|
||||
&result);
|
||||
}
|
||||
if (common_solver_parameters.has_threads()) {
|
||||
GScipSetMaxNumThreads(common_solver_parameters.threads(), &result);
|
||||
|
||||
if (solve_parameters.has_threads()) {
|
||||
GScipSetMaxNumThreads(solve_parameters.threads(), &result);
|
||||
}
|
||||
if (common_solver_parameters.has_enable_output()) {
|
||||
// GScip has also GScipSetOutputEnabled() but this changes the log
|
||||
// level. Setting `silence_output` sets the `quiet` field on the default
|
||||
// message handler of SCIP which removes the output. Here it is important to
|
||||
// use this rather than changing the log level so that if the user registers
|
||||
// for CALLBACK_EVENT_MESSAGE they do get some messages even when
|
||||
// `enable_output` is false.
|
||||
result.set_silence_output(!common_solver_parameters.enable_output());
|
||||
|
||||
if (solve_parameters.has_relative_gap_limit()) {
|
||||
(*result.mutable_real_params())["limits/gap"] =
|
||||
solve_parameters.relative_gap_limit();
|
||||
}
|
||||
if (common_solver_parameters.has_random_seed()) {
|
||||
GScipSetRandomSeed(&result, common_solver_parameters.random_seed());
|
||||
|
||||
if (solve_parameters.has_absolute_gap_limit()) {
|
||||
(*result.mutable_real_params())["limits/absgap"] =
|
||||
solve_parameters.absolute_gap_limit();
|
||||
}
|
||||
if (common_solver_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
|
||||
|
||||
// GScip has also GScipSetOutputEnabled() but this changes the log
|
||||
// level. Setting `silence_output` sets the `quiet` field on the default
|
||||
// message handler of SCIP which removes the output. Here it is important to
|
||||
// use this rather than changing the log level so that if the user registers
|
||||
// for CALLBACK_EVENT_MESSAGE they do get some messages even when
|
||||
// `enable_output` is false.
|
||||
result.set_silence_output(!solve_parameters.enable_output());
|
||||
|
||||
if (solve_parameters.has_random_seed()) {
|
||||
GScipSetRandomSeed(&result, solve_parameters.random_seed());
|
||||
}
|
||||
|
||||
if (solve_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
|
||||
char alg;
|
||||
switch (common_solver_parameters.lp_algorithm()) {
|
||||
switch (solve_parameters.lp_algorithm()) {
|
||||
case LP_ALGORITHM_PRIMAL_SIMPLEX:
|
||||
alg = 'p';
|
||||
break;
|
||||
@@ -434,26 +456,25 @@ GScipParameters GScipSolver::MergeCommonParameters(
|
||||
break;
|
||||
default:
|
||||
LOG(FATAL) << "LPAlgorithm: "
|
||||
<< ProtoEnumToString(common_solver_parameters.lp_algorithm())
|
||||
<< ProtoEnumToString(solve_parameters.lp_algorithm())
|
||||
<< " unknown, error setting gSCIP parameters";
|
||||
}
|
||||
(*result.mutable_char_params())["lp/initalgorithm"] = alg;
|
||||
}
|
||||
if (common_solver_parameters.cuts() != EMPHASIS_UNSPECIFIED) {
|
||||
result.set_separating(
|
||||
ConvertMathOptEmphasis(common_solver_parameters.cuts()));
|
||||
|
||||
if (solve_parameters.cuts() != EMPHASIS_UNSPECIFIED) {
|
||||
result.set_separating(ConvertMathOptEmphasis(solve_parameters.cuts()));
|
||||
}
|
||||
if (common_solver_parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
|
||||
if (solve_parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
|
||||
result.set_heuristics(
|
||||
ConvertMathOptEmphasis(common_solver_parameters.heuristics()));
|
||||
ConvertMathOptEmphasis(solve_parameters.heuristics()));
|
||||
}
|
||||
if (common_solver_parameters.presolve() != EMPHASIS_UNSPECIFIED) {
|
||||
result.set_presolve(
|
||||
ConvertMathOptEmphasis(common_solver_parameters.presolve()));
|
||||
if (solve_parameters.presolve() != EMPHASIS_UNSPECIFIED) {
|
||||
result.set_presolve(ConvertMathOptEmphasis(solve_parameters.presolve()));
|
||||
}
|
||||
if (common_solver_parameters.scaling() != EMPHASIS_UNSPECIFIED) {
|
||||
if (solve_parameters.scaling() != EMPHASIS_UNSPECIFIED) {
|
||||
int scaling_value;
|
||||
switch (common_solver_parameters.scaling()) {
|
||||
switch (solve_parameters.scaling()) {
|
||||
case EMPHASIS_OFF:
|
||||
scaling_value = 0;
|
||||
break;
|
||||
@@ -467,12 +488,14 @@ GScipParameters GScipSolver::MergeCommonParameters(
|
||||
break;
|
||||
default:
|
||||
LOG(FATAL) << "Scaling emphasis: "
|
||||
<< ProtoEnumToString(common_solver_parameters.scaling())
|
||||
<< ProtoEnumToString(solve_parameters.scaling())
|
||||
<< " unknown, error setting gSCIP parameters";
|
||||
}
|
||||
(*result.mutable_int_params())["lp/scaling"] = scaling_value;
|
||||
}
|
||||
result.MergeFrom(gscip_parameters);
|
||||
|
||||
result.MergeFrom(solve_parameters.gscip());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -489,91 +512,119 @@ std::string JoinDetails(const std::string& gscip_detail,
|
||||
return absl::StrCat(gscip_detail, "; ", math_opt_detail);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
ProblemStatusProto GetProblemStatusProto(const GScipOutput::Status gscip_status,
|
||||
const bool has_feasible_solution,
|
||||
const bool has_finite_dual_bound) {
|
||||
ProblemStatusProto problem_status;
|
||||
if (has_feasible_solution) {
|
||||
problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE);
|
||||
} else {
|
||||
problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED);
|
||||
}
|
||||
problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED);
|
||||
|
||||
absl::StatusOr<std::pair<SolveResultProto::TerminationReason, std::string>>
|
||||
GScipSolver::ConvertTerminationReason(const GScipOutput::Status gscip_status,
|
||||
const std::string& gscip_status_detail,
|
||||
const bool has_feasible_solution) {
|
||||
switch (gscip_status) {
|
||||
case GScipOutput::UNKNOWN:
|
||||
return std::make_pair(SolveResultProto::TERMINATION_REASON_UNSPECIFIED,
|
||||
gscip_status_detail);
|
||||
case GScipOutput::USER_INTERRUPT:
|
||||
return std::make_pair(SolveResultProto::INTERRUPTED, gscip_status_detail);
|
||||
case GScipOutput::NODE_LIMIT:
|
||||
return std::make_pair(
|
||||
SolveResultProto::NODE_LIMIT,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"Underlying gSCIP status: NODE_LIMIT."));
|
||||
case GScipOutput::TOTAL_NODE_LIMIT:
|
||||
return std::make_pair(
|
||||
SolveResultProto::NODE_LIMIT,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"Underlying gSCIP status: TOTAL_NODE_LIMIT."));
|
||||
case GScipOutput::STALL_NODE_LIMIT:
|
||||
return std::make_pair(SolveResultProto::SLOW_PROGRESS,
|
||||
gscip_status_detail);
|
||||
case GScipOutput::TIME_LIMIT:
|
||||
return std::make_pair(SolveResultProto::TIME_LIMIT, gscip_status_detail);
|
||||
case GScipOutput::MEM_LIMIT:
|
||||
return std::make_pair(SolveResultProto::MEMORY_LIMIT,
|
||||
gscip_status_detail);
|
||||
|
||||
case GScipOutput::SOL_LIMIT:
|
||||
return std::make_pair(SolveResultProto::SOLUTION_LIMIT,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"Underlying gSCIP status: SOL_LIMIT."));
|
||||
case GScipOutput::BEST_SOL_LIMIT:
|
||||
return std::make_pair(
|
||||
SolveResultProto::SOLUTION_LIMIT,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"Underlying gSCIP status: BEST_SOL_LIMIT."));
|
||||
|
||||
case GScipOutput::RESTART_LIMIT:
|
||||
return std::make_pair(
|
||||
SolveResultProto::OTHER_LIMIT,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"Underlying gSCIP status: RESTART_LIMIT."));
|
||||
case GScipOutput::OPTIMAL:
|
||||
return std::make_pair(SolveResultProto::OPTIMAL,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"Underlying gSCIP status: OPTIMAL."));
|
||||
case GScipOutput::GAP_LIMIT:
|
||||
return std::make_pair(SolveResultProto::OPTIMAL,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"Underlying gSCIP status: GAP_LIMIT."));
|
||||
problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
|
||||
break;
|
||||
case GScipOutput::INFEASIBLE:
|
||||
return std::make_pair(SolveResultProto::INFEASIBLE, gscip_status_detail);
|
||||
problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE);
|
||||
break;
|
||||
case GScipOutput::UNBOUNDED:
|
||||
problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE);
|
||||
break;
|
||||
case GScipOutput::INF_OR_UNBD:
|
||||
problem_status.set_primal_or_dual_infeasible(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (has_finite_dual_bound) {
|
||||
problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE);
|
||||
}
|
||||
return problem_status;
|
||||
}
|
||||
|
||||
absl::StatusOr<TerminationProto> ConvertTerminationReason(
|
||||
const GScipOutput::Status gscip_status,
|
||||
const std::string& gscip_status_detail, const bool has_feasible_solution) {
|
||||
switch (gscip_status) {
|
||||
case GScipOutput::USER_INTERRUPT:
|
||||
return TerminateForLimit(
|
||||
LIMIT_INTERRUPTED,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"underlying gSCIP status: USER_INTERRUPT"));
|
||||
case GScipOutput::NODE_LIMIT:
|
||||
return TerminateForLimit(
|
||||
LIMIT_NODE, JoinDetails(gscip_status_detail,
|
||||
"underlying gSCIP status: NODE_LIMIT"));
|
||||
case GScipOutput::TOTAL_NODE_LIMIT:
|
||||
return TerminateForLimit(
|
||||
LIMIT_NODE, JoinDetails(gscip_status_detail,
|
||||
"underlying gSCIP status: TOTAL_NODE_LIMIT"));
|
||||
case GScipOutput::STALL_NODE_LIMIT:
|
||||
return TerminateForLimit(LIMIT_SLOW_PROGRESS, gscip_status_detail);
|
||||
case GScipOutput::TIME_LIMIT:
|
||||
return TerminateForLimit(LIMIT_TIME, gscip_status_detail);
|
||||
case GScipOutput::MEM_LIMIT:
|
||||
return TerminateForLimit(LIMIT_MEMORY, gscip_status_detail);
|
||||
case GScipOutput::SOL_LIMIT:
|
||||
return TerminateForLimit(
|
||||
LIMIT_SOLUTION, JoinDetails(gscip_status_detail,
|
||||
"underlying gSCIP status: SOL_LIMIT"));
|
||||
case GScipOutput::BEST_SOL_LIMIT:
|
||||
return TerminateForLimit(
|
||||
LIMIT_SOLUTION,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"underlying gSCIP status: BEST_SOL_LIMIT"));
|
||||
case GScipOutput::RESTART_LIMIT:
|
||||
return TerminateForLimit(
|
||||
LIMIT_OTHER, JoinDetails(gscip_status_detail,
|
||||
"underlying gSCIP status: RESTART_LIMIT"));
|
||||
case GScipOutput::OPTIMAL:
|
||||
return TerminateForReason(
|
||||
TERMINATION_REASON_OPTIMAL,
|
||||
JoinDetails(gscip_status_detail, "underlying gSCIP status: OPTIMAL"));
|
||||
case GScipOutput::GAP_LIMIT:
|
||||
return TerminateForReason(
|
||||
TERMINATION_REASON_OPTIMAL,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"underlying gSCIP status: GAP_LIMIT"));
|
||||
case GScipOutput::INFEASIBLE:
|
||||
return TerminateForReason(TERMINATION_REASON_INFEASIBLE,
|
||||
gscip_status_detail);
|
||||
case GScipOutput::UNBOUNDED: {
|
||||
if (has_feasible_solution) {
|
||||
return std::make_pair(
|
||||
SolveResultProto::UNBOUNDED,
|
||||
return TerminateForReason(
|
||||
TERMINATION_REASON_UNBOUNDED,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"Underlying gSCIP status was UNBOUNDED, both primal "
|
||||
"ray and feasible solution are present."));
|
||||
"underlying gSCIP status was UNBOUNDED, both primal "
|
||||
"ray and feasible solution are present"));
|
||||
} else {
|
||||
return std::make_pair(
|
||||
SolveResultProto::DUAL_INFEASIBLE,
|
||||
return TerminateForReason(
|
||||
TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED,
|
||||
JoinDetails(
|
||||
gscip_status_detail,
|
||||
"Underlying gSCIP status was UNBOUNDED, but only primal ray "
|
||||
"was given, no feasible solution was found."));
|
||||
"underlying gSCIP status was UNBOUNDED, but only primal ray "
|
||||
"was given, no feasible solution was found"));
|
||||
}
|
||||
}
|
||||
|
||||
case GScipOutput::INF_OR_UNBD:
|
||||
return std::make_pair(
|
||||
SolveResultProto::DUAL_INFEASIBLE,
|
||||
return TerminateForReason(
|
||||
TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"Underlying gSCIP status: INF_OR_UNBD."));
|
||||
"underlying gSCIP status: INF_OR_UNBD"));
|
||||
|
||||
case GScipOutput::TERMINATE:
|
||||
return std::make_pair(
|
||||
SolveResultProto::OTHER_ERROR,
|
||||
JoinDetails(gscip_status_detail,
|
||||
"Underlying gSCIP status: OTHER_ERROR."));
|
||||
return TerminateForLimit(
|
||||
LIMIT_INTERRUPTED, JoinDetails(gscip_status_detail,
|
||||
"underlying gSCIP status: TERMINATE"));
|
||||
case GScipOutput::INVALID_SOLVER_PARAMETERS:
|
||||
return absl::InvalidArgumentError(gscip_status_detail);
|
||||
case GScipOutput::UNKNOWN:
|
||||
return absl::InternalError(JoinDetails(
|
||||
gscip_status_detail, "Unexpected GScipOutput.status: UNKNOWN"));
|
||||
default:
|
||||
return absl::InternalError(JoinDetails(
|
||||
gscip_status_detail, absl::StrCat("Missing GScipOutput.status case: ",
|
||||
@@ -581,17 +632,21 @@ GScipSolver::ConvertTerminationReason(const GScipOutput::Status gscip_status,
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
absl::StatusOr<SolveResultProto> GScipSolver::CreateSolveResultProto(
|
||||
GScipResult gscip_result,
|
||||
const ModelSolveParametersProto& model_parameters) {
|
||||
SolveResultProto solve_result;
|
||||
ASSIGN_OR_RETURN(
|
||||
const auto reason_and_detail,
|
||||
*solve_result.mutable_termination(),
|
||||
ConvertTerminationReason(gscip_result.gscip_output.status(),
|
||||
gscip_result.gscip_output.status_detail(),
|
||||
!gscip_result.solutions.empty()));
|
||||
solve_result.set_termination_reason(reason_and_detail.first);
|
||||
solve_result.set_termination_detail(reason_and_detail.second);
|
||||
*solve_result.mutable_solve_stats()->mutable_problem_status() =
|
||||
GetProblemStatusProto(
|
||||
gscip_result.gscip_output.status(), !gscip_result.solutions.empty(),
|
||||
std::isfinite(gscip_result.gscip_output.stats().best_bound()));
|
||||
|
||||
const int num_solutions = gscip_result.solutions.size();
|
||||
CHECK_EQ(num_solutions, gscip_result.objective_values.size());
|
||||
@@ -606,18 +661,20 @@ absl::StatusOr<SolveResultProto> GScipSolver::CreateSolveResultProto(
|
||||
return sorted;
|
||||
});
|
||||
for (int i = 0; i < gscip_result.solutions.size(); ++i) {
|
||||
SolutionProto* const solution = solve_result.add_solutions();
|
||||
PrimalSolutionProto* const primal_solution =
|
||||
solve_result.add_primal_solutions();
|
||||
solution->mutable_primal_solution();
|
||||
primal_solution->set_objective_value(gscip_result.objective_values[i]);
|
||||
primal_solution->set_feasibility_status(SOLUTION_STATUS_FEASIBLE);
|
||||
*primal_solution->mutable_variable_values() = FillSparseDoubleVector(
|
||||
sorted_variables.GetOrCreate(), variables_, gscip_result.solutions[i],
|
||||
model_parameters.primal_variables_filter());
|
||||
model_parameters.variable_values_filter());
|
||||
}
|
||||
if (!gscip_result.primal_ray.empty()) {
|
||||
*solve_result.add_primal_rays()->mutable_variable_values() =
|
||||
FillSparseDoubleVector(sorted_variables.GetOrCreate(), variables_,
|
||||
gscip_result.primal_ray,
|
||||
model_parameters.primal_variables_filter());
|
||||
model_parameters.variable_values_filter());
|
||||
}
|
||||
// TODO(user): add support for the basis and dual solutions in gscip, then
|
||||
// populate them here.
|
||||
@@ -634,45 +691,94 @@ absl::StatusOr<SolveResultProto> GScipSolver::CreateSolveResultProto(
|
||||
return solve_result;
|
||||
}
|
||||
|
||||
GScipSolver::GScipSolver(std::unique_ptr<GScip> gscip)
|
||||
: gscip_(std::move(ABSL_DIE_IF_NULL(gscip))) {
|
||||
interrupt_event_handler_.Register(gscip_.get());
|
||||
}
|
||||
|
||||
absl::StatusOr<std::unique_ptr<SolverInterface>> GScipSolver::New(
|
||||
const ModelProto& model, const SolverInitializerProto& initializer) {
|
||||
auto solver = absl::WrapUnique(new GScipSolver);
|
||||
ASSIGN_OR_RETURN(solver->gscip_, GScip::Create(model.name()));
|
||||
RETURN_IF_ERROR(solver->gscip_->SetMaximize(model.objective().maximize()));
|
||||
RETURN_IF_ERROR(
|
||||
solver->gscip_->SetObjectiveOffset(model.objective().offset()));
|
||||
const ModelProto& model, const InitArgs& init_args) {
|
||||
ASSIGN_OR_RETURN(std::unique_ptr<GScip> gscip, GScip::Create(model.name()));
|
||||
RETURN_IF_ERROR(gscip->SetMaximize(model.objective().maximize()));
|
||||
RETURN_IF_ERROR(gscip->SetObjectiveOffset(model.objective().offset()));
|
||||
// TODO(b/204083726): Remove this check if QP support is added
|
||||
if (!model.objective().quadratic_coefficients().row_ids().empty()) {
|
||||
return absl::InvalidArgumentError(
|
||||
"MathOpt does not currently support SCIP models with quadratic "
|
||||
"objectives");
|
||||
}
|
||||
// Can't be const because it had to be moved into the StatusOr and be
|
||||
// convereted to std::unique_ptr<SolverInterface>.
|
||||
auto solver = absl::WrapUnique(new GScipSolver(std::move(gscip)));
|
||||
|
||||
RETURN_IF_ERROR(solver->AddVariables(
|
||||
model.variables(),
|
||||
SparseDoubleVectorAsMap(model.objective().linear_coefficients())));
|
||||
RETURN_IF_ERROR(solver->AddLinearConstraints(
|
||||
model.linear_constraints(), model.linear_constraint_matrix()));
|
||||
|
||||
return solver;
|
||||
}
|
||||
|
||||
absl::StatusOr<SolveResultProto> GScipSolver::Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const CallbackRegistrationProto& callback_registration, const Callback cb) {
|
||||
const MessageCallback message_cb,
|
||||
const CallbackRegistrationProto& callback_registration, const Callback cb,
|
||||
SolveInterrupter* const interrupter) {
|
||||
const absl::Time start = absl::Now();
|
||||
|
||||
RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration,
|
||||
/*supported_events=*/{}));
|
||||
|
||||
const std::unique_ptr<GScipSolverCallbackHandler> callback_handler =
|
||||
GScipSolverCallbackHandler::RegisterIfNeeded(callback_registration, cb,
|
||||
start, gscip_->scip());
|
||||
|
||||
const GScipParameters gscip_parameters = MergeCommonParameters(
|
||||
parameters.common_parameters(), parameters.gscip_parameters());
|
||||
std::unique_ptr<GScipSolverMessageCallbackHandler> message_cb_handler;
|
||||
if (message_cb != nullptr) {
|
||||
message_cb_handler =
|
||||
std::make_unique<GScipSolverMessageCallbackHandler>(message_cb);
|
||||
}
|
||||
|
||||
const GScipParameters gscip_parameters = MergeParameters(parameters);
|
||||
// TODO(user): reorganize gscip to respect warning is error argument on bad
|
||||
// parameters.
|
||||
|
||||
ASSIGN_OR_RETURN(
|
||||
GScipResult gscip_result,
|
||||
gscip_->Solve(
|
||||
gscip_parameters,
|
||||
/*legacy_params=*/"",
|
||||
callback_handler ? callback_handler->MessageHandler() : nullptr));
|
||||
for (const SolutionHintProto& hint : model_parameters.solution_hints()) {
|
||||
absl::flat_hash_map<SCIP_VAR*, double> partial_solution;
|
||||
for (const auto [id, val] : MakeView(hint.variable_values())) {
|
||||
partial_solution.insert({variables_.at(id), val});
|
||||
}
|
||||
RETURN_IF_ERROR(gscip_->SuggestHint(partial_solution).status());
|
||||
}
|
||||
for (const auto [id, value] :
|
||||
MakeView(model_parameters.branching_priorities())) {
|
||||
RETURN_IF_ERROR(gscip_->SetBranchingPriority(variables_.at(id), value));
|
||||
}
|
||||
|
||||
// Before calling solve, set the interrupter on the event handler that calls
|
||||
// SCIPinterruptSolve().
|
||||
if (interrupter != nullptr) {
|
||||
interrupt_event_handler_.interrupter = interrupter;
|
||||
}
|
||||
const auto interrupter_cleanup = absl::MakeCleanup(
|
||||
[&]() { interrupt_event_handler_.interrupter = nullptr; });
|
||||
|
||||
ASSIGN_OR_RETURN(GScipResult gscip_result,
|
||||
gscip_->Solve(gscip_parameters,
|
||||
/*legacy_params=*/"",
|
||||
message_cb_handler != nullptr
|
||||
? message_cb_handler->MessageHandler()
|
||||
: nullptr));
|
||||
|
||||
// Flushes the last unfinished message as early as possible.
|
||||
message_cb_handler.reset();
|
||||
|
||||
if (callback_handler) {
|
||||
RETURN_IF_ERROR(callback_handler->Flush());
|
||||
}
|
||||
|
||||
ASSIGN_OR_RETURN(
|
||||
SolveResultProto result,
|
||||
CreateSolveResultProto(std::move(gscip_result), model_parameters));
|
||||
@@ -693,9 +799,13 @@ absl::flat_hash_set<SCIP_VAR*> GScipSolver::LookupAllVariables(
|
||||
|
||||
bool GScipSolver::CanUpdate(const ModelUpdateProto& model_update) {
|
||||
return gscip_
|
||||
->CanSafeBulkDelete(
|
||||
LookupAllVariables(model_update.deleted_variable_ids()))
|
||||
.ok();
|
||||
->CanSafeBulkDelete(
|
||||
LookupAllVariables(model_update.deleted_variable_ids()))
|
||||
.ok() &&
|
||||
model_update.objective_updates()
|
||||
.quadratic_coefficients()
|
||||
.row_ids()
|
||||
.empty();
|
||||
}
|
||||
|
||||
absl::Status GScipSolver::Update(const ModelUpdateProto& model_update) {
|
||||
@@ -743,6 +853,61 @@ absl::Status GScipSolver::Update(const ModelUpdateProto& model_update) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
GScipSolver::InterruptEventHandler::InterruptEventHandler()
|
||||
: GScipEventHandler(
|
||||
{.name = "interrupt event handler",
|
||||
.description = "Event handler to call SCIPinterruptSolve() when a "
|
||||
"user SolveInterrupter is triggered."}) {}
|
||||
|
||||
SCIP_RETCODE GScipSolver::InterruptEventHandler::Init(GScip* const gscip) {
|
||||
// We don't register any event if we don't have an interrupter.
|
||||
if (interrupter == nullptr) {
|
||||
return SCIP_OKAY;
|
||||
}
|
||||
|
||||
// TODO(b/193537362): see if these events are enough or if we should have more
|
||||
// of these.
|
||||
CatchEvent(SCIP_EVENTTYPE_PRESOLVEROUND);
|
||||
CatchEvent(SCIP_EVENTTYPE_NODEEVENT);
|
||||
|
||||
return TryCallInterruptIfNeeded(gscip);
|
||||
}
|
||||
|
||||
SCIP_RETCODE GScipSolver::InterruptEventHandler::Execute(
|
||||
const GScipEventHandlerContext context) {
|
||||
return TryCallInterruptIfNeeded(context.gscip());
|
||||
}
|
||||
|
||||
SCIP_RETCODE GScipSolver::InterruptEventHandler::TryCallInterruptIfNeeded(
|
||||
GScip* const gscip) {
|
||||
if (interrupter == nullptr) {
|
||||
LOG(WARNING) << "TryCallInterruptIfNeeded() called after interrupter has "
|
||||
"been reset!";
|
||||
return SCIP_OKAY;
|
||||
}
|
||||
|
||||
if (!interrupter->IsInterrupted()) {
|
||||
return SCIP_OKAY;
|
||||
}
|
||||
|
||||
const SCIP_STAGE stage = SCIPgetStage(gscip->scip());
|
||||
switch (stage) {
|
||||
case SCIP_STAGE_INIT:
|
||||
case SCIP_STAGE_FREE:
|
||||
// This should never happen anyway; but if this happens, we may want to
|
||||
// know about it in unit tests.
|
||||
LOG(DFATAL) << "TryCallInterruptIfNeeded() called in stage "
|
||||
<< (stage == SCIP_STAGE_INIT ? "INIT" : "FREE");
|
||||
return SCIP_OKAY;
|
||||
case SCIP_STAGE_INITSOLVE:
|
||||
LOG(WARNING) << "TryCallInterruptIfNeeded() called in INITSOLVE stage; "
|
||||
"we can't call SCIPinterruptSolve() in this stage.";
|
||||
return SCIP_OKAY;
|
||||
default:
|
||||
return SCIPinterruptSolve(gscip->scip());
|
||||
}
|
||||
}
|
||||
|
||||
MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_GSCIP, GScipSolver::New)
|
||||
|
||||
} // namespace math_opt
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "ortools/base/integral_types.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/status/status.h"
|
||||
@@ -29,7 +27,9 @@
|
||||
#include "scip/type_var.h"
|
||||
#include "ortools/gscip/gscip.h"
|
||||
#include "ortools/gscip/gscip.pb.h"
|
||||
#include "ortools/gscip/gscip_event_handler.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_parameters.pb.h"
|
||||
@@ -44,22 +44,59 @@ namespace math_opt {
|
||||
class GScipSolver : public SolverInterface {
|
||||
public:
|
||||
static absl::StatusOr<std::unique_ptr<SolverInterface>> New(
|
||||
const ModelProto& model, const SolverInitializerProto& initializer);
|
||||
const ModelProto& model, const InitArgs& init_args);
|
||||
|
||||
absl::StatusOr<SolveResultProto> Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const CallbackRegistrationProto& callback_registration,
|
||||
Callback cb) override;
|
||||
MessageCallback message_cb,
|
||||
const CallbackRegistrationProto& callback_registration, Callback cb,
|
||||
SolveInterrupter* interrupter) override;
|
||||
absl::Status Update(const ModelUpdateProto& model_update) override;
|
||||
bool CanUpdate(const ModelUpdateProto& model_update) override;
|
||||
|
||||
static GScipParameters MergeCommonParameters(
|
||||
const CommonSolveParametersProto& common_solver_parameters,
|
||||
const GScipParameters& gscip_parameters);
|
||||
static GScipParameters MergeParameters(
|
||||
const SolveParametersProto& solve_parameters);
|
||||
|
||||
private:
|
||||
GScipSolver() = default;
|
||||
// Event handler that it used to call SCIPinterruptSolve() is a safe manner.
|
||||
//
|
||||
// At the start of SCIPsolve(), SCIP resets `userinterrupt` to false. It does
|
||||
// the same in SCIPpresolve(), which is called at the beginning of SCIPsolve()
|
||||
// but also at the beginning of each restart. the `userinterrupt` can also be
|
||||
// reset when the transformed problem is freed when the parameter
|
||||
// "misc/resetstat" is used. On top of that, it is not possible to call
|
||||
// SCIPinterruptSolve() in SCIP_STAGE_INITSOLVE stage; which occurs in the
|
||||
// middle of the solve and at restarts.
|
||||
//
|
||||
// If this was no enough, SCIPinterruptSolve() calls SCIPcheckStage() which is
|
||||
// not thread-safe.
|
||||
//
|
||||
// As a consequence, although it is possible to call SCIPinterruptSolve() from
|
||||
// another thread, it is unreliable at best. Here we take a safer approach: we
|
||||
// call it only from the Exec() of an even handler. This solves all thread
|
||||
// safety issues and, if we have been careful, also ensures we don't call it
|
||||
// in the wrong stage. This also solves the issue the multiple resets of the
|
||||
// `userinterrupt` flag since each time we are called after the interrupter
|
||||
// has been triggered, we simply call SCIPinterruptSolve() until SCIP finally
|
||||
// listens.
|
||||
struct InterruptEventHandler : public GScipEventHandler {
|
||||
InterruptEventHandler();
|
||||
|
||||
SCIP_RETCODE Init(GScip* gscip) override;
|
||||
SCIP_RETCODE Execute(GScipEventHandlerContext) override;
|
||||
|
||||
// Calls SCIPinterruptSolve() if the interrupter is set and triggered and
|
||||
// SCIP is in a valid stage for that.
|
||||
SCIP_RETCODE TryCallInterruptIfNeeded(GScip* gscip);
|
||||
|
||||
// This will be set before SCIPsolve() is called and reset after the end of
|
||||
// the call. It may be null when the user does not provide an interrupter;
|
||||
// in that case we don't register any event.
|
||||
SolveInterrupter* interrupter = nullptr;
|
||||
};
|
||||
|
||||
explicit GScipSolver(std::unique_ptr<GScip> gscip);
|
||||
|
||||
absl::Status AddVariables(const VariablesProto& variables,
|
||||
const absl::flat_hash_map<int64_t, double>&
|
||||
@@ -73,16 +110,12 @@ class GScipSolver : public SolverInterface {
|
||||
const SparseDoubleMatrixProto& linear_constraint_matrix);
|
||||
absl::flat_hash_set<SCIP_VAR*> LookupAllVariables(
|
||||
absl::Span<const int64_t> variable_ids);
|
||||
static absl::StatusOr<
|
||||
std::pair<SolveResultProto::TerminationReason, std::string>>
|
||||
ConvertTerminationReason(GScipOutput::Status gscip_status,
|
||||
const std::string& gscip_status_detail,
|
||||
bool has_feasible_solution);
|
||||
absl::StatusOr<SolveResultProto> CreateSolveResultProto(
|
||||
GScipResult gscip_result,
|
||||
const ModelSolveParametersProto& model_parameters);
|
||||
|
||||
std::unique_ptr<GScip> gscip_;
|
||||
const std::unique_ptr<GScip> gscip_;
|
||||
InterruptEventHandler interrupt_event_handler_;
|
||||
absl::flat_hash_map<int64_t, SCIP_VAR*> variables_;
|
||||
absl::flat_hash_map<int64_t, SCIP_CONS*> linear_constraints_;
|
||||
int64_t next_unused_variable_id_ = 0;
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
#include "ortools/math_opt/solvers/gscip_solver_callback.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
@@ -22,21 +22,14 @@
|
||||
#include "absl/memory/memory.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "absl/types/optional.h"
|
||||
#include "scip/scip.h"
|
||||
#include "scip/type_scip.h"
|
||||
#include "ortools/gscip/gscip_message_handler.h"
|
||||
#include "ortools/linear_solver/scip_helper_macros.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/math_opt_proto_utils.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/solvers/message_callback_data.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
#include "ortools/base/protoutil.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
@@ -47,14 +40,7 @@ GScipSolverCallbackHandler::RegisterIfNeeded(
|
||||
const SolverInterface::Callback callback, const absl::Time solve_start,
|
||||
SCIP* const scip) {
|
||||
// TODO(b/180617976): Don't ignore unknown callbacks.
|
||||
const absl::flat_hash_set<CallbackEventProto> events =
|
||||
EventSet(callback_registration);
|
||||
if (!events.contains(CALLBACK_EVENT_MESSAGE)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return absl::WrapUnique(
|
||||
new GScipSolverCallbackHandler(callback, solve_start, scip));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GScipSolverCallbackHandler::GScipSolverCallbackHandler(
|
||||
@@ -64,79 +50,12 @@ GScipSolverCallbackHandler::GScipSolverCallbackHandler(
|
||||
solve_start_(std::move(solve_start)),
|
||||
scip_(ABSL_DIE_IF_NULL(scip)) {}
|
||||
|
||||
GScipMessageHandler GScipSolverCallbackHandler::MessageHandler() {
|
||||
return std::bind(&GScipSolverCallbackHandler::MessageCallback, this,
|
||||
std::placeholders::_1, std::placeholders::_2);
|
||||
}
|
||||
|
||||
absl::Status GScipSolverCallbackHandler::Flush() {
|
||||
{
|
||||
// Here we don't expect to be called in MessageCallback() at the same time
|
||||
// since GScip is not supposed to call the callback given to GScip::Solve()
|
||||
// after the end of this Solve(). But we lock anyway in case GScip does not
|
||||
// honor its contract or if the caller calls Flush() before the end of the
|
||||
// GScip::Solve().
|
||||
//
|
||||
// See MessageCallback() for the rationale of keeping the lock while calling
|
||||
// CallUserCallback().
|
||||
const absl::MutexLock lock(&message_mutex_);
|
||||
|
||||
absl::optional<CallbackDataProto> data = message_callback_data_.Flush();
|
||||
if (data) {
|
||||
RETURN_IF_ERROR(util_time::EncodeGoogleApiProto(
|
||||
absl::Now() - solve_start_, data->mutable_runtime()))
|
||||
<< "Failed to encode the time.";
|
||||
CallUserCallback(*data);
|
||||
}
|
||||
}
|
||||
|
||||
const absl::MutexLock lock(&callback_mutex_);
|
||||
return status_;
|
||||
}
|
||||
|
||||
void GScipSolverCallbackHandler::MessageCallback(
|
||||
const GScipMessageType type, const absl::string_view message) {
|
||||
// We hold the mutex until the end of the call of the user callback to ensure
|
||||
// proper ordering of messages. We don't expect any user action in the user
|
||||
// callback to trigger another call to MessageCallback(). If it happens to be
|
||||
// the case then we will need to make the code of CallUserCallback()
|
||||
// asynchronous to ensure that we don't end up making recursive calls to the
|
||||
// user callback.
|
||||
const absl::MutexLock lock(&message_mutex_);
|
||||
|
||||
absl::optional<CallbackDataProto> data =
|
||||
message_callback_data_.Parse(message);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const absl::Status runtime_status = util_time::EncodeGoogleApiProto(
|
||||
absl::Now() - solve_start_, data->mutable_runtime());
|
||||
if (!runtime_status.ok()) {
|
||||
absl::MutexLock lock(&callback_mutex_);
|
||||
// Here we must not modify the status if it is already not OK.
|
||||
if (!status_.ok()) {
|
||||
return;
|
||||
}
|
||||
status_ = util::StatusBuilder(runtime_status)
|
||||
<< "Failed to encode the time.";
|
||||
// TODO(b/182919884): Make sure it is correct to use SCIPinterruptSolve()
|
||||
// here and maybe migrate to the same architecture as the one used to
|
||||
// interrupt the solve from foreign threads..
|
||||
const auto interrupt_status = SCIP_TO_STATUS(SCIPinterruptSolve(scip_));
|
||||
LOG_IF(ERROR, !interrupt_status.ok())
|
||||
<< "Failed to interrupt the solve on error: " << interrupt_status;
|
||||
return;
|
||||
}
|
||||
|
||||
// Events of type CALLBACK_EVENT_MESSAGE are not expected to return anything
|
||||
// but `terminate`. Since CallUserCallback() already handles the termination,
|
||||
// we can simply ignore the returned value here.
|
||||
CallUserCallback(*data);
|
||||
}
|
||||
|
||||
absl::optional<CallbackResultProto>
|
||||
GScipSolverCallbackHandler::CallUserCallback(
|
||||
std::optional<CallbackResultProto> GScipSolverCallbackHandler::CallUserCallback(
|
||||
const CallbackDataProto& callback_data) {
|
||||
// We hold the lock during the call of the user callback to ensure only one
|
||||
// call execute at a time. Having multiple calls at once may be an issue when
|
||||
@@ -144,21 +63,10 @@ GScipSolverCallbackHandler::CallUserCallback(
|
||||
// another thread is about to make its call for another callback.
|
||||
//
|
||||
// We don't expect any valid actions taken by the user is a callback to lead
|
||||
// to another callback. That said, a potential corner case could be that
|
||||
// adding a constraint lead to a message callback. If this happens, then this
|
||||
// code will need to be made more complex to deal with that. And there won't
|
||||
// be any easy solution.
|
||||
//
|
||||
// The simplest would be to have the message callbacks being delivered by a
|
||||
// background thread so that a call to MessageCallback() is never blocking on
|
||||
// the user answering to it. The issue here would be to deal with `terminate`
|
||||
// since we can't call SCIPinterruptSolve() from another thread than a SCIP
|
||||
// thread (it is not thread safe). Maybe this means that `terminate` should
|
||||
// not be callable for message callbacks, or that the termination should be
|
||||
// delayed to the next time it can be made.
|
||||
// to another callback.
|
||||
absl::MutexLock lock(&callback_mutex_);
|
||||
if (!status_.ok()) {
|
||||
return absl::nullopt;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::StatusOr<CallbackResultProto> result_or = callback_(callback_data);
|
||||
@@ -176,7 +84,7 @@ GScipSolverCallbackHandler::CallUserCallback(
|
||||
<< interrupt_status;
|
||||
}
|
||||
}
|
||||
return absl::nullopt;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return *std::move(result_or);
|
||||
|
||||
@@ -19,14 +19,11 @@
|
||||
|
||||
#include "absl/base/thread_annotations.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "scip/type_scip.h"
|
||||
#include "ortools/gscip/gscip.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/solvers/message_callback_data.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
@@ -36,6 +33,12 @@ namespace math_opt {
|
||||
// It deals with solve interruption when the user request it or when an error
|
||||
// occurs during the call of the user callback. Any such error is returned by
|
||||
// Flush().
|
||||
//
|
||||
// TODO(b/193537362): see if we need to share code with the handling of
|
||||
// SolveInterrupter. It is likely that it could the case to make sure the
|
||||
// `userinterrupt` flag is not lost. It may require sharing the same SCIP event
|
||||
// handler to make sure the user callback is called first; but maybe that is not
|
||||
// necessary.
|
||||
class GScipSolverCallbackHandler {
|
||||
public:
|
||||
// Returns a non null handler if needed (there are supported events that we
|
||||
@@ -55,9 +58,6 @@ class GScipSolverCallbackHandler {
|
||||
GScipSolverCallbackHandler& operator=(const GScipSolverCallbackHandler&) =
|
||||
delete;
|
||||
|
||||
// Returns the handler to pass to GScip::Solve().
|
||||
GScipMessageHandler MessageHandler();
|
||||
|
||||
// Makes any last pending calls and returns the first error that occurred
|
||||
// while calling the user callback. Returns OkStatus if no error has occurred.
|
||||
absl::Status Flush();
|
||||
@@ -66,11 +66,6 @@ class GScipSolverCallbackHandler {
|
||||
GScipSolverCallbackHandler(SolverInterface::Callback callback,
|
||||
absl::Time solve_start, SCIP* scip);
|
||||
|
||||
// Updates message_callback_data_ and makes the necessary calls to the user
|
||||
// callback if necessary. This method has the expected signature for a
|
||||
// GScipMessageHandler.
|
||||
void MessageCallback(GScipMessageType type, absl::string_view message);
|
||||
|
||||
// Makes a call to the user callback, updating the status_ and interrupting
|
||||
// the solve if needed (in case of error or if requested by the user).
|
||||
//
|
||||
@@ -80,7 +75,7 @@ class GScipSolverCallbackHandler {
|
||||
//
|
||||
// This function will hold the callback_mutex_ while making the call to the
|
||||
// user callback to serialize calls.
|
||||
absl::optional<CallbackResultProto> CallUserCallback(
|
||||
std::optional<CallbackResultProto> CallUserCallback(
|
||||
const CallbackDataProto& callback_data)
|
||||
ABSL_LOCKS_EXCLUDED(callback_mutex_);
|
||||
|
||||
@@ -98,13 +93,6 @@ class GScipSolverCallbackHandler {
|
||||
|
||||
// The first error status returned by the user callback.
|
||||
absl::Status status_ ABSL_GUARDED_BY(callback_mutex_);
|
||||
|
||||
// Mutex serializing access to message_callback_data_ and the serialization of
|
||||
// calls to the user callback for CALLBACK_EVENT_MESSAGE events.
|
||||
absl::Mutex message_mutex_;
|
||||
|
||||
// The buffer used to generate the message events.
|
||||
MessageCallbackData message_callback_data_ ABSL_GUARDED_BY(message_mutex_);
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/solvers/gscip_solver_message_callback_handler.h"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "ortools/gscip/gscip_message_handler.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/solvers/message_callback_data.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
GScipSolverMessageCallbackHandler::GScipSolverMessageCallbackHandler(
|
||||
SolverInterface::MessageCallback message_callback)
|
||||
: message_callback_(std::move(message_callback)) {}
|
||||
|
||||
GScipSolverMessageCallbackHandler::~GScipSolverMessageCallbackHandler() {
|
||||
const absl::MutexLock lock(&message_mutex_);
|
||||
const std::vector<std::string> lines = message_callback_data_.Flush();
|
||||
if (!lines.empty()) {
|
||||
message_callback_(lines);
|
||||
}
|
||||
}
|
||||
|
||||
GScipMessageHandler GScipSolverMessageCallbackHandler::MessageHandler() {
|
||||
return std::bind(&GScipSolverMessageCallbackHandler::MessageCallback, this,
|
||||
std::placeholders::_1, std::placeholders::_2);
|
||||
}
|
||||
|
||||
void GScipSolverMessageCallbackHandler::MessageCallback(
|
||||
GScipMessageType, const absl::string_view message) {
|
||||
const absl::MutexLock lock(&message_mutex_);
|
||||
const std::vector<std::string> lines = message_callback_data_.Parse(message);
|
||||
if (!lines.empty()) {
|
||||
message_callback_(lines);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_SOLVERS_GSCIP_SOLVER_MESSAGE_CALLBACK_HANDLER_H_
|
||||
#define OR_TOOLS_MATH_OPT_SOLVERS_GSCIP_SOLVER_MESSAGE_CALLBACK_HANDLER_H_
|
||||
|
||||
#include "absl/base/thread_annotations.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "ortools/gscip/gscip.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/solvers/message_callback_data.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// Handler for message callbacks.
|
||||
//
|
||||
// The message callback is called on calls to MessageHandler() and when this
|
||||
// object is destroyed (i.e. when we flush the message callback data). Doing so
|
||||
// in the destructor ensures that even in case of solver failure we do call the
|
||||
// message callback with the last pending messages before returning the error.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// std:unique_ptr<GScipSolverMessageCallbackHandler> message_callback_handler;
|
||||
// if (message_callback != nullptr) {
|
||||
// message_callback_handler =
|
||||
// std::make_unique<GScipSolverMessageCallbackHandler>(message_callback);
|
||||
// }
|
||||
//
|
||||
// GScip* gscip = ...;
|
||||
// RETURN_IF_ERROR(
|
||||
// gscip->Solve(...,
|
||||
// message_callback_handler != nullptr
|
||||
// ? message_callback_handler.MessageHandler()
|
||||
// : nullptr);
|
||||
//
|
||||
// // Flush the last unset message as soon as the solve is done. GScip won't
|
||||
// // call the MessageHandler() after the end of the solve so there is no need
|
||||
// // to wait here.
|
||||
// message_callback_handler.reset();
|
||||
//
|
||||
// ...
|
||||
class GScipSolverMessageCallbackHandler {
|
||||
public:
|
||||
// The input callback must not be null.
|
||||
explicit GScipSolverMessageCallbackHandler(
|
||||
SolverInterface::MessageCallback message_callback);
|
||||
|
||||
// Calls the message callback with the last unfinished line if it exists.
|
||||
~GScipSolverMessageCallbackHandler();
|
||||
|
||||
GScipSolverMessageCallbackHandler(const GScipSolverMessageCallbackHandler&) =
|
||||
delete;
|
||||
GScipSolverMessageCallbackHandler& operator=(
|
||||
const GScipSolverMessageCallbackHandler&) = delete;
|
||||
|
||||
// Returns the handler to pass to GScip::Solve().
|
||||
GScipMessageHandler MessageHandler();
|
||||
|
||||
private:
|
||||
// Updates message_callback_data_ and makes the call to the message callback
|
||||
// if necessary. This method has the expected signature for a
|
||||
// GScipMessageHandler.
|
||||
void MessageCallback(GScipMessageType, absl::string_view message);
|
||||
|
||||
// Mutex serializing access to message_callback_data_ and the serialization of
|
||||
// calls to the message callback.
|
||||
absl::Mutex message_mutex_;
|
||||
|
||||
// The message callback; never nullptr. The message_mutex_ should be held
|
||||
// while calling it to ensure proper ordering of the messages.
|
||||
const SolverInterface::MessageCallback message_callback_
|
||||
ABSL_GUARDED_BY(message_mutex_);
|
||||
|
||||
// The buffer used to generate the message events.
|
||||
MessageCallbackData message_callback_data_ ABSL_GUARDED_BY(message_mutex_);
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_SOLVERS_GSCIP_SOLVER_MESSAGE_CALLBACK_HANDLER_H_
|
||||
54
ortools/math_opt/solvers/gurobi.proto
Normal file
54
ortools/math_opt/solvers/gurobi.proto
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Proto messages specific to Gurobi.
|
||||
syntax = "proto3";
|
||||
|
||||
package operations_research.math_opt;
|
||||
|
||||
// Parameters used to initialize the Gurobi solver.
|
||||
message GurobiInitializerProto {
|
||||
message ISVKey {
|
||||
string name = 1;
|
||||
string application_name = 2;
|
||||
int64 expiration = 3;
|
||||
string key = 4;
|
||||
}
|
||||
|
||||
// An optional ISV key to use.
|
||||
//
|
||||
// See http://www.gurobi.com/products/licensing-pricing/isv-program.
|
||||
ISVKey isv_key = 1;
|
||||
}
|
||||
|
||||
// Gurobi specific parameters for solving. See
|
||||
// https://www.gurobi.com/documentation/9.1/refman/parameters.html
|
||||
// for a list of possible parameters.
|
||||
//
|
||||
// Example text proto to set the Barrier Iteration Limit:
|
||||
// parameters : [{name: "BarIterLimit" value: "10}]
|
||||
//
|
||||
// With Gurobi, the order that parameters are applied can have an impact in rare
|
||||
// situations. Parameters are applied in the following order:
|
||||
// * LogToConsole is set from CommonSolveParameters.enable_output.
|
||||
// * Any common parameters not overwritten by GurobiParameters.
|
||||
// * param_values in iteration order (insertion order).
|
||||
// We set LogToConsole first because setting other parameters can generate
|
||||
// output.
|
||||
message GurobiParametersProto {
|
||||
message Parameter {
|
||||
string name = 1;
|
||||
string value = 2;
|
||||
}
|
||||
repeated Parameter parameters = 1;
|
||||
}
|
||||
23
ortools/math_opt/solvers/gurobi/BUILD.bazel
Normal file
23
ortools/math_opt/solvers/gurobi/BUILD.bazel
Normal file
@@ -0,0 +1,23 @@
|
||||
package(default_visibility = ["//ortools/math_opt:__subpackages__"])
|
||||
|
||||
cc_library(
|
||||
name = "g_gurobi",
|
||||
srcs = [
|
||||
"g_gurobi.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"g_gurobi.h",
|
||||
],
|
||||
deps = [
|
||||
"//ortools/base",
|
||||
"//ortools/base:cleanup",
|
||||
"//ortools/base:source_location",
|
||||
"//ortools/base:status_macros",
|
||||
"//ortools/gurobi:environment",
|
||||
"@com_google_absl//absl/memory",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/strings:str_format",
|
||||
"@com_google_absl//absl/types:span",
|
||||
],
|
||||
)
|
||||
585
ortools/math_opt/solvers/gurobi/g_gurobi.cc
Normal file
585
ortools/math_opt/solvers/gurobi/g_gurobi.cc
Normal file
@@ -0,0 +1,585 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/solvers/gurobi/g_gurobi.h"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "ortools/base/cleanup.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/str_format.h"
|
||||
#include "ortools/base/source_location.h"
|
||||
#include "ortools/base/status_builder.h"
|
||||
#include "ortools/base/status_macros.h"
|
||||
|
||||
namespace operations_research::math_opt {
|
||||
|
||||
namespace {
|
||||
constexpr int kGrbOk = 0;
|
||||
|
||||
struct UserCallbackData {
|
||||
Gurobi::Callback user_cb;
|
||||
absl::Status status = absl::OkStatus();
|
||||
Gurobi* gurobi = nullptr;
|
||||
};
|
||||
|
||||
int GurobiCallback(GRBmodel* const model, void* const cbdata, const int where,
|
||||
void* const usrdata) {
|
||||
CHECK(usrdata != nullptr);
|
||||
CHECK(model != nullptr);
|
||||
auto user_cb_data = static_cast<UserCallbackData*>(usrdata);
|
||||
CHECK_EQ(model, user_cb_data->gurobi->model());
|
||||
// NOTE: if a previous callback failed, we never run the callback again.
|
||||
if (!user_cb_data->status.ok()) {
|
||||
return GRB_ERROR_CALLBACK;
|
||||
}
|
||||
const Gurobi::CallbackContext context(user_cb_data->gurobi, cbdata, where);
|
||||
user_cb_data->status = user_cb_data->user_cb(context);
|
||||
if (!user_cb_data->status.ok()) {
|
||||
user_cb_data->gurobi->Terminate();
|
||||
return GRB_ERROR_CALLBACK;
|
||||
}
|
||||
return kGrbOk;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void GurobiFreeEnv::operator()(GRBenv* const env) const {
|
||||
if (env != nullptr) {
|
||||
GRBfreeenv(env);
|
||||
}
|
||||
}
|
||||
|
||||
absl::StatusOr<GRBenvUniquePtr> GurobiNewMasterEnv(
|
||||
const std::optional<GurobiIsvKey>& isv_key) {
|
||||
GRBenv* naked_master_env = nullptr;
|
||||
int err;
|
||||
std::string_view init_env_method;
|
||||
if (isv_key.has_value()) {
|
||||
err = GRBisqp(&naked_master_env, /*logfilename=*/
|
||||
nullptr, isv_key->name.c_str(),
|
||||
isv_key->application_name.c_str(), isv_key->expiration,
|
||||
isv_key->key.c_str());
|
||||
init_env_method = "GRBisqp()";
|
||||
} else {
|
||||
err = GRBloadenv(&naked_master_env, /*logfilename=*/nullptr);
|
||||
init_env_method = "GRBloadenv()";
|
||||
}
|
||||
if (err != kGrbOk) {
|
||||
// Surprisingly, even when Gurobi fails to load the environment, it still
|
||||
// creates one. Here we make sure to free it properly.
|
||||
//
|
||||
// We can also use it with GRBgeterrormsg() to get the associated error
|
||||
// message that goes with the error and the contains additional data like
|
||||
// the user, the host and the hostid.
|
||||
const GRBenvUniquePtr master_env(naked_master_env);
|
||||
return util::InvalidArgumentErrorBuilder()
|
||||
<< "failed to create Gurobi master environment, " << init_env_method
|
||||
<< " returned the error (" << err
|
||||
<< "): " << GRBgeterrormsg(master_env.get());
|
||||
}
|
||||
return GRBenvUniquePtr(naked_master_env);
|
||||
}
|
||||
|
||||
absl::StatusOr<std::unique_ptr<Gurobi>> Gurobi::NewWithSharedMasterEnv(
|
||||
GRBenv* const master_env) {
|
||||
CHECK(master_env != nullptr);
|
||||
return New(nullptr, master_env);
|
||||
}
|
||||
|
||||
absl::StatusOr<std::unique_ptr<Gurobi>> Gurobi::New(
|
||||
GRBenvUniquePtr master_env) {
|
||||
if (master_env == nullptr) {
|
||||
ASSIGN_OR_RETURN(master_env, GurobiNewMasterEnv());
|
||||
}
|
||||
GRBenv* const raw_master_env = master_env.get();
|
||||
return New(std::move(master_env), raw_master_env);
|
||||
}
|
||||
|
||||
Gurobi::Gurobi(GRBenvUniquePtr optional_owned_master_env, GRBmodel* const model,
|
||||
GRBenv* const model_env)
|
||||
: owned_master_env_(std::move(optional_owned_master_env)),
|
||||
gurobi_model_(ABSL_DIE_IF_NULL(model)),
|
||||
model_env_(ABSL_DIE_IF_NULL(model_env)) {}
|
||||
|
||||
absl::StatusOr<std::unique_ptr<Gurobi>> Gurobi::New(
|
||||
GRBenvUniquePtr optional_owned_master_env, GRBenv* const master_env) {
|
||||
CHECK(master_env != nullptr);
|
||||
GRBmodel* model = nullptr;
|
||||
const int err = GRBnewmodel(master_env, &model,
|
||||
/*Pname=*/nullptr,
|
||||
/*numvars=*/0,
|
||||
/*obj=*/nullptr, /*lb=*/nullptr,
|
||||
/*ub=*/nullptr, /*vtype=*/nullptr,
|
||||
/*varnames=*/nullptr);
|
||||
if (err != kGrbOk) {
|
||||
return util::InvalidArgumentErrorBuilder()
|
||||
<< "Error creating gurobi model on GRBnewmodel(), error code: "
|
||||
<< err << " message: " << GRBgeterrormsg(master_env);
|
||||
}
|
||||
CHECK(model != nullptr);
|
||||
GRBenv* const model_env = GRBgetenv(model);
|
||||
|
||||
if (VLOG_IS_ON(3)) {
|
||||
int gurobi_major, gurobi_minor, gurobi_technical;
|
||||
GRBversion(&gurobi_major, &gurobi_minor, &gurobi_technical);
|
||||
VLOG(3) << absl::StrFormat(
|
||||
"Successfully created model for Gurobi v%d.%d.%d (%s)", gurobi_major,
|
||||
gurobi_minor, gurobi_technical, GRBplatform());
|
||||
}
|
||||
return absl::WrapUnique(
|
||||
new Gurobi(std::move(optional_owned_master_env), model, model_env));
|
||||
}
|
||||
|
||||
Gurobi::~Gurobi() {
|
||||
const int err = GRBfreemodel(gurobi_model_);
|
||||
if (err != kGrbOk) {
|
||||
LOG(ERROR) << "Error freeing gurobi model, code: " << err
|
||||
<< ", message: " << GRBgeterrormsg(model_env_);
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status Gurobi::ToStatus(const int grb_err, const absl::StatusCode code,
|
||||
const absl::SourceLocation loc) const {
|
||||
if (grb_err == kGrbOk) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
return util::StatusBuilder(code)
|
||||
<< "Gurobi error code: " << grb_err
|
||||
<< ", message: " << GRBgeterrormsg(model_env_);
|
||||
}
|
||||
|
||||
absl::Status Gurobi::AddVars(const absl::Span<const double> obj,
|
||||
const absl::Span<const double> lb,
|
||||
const absl::Span<const double> ub,
|
||||
const absl::Span<const char> vtype,
|
||||
const absl::Span<const std::string> names) {
|
||||
return AddVars({}, {}, {}, obj, lb, ub, vtype, names);
|
||||
}
|
||||
|
||||
absl::Status Gurobi::AddVars(const absl::Span<const int> vbegin,
|
||||
const absl::Span<const int> vind,
|
||||
const absl::Span<const double> vval,
|
||||
const absl::Span<const double> obj,
|
||||
const absl::Span<const double> lb,
|
||||
const absl::Span<const double> ub,
|
||||
const absl::Span<const char> vtype,
|
||||
const absl::Span<const std::string> names) {
|
||||
CHECK_EQ(vind.size(), vval.size());
|
||||
const int num_vars = lb.size();
|
||||
CHECK_EQ(ub.size(), num_vars);
|
||||
CHECK_EQ(vtype.size(), num_vars);
|
||||
double* c_obj = nullptr;
|
||||
if (!obj.empty()) {
|
||||
CHECK_EQ(obj.size(), num_vars);
|
||||
c_obj = const_cast<double*>(obj.data());
|
||||
}
|
||||
if (!vbegin.empty()) {
|
||||
CHECK_EQ(vbegin.size(), num_vars);
|
||||
}
|
||||
char** c_names = nullptr;
|
||||
std::vector<char*> c_names_data;
|
||||
if (!names.empty()) {
|
||||
CHECK_EQ(num_vars, names.size());
|
||||
for (const std::string& name : names) {
|
||||
c_names_data.push_back(const_cast<char*>(name.c_str()));
|
||||
}
|
||||
c_names = c_names_data.data();
|
||||
}
|
||||
return ToStatus(GRBaddvars(/*model=*/gurobi_model_, /*numvars=*/num_vars,
|
||||
/*numnz=*/vind.size(),
|
||||
/*vbeg=*/const_cast<int*>(vbegin.data()),
|
||||
/*vind=*/const_cast<int*>(vind.data()),
|
||||
/*vval=*/const_cast<double*>(vval.data()),
|
||||
/*obj=*/c_obj,
|
||||
/*lb=*/const_cast<double*>(lb.data()),
|
||||
/*ub=*/const_cast<double*>(ub.data()),
|
||||
/*vtype=*/const_cast<char*>(vtype.data()),
|
||||
/*varnames=*/c_names));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::DelVars(const absl::Span<const int> ind) {
|
||||
return ToStatus(
|
||||
GRBdelvars(gurobi_model_, ind.size(), const_cast<int*>(ind.data())));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::AddConstrs(const absl::Span<const char> sense,
|
||||
const absl::Span<const double> rhs,
|
||||
const absl::Span<const std::string> names) {
|
||||
const int num_cons = sense.size();
|
||||
CHECK_EQ(rhs.size(), num_cons);
|
||||
char** c_names = nullptr;
|
||||
std::vector<char*> c_names_data;
|
||||
if (!names.empty()) {
|
||||
CHECK_EQ(num_cons, names.size());
|
||||
for (const std::string& name : names) {
|
||||
c_names_data.push_back(const_cast<char*>(name.c_str()));
|
||||
}
|
||||
c_names = c_names_data.data();
|
||||
}
|
||||
return ToStatus(GRBaddconstrs(
|
||||
/*model=*/gurobi_model_,
|
||||
/*numconstrs=*/num_cons,
|
||||
/*numnz=*/0, /*cbeg=*/nullptr, /*cind=*/nullptr,
|
||||
/*cval=*/nullptr, /*sense=*/const_cast<char*>(sense.data()),
|
||||
/*rhs=*/const_cast<double*>(rhs.data()), /*constrnames=*/c_names));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::DelConstrs(const absl::Span<const int> ind) {
|
||||
return ToStatus(
|
||||
GRBdelconstrs(gurobi_model_, ind.size(), const_cast<int*>(ind.data())));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::AddQpTerms(const absl::Span<const int> qrow,
|
||||
const absl::Span<const int> qcol,
|
||||
const absl::Span<const double> qval) {
|
||||
const int numqnz = qrow.size();
|
||||
CHECK_EQ(qcol.size(), numqnz);
|
||||
CHECK_EQ(qval.size(), numqnz);
|
||||
return ToStatus(GRBaddqpterms(
|
||||
gurobi_model_, numqnz, const_cast<int*>(qcol.data()),
|
||||
const_cast<int*>(qrow.data()), const_cast<double*>(qval.data())));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::DelQ() { return ToStatus(GRBdelq(gurobi_model_)); }
|
||||
|
||||
absl::Status Gurobi::ChgCoeffs(const absl::Span<const int> cind,
|
||||
const absl::Span<const int> vind,
|
||||
const absl::Span<const double> val) {
|
||||
const int num_changes = cind.size();
|
||||
CHECK_EQ(vind.size(), num_changes);
|
||||
CHECK_EQ(val.size(), num_changes);
|
||||
return ToStatus(GRBchgcoeffs(
|
||||
gurobi_model_, num_changes, const_cast<int*>(cind.data()),
|
||||
const_cast<int*>(vind.data()), const_cast<double*>(val.data())));
|
||||
}
|
||||
|
||||
absl::StatusOr<int> Gurobi::GetNnz(const int first_var, const int num_vars) {
|
||||
int nnz = 0;
|
||||
RETURN_IF_ERROR(ToStatus(GRBgetvars(gurobi_model_, &nnz, nullptr, nullptr,
|
||||
nullptr, first_var, num_vars)));
|
||||
return nnz;
|
||||
}
|
||||
|
||||
absl::Status Gurobi::GetVars(const absl::Span<int> vbegin,
|
||||
const absl::Span<int> vind,
|
||||
const absl::Span<double> vval, const int first_var,
|
||||
const int num_vars) {
|
||||
CHECK_EQ(vbegin.size(), num_vars);
|
||||
CHECK_EQ(vind.size(), vval.size());
|
||||
int nnz = 0;
|
||||
RETURN_IF_ERROR(
|
||||
ToStatus(GRBgetvars(gurobi_model_, &nnz, vbegin.data(), vind.data(),
|
||||
vval.data(), first_var, num_vars)));
|
||||
CHECK_EQ(nnz, vind.size());
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<Gurobi::SparseMat> Gurobi::GetVars(const int first_var,
|
||||
const int num_vars) {
|
||||
SparseMat result;
|
||||
ASSIGN_OR_RETURN(const int nnz, GetNnz(first_var, num_vars));
|
||||
result.begins.resize(num_vars);
|
||||
result.inds.resize(nnz);
|
||||
result.vals.resize(nnz);
|
||||
int read_nnz = 0;
|
||||
RETURN_IF_ERROR(ToStatus(
|
||||
GRBgetvars(gurobi_model_, &read_nnz, result.begins.data(),
|
||||
result.inds.data(), result.vals.data(), first_var, num_vars)));
|
||||
CHECK_EQ(read_nnz, nnz);
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::Status Gurobi::UpdateModel() {
|
||||
return ToStatus(GRBupdatemodel(gurobi_model_));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::Optimize(Callback cb) {
|
||||
bool needs_cb_cleanup = false;
|
||||
UserCallbackData user_cb_data;
|
||||
if (cb != nullptr) {
|
||||
user_cb_data.user_cb = std::move(cb);
|
||||
user_cb_data.gurobi = this;
|
||||
RETURN_IF_ERROR(ToStatus(
|
||||
GRBsetcallbackfunc(gurobi_model_, GurobiCallback, &user_cb_data)));
|
||||
needs_cb_cleanup = true;
|
||||
}
|
||||
|
||||
// Failsafe to try and clear the callback if there is another error. We cannot
|
||||
// raise an error in a destructor, we can only log it.
|
||||
auto callback_cleanup = absl::MakeCleanup([&]() {
|
||||
if (needs_cb_cleanup) {
|
||||
int error = GRBsetcallbackfunc(gurobi_model_, nullptr, nullptr);
|
||||
if (error != kGrbOk) {
|
||||
LOG(ERROR) << "Error cleaning up callback";
|
||||
}
|
||||
}
|
||||
});
|
||||
absl::Status solve_status = ToStatus(GRBoptimize(gurobi_model_));
|
||||
RETURN_IF_ERROR(user_cb_data.status) << "Error in Optimize callback.";
|
||||
RETURN_IF_ERROR(solve_status);
|
||||
if (needs_cb_cleanup) {
|
||||
needs_cb_cleanup = false;
|
||||
RETURN_IF_ERROR(
|
||||
ToStatus(GRBsetcallbackfunc(gurobi_model_, nullptr, nullptr)));
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
bool Gurobi::IsAttrAvailable(const char* name) const {
|
||||
return GRBisattravailable(gurobi_model_, name) > 0;
|
||||
}
|
||||
|
||||
absl::StatusOr<int> Gurobi::GetIntAttr(const char* const name) const {
|
||||
int result;
|
||||
RETURN_IF_ERROR(ToStatus(GRBgetintattr(gurobi_model_, name, &result)))
|
||||
<< "Error getting Gurobi int attribute: " << name;
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<double> Gurobi::GetDoubleAttr(const char* const name) const {
|
||||
double result;
|
||||
RETURN_IF_ERROR(ToStatus(GRBgetdblattr(gurobi_model_, name, &result)))
|
||||
<< "Error getting Gurobi double attribute: " << name;
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::string> Gurobi::GetStringAttr(
|
||||
const char* const name) const {
|
||||
// WARNING: if a string attribute is the empty string, we need to be careful,
|
||||
// std::string(char*) cannot take a nullptr.
|
||||
char* result = nullptr;
|
||||
RETURN_IF_ERROR(ToStatus(GRBgetstrattr(gurobi_model_, name, &result)))
|
||||
<< "Error getting Gurobi string attribute: " << name;
|
||||
if (result == nullptr) {
|
||||
return std::string();
|
||||
}
|
||||
return std::string(result);
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetStringAttr(const char* const attr_name,
|
||||
const std::string& value) {
|
||||
return ToStatus(GRBsetstrattr(gurobi_model_, attr_name, value.c_str()));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetIntAttr(const char* const attr_name, const int value) {
|
||||
return ToStatus(GRBsetintattr(gurobi_model_, attr_name, value));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetDoubleAttr(const char* const attr_name,
|
||||
const double value) {
|
||||
return ToStatus(GRBsetdblattr(gurobi_model_, attr_name, value));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetIntAttrArray(const char* const name,
|
||||
const absl::Span<const int> new_values) {
|
||||
return ToStatus(GRBsetintattrarray(gurobi_model_, name, 0, new_values.size(),
|
||||
const_cast<int*>(new_values.data())));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetDoubleAttrArray(
|
||||
const char* const name, const absl::Span<const double> new_values) {
|
||||
return ToStatus(GRBsetdblattrarray(gurobi_model_, name, 0, new_values.size(),
|
||||
const_cast<double*>(new_values.data())));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetCharAttrArray(const char* const name,
|
||||
const absl::Span<const char> new_values) {
|
||||
return ToStatus(GRBsetcharattrarray(gurobi_model_, name, 0, new_values.size(),
|
||||
const_cast<char*>(new_values.data())));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::GetIntAttrArray(const char* const name,
|
||||
const absl::Span<int> attr_out) const {
|
||||
RETURN_IF_ERROR(ToStatus(GRBgetintattrarray(
|
||||
gurobi_model_, name, 0, attr_out.size(), attr_out.data())))
|
||||
<< "Error getting Gurobi int array attribute: " << name;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<std::vector<int>> Gurobi::GetIntAttrArray(const char* const name,
|
||||
const int len) const {
|
||||
std::vector<int> result(len);
|
||||
RETURN_IF_ERROR(GetIntAttrArray(name, absl::MakeSpan(result)));
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::Status Gurobi::GetDoubleAttrArray(
|
||||
const char* const name, const absl::Span<double> attr_out) const {
|
||||
RETURN_IF_ERROR(ToStatus(GRBgetdblattrarray(
|
||||
gurobi_model_, name, 0, attr_out.size(), attr_out.data())))
|
||||
<< "Error getting Gurobi double array attribute: " << name;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<std::vector<double>> Gurobi::GetDoubleAttrArray(
|
||||
const char* const name, const int len) const {
|
||||
std::vector<double> result(len);
|
||||
RETURN_IF_ERROR(GetDoubleAttrArray(name, absl::MakeSpan(result)));
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::Status Gurobi::GetCharAttrArray(const char* const name,
|
||||
const absl::Span<char> attr_out) const {
|
||||
RETURN_IF_ERROR(ToStatus(GRBgetcharattrarray(
|
||||
gurobi_model_, name, 0, attr_out.size(), attr_out.data())))
|
||||
<< "Error getting Gurobi char array attribute: " << name;
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::StatusOr<std::vector<char>> Gurobi::GetCharAttrArray(
|
||||
const char* const name, const int len) const {
|
||||
std::vector<char> result(len);
|
||||
RETURN_IF_ERROR(GetCharAttrArray(name, absl::MakeSpan(result)));
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetIntAttrList(const char* const name,
|
||||
const absl::Span<const int> ind,
|
||||
const absl::Span<const int> new_values) {
|
||||
const int len = ind.size();
|
||||
CHECK_EQ(new_values.size(), len);
|
||||
return ToStatus(GRBsetintattrlist(gurobi_model_, name, len,
|
||||
const_cast<int*>(ind.data()),
|
||||
const_cast<int*>(new_values.data())));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetDoubleAttrList(
|
||||
const char* const name, const absl::Span<const int> ind,
|
||||
const absl::Span<const double> new_values) {
|
||||
const int len = ind.size();
|
||||
CHECK_EQ(new_values.size(), len);
|
||||
return ToStatus(GRBsetdblattrlist(gurobi_model_, name, len,
|
||||
const_cast<int*>(ind.data()),
|
||||
const_cast<double*>(new_values.data())));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetCharAttrList(const char* const name,
|
||||
const absl::Span<const int> ind,
|
||||
const absl::Span<const char> new_values) {
|
||||
const int len = ind.size();
|
||||
CHECK_EQ(new_values.size(), len);
|
||||
return ToStatus(GRBsetcharattrlist(gurobi_model_, name, len,
|
||||
const_cast<int*>(ind.data()),
|
||||
const_cast<char*>(new_values.data())));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetParam(const char* const name,
|
||||
const std::string& value) {
|
||||
return ToStatus(GRBsetparam(model_env_, name, value.c_str()));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetIntParam(const char* const name, const int value) {
|
||||
return ToStatus(GRBsetintparam(model_env_, name, value));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetDoubleParam(const char* const name,
|
||||
const double value) {
|
||||
return ToStatus(GRBsetdblparam(model_env_, name, value));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::SetStringParam(const char* const name,
|
||||
const std::string& value) {
|
||||
return ToStatus(GRBsetstrparam(model_env_, name, value.c_str()));
|
||||
}
|
||||
|
||||
absl::StatusOr<int> Gurobi::GetIntParam(const char* const name) {
|
||||
int result;
|
||||
RETURN_IF_ERROR(ToStatus(GRBgetintparam(model_env_, name, &result)));
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<double> Gurobi::GetDoubleParam(const char* const name) {
|
||||
double result;
|
||||
RETURN_IF_ERROR(ToStatus(GRBgetdblparam(model_env_, name, &result)));
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::string> Gurobi::GetStringParam(const char* const name) {
|
||||
std::vector<char> result(GRB_MAX_STRLEN);
|
||||
RETURN_IF_ERROR(ToStatus(GRBgetstrparam(model_env_, name, result.data())));
|
||||
return std::string(result.data());
|
||||
}
|
||||
|
||||
absl::Status Gurobi::ResetParameters() {
|
||||
return ToStatus(GRBresetparams(model_env_));
|
||||
}
|
||||
|
||||
void Gurobi::Terminate() { GRBterminate(gurobi_model_); }
|
||||
|
||||
Gurobi::CallbackContext::CallbackContext(Gurobi* const gurobi,
|
||||
void* const cb_data, const int where)
|
||||
: gurobi_(ABSL_DIE_IF_NULL(gurobi)), cb_data_(cb_data), where_(where) {}
|
||||
|
||||
absl::StatusOr<int> Gurobi::CallbackContext::CbGetInt(const int what) const {
|
||||
int result;
|
||||
RETURN_IF_ERROR(gurobi_->ToStatus(
|
||||
GRBcbget(cb_data_, where_, what, static_cast<void*>(&result))));
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::StatusOr<double> Gurobi::CallbackContext::CbGetDouble(
|
||||
const int what) const {
|
||||
double result;
|
||||
RETURN_IF_ERROR(gurobi_->ToStatus(
|
||||
GRBcbget(cb_data_, where_, what, static_cast<void*>(&result))));
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::Status Gurobi::CallbackContext::CbGetDoubleArray(
|
||||
const int what, const absl::Span<double> result) const {
|
||||
return gurobi_->ToStatus(
|
||||
GRBcbget(cb_data_, where_, what, static_cast<void*>(result.data())));
|
||||
}
|
||||
|
||||
absl::StatusOr<std::string> Gurobi::CallbackContext::CbGetMessage() const {
|
||||
char* result = nullptr;
|
||||
RETURN_IF_ERROR(gurobi_->ToStatus(GRBcbget(
|
||||
cb_data_, where_, GRB_CB_MSG_STRING, static_cast<void*>(&result))));
|
||||
if (result == nullptr) {
|
||||
return std::string();
|
||||
}
|
||||
return std::string(result);
|
||||
}
|
||||
|
||||
absl::Status Gurobi::CallbackContext::CbCut(
|
||||
const absl::Span<const int> cutind, const absl::Span<const double> cutval,
|
||||
const char cutsense, const double cutrhs) const {
|
||||
const int cut_len = cutind.size();
|
||||
CHECK_EQ(cutval.size(), cut_len);
|
||||
return gurobi_->ToStatus(
|
||||
GRBcbcut(cb_data_, cut_len, const_cast<int*>(cutind.data()),
|
||||
const_cast<double*>(cutval.data()), cutsense, cutrhs));
|
||||
}
|
||||
|
||||
absl::Status Gurobi::CallbackContext::CbLazy(
|
||||
const absl::Span<const int> lazyind, const absl::Span<const double> lazyval,
|
||||
const char lazysense, const double lazyrhs) const {
|
||||
const int lazy_len = lazyind.size();
|
||||
CHECK_EQ(lazyval.size(), lazy_len);
|
||||
return gurobi_->ToStatus(
|
||||
GRBcblazy(cb_data_, lazy_len, const_cast<int*>(lazyind.data()),
|
||||
const_cast<double*>(lazyval.data()), lazysense, lazyrhs));
|
||||
}
|
||||
|
||||
absl::StatusOr<double> Gurobi::CallbackContext::CbSolution(
|
||||
const absl::Span<const double> solution) const {
|
||||
double result;
|
||||
RETURN_IF_ERROR(gurobi_->ToStatus(
|
||||
GRBcbsolution(cb_data_, const_cast<double*>(solution.data()), &result)));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace operations_research::math_opt
|
||||
481
ortools/math_opt/solvers/gurobi/g_gurobi.h
Normal file
481
ortools/math_opt/solvers/gurobi/g_gurobi.h
Normal file
@@ -0,0 +1,481 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Google C++ bindings for Gurobi C API.
|
||||
//
|
||||
// Attempts to be as close to the Gurobi C API as possible, with the following
|
||||
// differences:
|
||||
// * Use destructors to automatically clean up the environment and model.
|
||||
// * Use absl::Status to propagate errors instead of int gurobi error codes.
|
||||
// * Use absl::StatusOr instead of output arguments.
|
||||
// * Use absl::Span<T> instead of T* and size for array args.
|
||||
// * Use std::string instead of null terminated char* for string values (note
|
||||
// that attribute names are still char*).
|
||||
// * When setting array data, accept const data (absl::Span<const T>).
|
||||
// * Callbacks are passed as an argument to optimize and then are cleared.
|
||||
// * Callbacks propagate errors with status.
|
||||
// * There is no distinction between a GRBmodel and the GRBenv created for a
|
||||
// model, they are jointly captured by the newly defined Gurobi object.
|
||||
// * Parameters are set on the Gurobi class rather than on a GRBenv. We do not
|
||||
// provide an API fo setting parameters on the master environment, only on
|
||||
// the child environment created by GRBnewmodel (for details see
|
||||
// https://www.gurobi.com/documentation/9.1/refman/c_newmodel.html ).
|
||||
#ifndef OR_TOOLS_MATH_OPT_SOLVERS_GUROBI_G_GUROBI_H_
|
||||
#define OR_TOOLS_MATH_OPT_SOLVERS_GUROBI_G_GUROBI_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "ortools/base/source_location.h"
|
||||
#include "absl/types/span.h"
|
||||
|
||||
#include "ortools/gurobi/environment.h"
|
||||
|
||||
namespace operations_research::math_opt {
|
||||
|
||||
// An ISV key for the Gurobi solver, an alternative to using a license file.
|
||||
//
|
||||
// See http://www.gurobi.com/products/licensing-pricing/isv-program.
|
||||
struct GurobiIsvKey {
|
||||
std::string name;
|
||||
std::string application_name;
|
||||
int64_t expiration = 0;
|
||||
std::string key;
|
||||
};
|
||||
|
||||
// Functor to use as deleter for std::unique_ptr that stores a master GRBenv,
|
||||
// used by GRBenvUniquePtr. Most users will not use this directly.
|
||||
struct GurobiFreeEnv {
|
||||
void operator()(GRBenv* const env) const;
|
||||
};
|
||||
|
||||
// Unique pointer to a GRBenv. It destroys the environment on destruction
|
||||
// calling GRBfreeenv. Most users will not use this directly.
|
||||
using GRBenvUniquePtr = std::unique_ptr<GRBenv, GurobiFreeEnv>;
|
||||
|
||||
// Returns a new master Gurobi environment, using the ISV key if provided, or a
|
||||
// regular license otherwise. Gurobi::New() creates an environment automatically
|
||||
// if not provided, so most users will not use this directly.
|
||||
absl::StatusOr<GRBenvUniquePtr> GurobiNewMasterEnv(
|
||||
const std::optional<GurobiIsvKey>& isv_key = std::nullopt);
|
||||
|
||||
// Models and solves optimization problems with Gurobi.
|
||||
//
|
||||
// This is a thin wrapper on the Gurobi C API, holding a GRBmodel,
|
||||
// associated GRBenv that GRBnewmodel creates, and optionally the master
|
||||
// environment to clean up on deletion.
|
||||
//
|
||||
// Throughout, we refer to the child GRBenv created by GRBnewmodel as the
|
||||
// "model environment" while the GRBenv that was used to create the model as
|
||||
// the "master environment", for details see:
|
||||
// https://www.gurobi.com/documentation/9.1/refman/c_newmodel.html
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Attributes
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Most properties of a Gurobi optimization model are set and read with
|
||||
// attributes, using the attribute names defined in the Gurobi C API. There are
|
||||
// scalar attributes returning a single value of the following types:
|
||||
// * int, e.g. GRB_INT_ATTR_MODELSENSE
|
||||
// * double, e.g. GRB_DBL_ATTR_OBJVAL
|
||||
// * string, e.g. GRB_STR_ATTR_MODELNAME
|
||||
// and array attributes returning a list of values of the following types:
|
||||
// * int array, e.g. GRB_INT_ATTR_BRANCHPRIORITY
|
||||
// * double array, e.g. GRB_DBL_ATTR_LB
|
||||
// * char array, e.g. GRB_CHAR_ATTR_VTYPE
|
||||
//
|
||||
// You set a scalar attribute with the methods SetXXXAttr, e.g.
|
||||
// std::unique_ptr<Gurobi> gurobi = Gurobi::New().value();
|
||||
// absl::Status s = gurobi->SetIntAttr(GRB_INT_ATTR_MODELSENSE, 1);
|
||||
// Note that not all attributes can be set; consult the Gurobi attribute docs.
|
||||
//
|
||||
// Attributes can also be read. However, attributes can be unavailable depending
|
||||
// on the context, e.g. the solution objective value is not available before
|
||||
// solving. You can determine when an attribute is available either from the
|
||||
// Gurobi documentation or by directly testing:
|
||||
// std::unique_ptr<Gurobi> gurobi = Gurobi::New().value();
|
||||
// bool is_avail = gurobi->IsAttrAvailable(GRB_DBL_ATTR_OBJVAL);
|
||||
// To read an attribute:
|
||||
// std::unique_ptr<Gurobi> gurobi = Gurobi::New().value();
|
||||
// absl::StatusOr<double> obj = gurobi->GetDoubleAttr(GRB_DBL_ATTR_OBJVAL);
|
||||
// (The method *should* succeed when IsAttrAvailable() is true and you have
|
||||
// specified the type of attribute correctly.)
|
||||
//
|
||||
// Array attributes are similar, but the API differs slightly. E.g. to set the
|
||||
// first three variable lower bounds to 1.0:
|
||||
// std::unique_ptr<Gurobi> gurobi = Gurobi::New().value();
|
||||
// absl::Status s = gurobi->SetDoubleAttrArray(GRB_DBL_ATTR_LB, {1, 1, 1});
|
||||
// You can also set specific indices, see SetDoubleAttrList. To read, use:
|
||||
// Gurobi* gurobi = ...;
|
||||
// int num_vars = ...;
|
||||
// absl::StatusOr<std::vector<double>> lbs =
|
||||
// gurobi->GetDoubleAttrArray(GRB_DBL_ATTR_LB, num_vars);
|
||||
// An overload to write the result into an absl::Span is also provided.
|
||||
//
|
||||
// WARNING: as with the Gurobi C API, attributes cannot be read immediately
|
||||
// after they have been set. You need to call UpdateModel() (which is called by
|
||||
// Optimize()) before reading the model back. E.g.
|
||||
// std::unique_ptr<Gurobi> gurobi = Gurobi::New().value();
|
||||
// CHECK_OK(gurobi->AddVars({1, 1}, {0, 0}, {1, 1},
|
||||
// {GRB_INTEGER, GRB_INTEGER}, {"x", "y"}));
|
||||
// int num_vars = gurobi->GetIntAttr(GRB_INT_ATTR_NUMVARS).value(); // Is 0.
|
||||
// CHECK_OK(gurobi->UpdateModel());
|
||||
// num_vars = gurobi->GetIntAttr(GRB_INT_ATTR_NUMVARS).value(); // Is now 2.
|
||||
// Calls to UpdateModel() are expensive and should be minimized.
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Parameters
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Parameters are associated directly with Gurobi rather than a GRBenv as in the
|
||||
// C API. Parameters have three types: int, double and string. You can get and
|
||||
// set them by their C API names, e.g.
|
||||
// std::unique_ptr<Gurobi> gurobi = Gurobi::New().value();
|
||||
// gurobi->SetIntParam(GRB_INT_PAR_LOGTOCONSOLE, 1);
|
||||
// gurobi->GetIntParam(GRB_INT_PAR_LOGTOCONSOLE); // Returns 1.
|
||||
// Unlike attributes, values can be read immediately, no call to UpdateModel()
|
||||
// is required.
|
||||
class Gurobi {
|
||||
public:
|
||||
// A sparse matrix in compressed sparse column (CSC) format. E.g.
|
||||
// [[2, 0, 4],
|
||||
// [8, 6, 0]]
|
||||
// Would be {.begins={0, 2, 3}, .inds={0, 1, 1, 0}, .vals={2, 8, 6, 4}}
|
||||
struct SparseMat {
|
||||
// Has size equal to the number of columns, the index in inds where this
|
||||
// column begins.
|
||||
std::vector<int> begins;
|
||||
|
||||
// Has size equal to the number of nonzeros in the matrix, the row for this
|
||||
// entry.
|
||||
std::vector<int> inds;
|
||||
|
||||
// Has size equal to the number of nonzeros in the matrix, the value for
|
||||
// this entry.
|
||||
std::vector<double> vals;
|
||||
};
|
||||
|
||||
// The argument of Gurobi callbacks, allows you to read callback specific
|
||||
// data and send information back to the solver.
|
||||
class CallbackContext {
|
||||
public:
|
||||
// For internal use only.
|
||||
CallbackContext(Gurobi* gurobi, void* cb_data, int where);
|
||||
|
||||
// The current event of the callback, see Callback Codes in Gurobi docs.
|
||||
int where() const { return where_; }
|
||||
Gurobi* gurobi() const { return gurobi_; }
|
||||
|
||||
// Calls GRBcbget() on "what" with result type int, see Callback Codes in
|
||||
// Gurobi docs for values of "what".
|
||||
absl::StatusOr<int> CbGetInt(int what) const;
|
||||
|
||||
// Calls GRBcbget() on "what" with result type double, see Callback Codes in
|
||||
// Gurobi docs for values of "what".
|
||||
absl::StatusOr<double> CbGetDouble(int what) const;
|
||||
|
||||
// Calls GRBcbget() on "what" with result type double*, see Callback Codes
|
||||
// in Gurobi docs for values of "what".
|
||||
//
|
||||
// The user is responsible for ensuring that result is large enough to hold
|
||||
// the result.
|
||||
absl::Status CbGetDoubleArray(int what, absl::Span<double> result) const;
|
||||
|
||||
// Calls GRBcbget() where what=MSG_STRING (call only at where=MESSAGE).
|
||||
absl::StatusOr<std::string> CbGetMessage() const;
|
||||
|
||||
// Calls GRBcbcut().
|
||||
absl::Status CbCut(absl::Span<const int> cutind,
|
||||
absl::Span<const double> cutval, char cutsense,
|
||||
double cutrhs) const;
|
||||
|
||||
// Calls GRBcblazy().
|
||||
absl::Status CbLazy(absl::Span<const int> lazyind,
|
||||
absl::Span<const double> lazyval, char lazysense,
|
||||
double lazyrhs) const;
|
||||
|
||||
// Calls GRBcbsolution().
|
||||
absl::StatusOr<double> CbSolution(absl::Span<const double> solution) const;
|
||||
|
||||
private:
|
||||
Gurobi* const gurobi_;
|
||||
void* const cb_data_;
|
||||
const int where_;
|
||||
};
|
||||
|
||||
// Invoked regularly by Gurobi while solving if provided as an argument to
|
||||
// Gurobi::Optimize(). If the user returns a status error in the callback:
|
||||
// * Termination of the solve is requested.
|
||||
// * The error is propagated to the return value of Gurobi::Optimize().
|
||||
// * The callback will not be invoked again.
|
||||
using Callback = std::function<absl::Status(const CallbackContext&)>;
|
||||
|
||||
// Creates a new Gurobi, taking ownership of master_env if provided (if no
|
||||
// environment is given, a new one is created internally from the license
|
||||
// file).
|
||||
static absl::StatusOr<std::unique_ptr<Gurobi>> New(
|
||||
GRBenvUniquePtr master_env = nullptr);
|
||||
|
||||
// Creates a new Gurobi using an existing GRBenv, where master_env cannot be
|
||||
// nullptr. Unlike Gurobi::New(), the returned Gurobi will not clean up the
|
||||
// master environment on destruction.
|
||||
//
|
||||
// A GurobiEnv can be shared between models with the following restrictions:
|
||||
// - Environments are not thread-safe (so use one thread or mutual exclusion
|
||||
// for Gurobi::New()).
|
||||
// - The master environment must outlive each Gurobi instance.
|
||||
// - Every "master" environment counts as a "use" of a Gurobi License.
|
||||
// Depending on your license type, you may need to share to run concurrent
|
||||
// solves in the same process.
|
||||
static absl::StatusOr<std::unique_ptr<Gurobi>> NewWithSharedMasterEnv(
|
||||
GRBenv* master_env);
|
||||
|
||||
~Gurobi();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Model Building
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Calls GRBaddvars() to add variables to the model.
|
||||
//
|
||||
// Requirements:
|
||||
// * lb, ub and vtype must have size equal to the number of new variables.
|
||||
// * obj should either:
|
||||
// - have size equal to the number of new variables,
|
||||
// - be empty (all new variables have objective coefficient 0).
|
||||
// * names should either:
|
||||
// - have size equal to the number of new variables,
|
||||
// - be empty (all new variables have name "").
|
||||
absl::Status AddVars(absl::Span<const double> obj,
|
||||
absl::Span<const double> lb, absl::Span<const double> ub,
|
||||
absl::Span<const char> vtype,
|
||||
absl::Span<const std::string> names);
|
||||
|
||||
// Calls GRBaddvars() to add variables and linear constraint columns to the
|
||||
// model.
|
||||
//
|
||||
// The new linear constraint matrix columns are given in CSC format (see
|
||||
// SparseMat above for an example).
|
||||
//
|
||||
// Requirements:
|
||||
// * lb, ub and vtype must have size equal to the number of new variables.
|
||||
// * obj should either:
|
||||
// - have size equal to the number of new variables,
|
||||
// - be empty (all new variables have objective coefficient 0).
|
||||
// * names should either:
|
||||
// - have size equal to the number of new variables,
|
||||
// - be empty (all new variables have name "").
|
||||
// * vbegin should have size equal to the number of new variables.
|
||||
// * vind and vsize should have size equal to the number of new nonzeros in
|
||||
// the linear constraint matrix.
|
||||
// Note: vbegin, vind and vval can all be empty if you do not want to modify
|
||||
// the constraint matrix, this is equivalent to the simpler overload above.
|
||||
absl::Status AddVars(absl::Span<const int> vbegin, absl::Span<const int> vind,
|
||||
absl::Span<const double> vval,
|
||||
absl::Span<const double> obj,
|
||||
absl::Span<const double> lb, absl::Span<const double> ub,
|
||||
absl::Span<const char> vtype,
|
||||
absl::Span<const std::string> names);
|
||||
|
||||
// Calls GRBdelvars().
|
||||
absl::Status DelVars(absl::Span<const int> ind);
|
||||
|
||||
// Calls GRBaddconstrs().
|
||||
//
|
||||
// Requirements:
|
||||
// * sense and rhs must have size equal to the number of new constraints.
|
||||
// * names should either:
|
||||
// - have size equal to the number of new constraints,
|
||||
// - be empty (all new constraints have name "").
|
||||
absl::Status AddConstrs(absl::Span<const char> sense,
|
||||
absl::Span<const double> rhs,
|
||||
absl::Span<const std::string> names);
|
||||
|
||||
// Calls GRBdelconstrs().
|
||||
absl::Status DelConstrs(absl::Span<const int> ind);
|
||||
|
||||
// Calls GRBchgcoeffs().
|
||||
//
|
||||
// Requirements:
|
||||
// * cind, vind, and val have size equal to the number of changed constraint
|
||||
// matrix entries.
|
||||
absl::Status ChgCoeffs(absl::Span<const int> cind, absl::Span<const int> vind,
|
||||
absl::Span<const double> val);
|
||||
|
||||
// Calls GRBaddqpterms().
|
||||
//
|
||||
// Requirements:
|
||||
// * qrow, qcol, and qval have size equal to the number of new quadratic
|
||||
// objective terms.
|
||||
absl::Status AddQpTerms(absl::Span<const int> qrow,
|
||||
absl::Span<const int> qcol,
|
||||
absl::Span<const double> qval);
|
||||
|
||||
// Calls GRBdelq().
|
||||
//
|
||||
// Deletes all quadratic objective coefficients.
|
||||
absl::Status DelQ();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Linear constraint matrix queries.
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Calls GRBgetvars().
|
||||
//
|
||||
// The number of nonzeros in the constraint matrix for the num_vars columns
|
||||
// starting with first_var.
|
||||
//
|
||||
// Warning: will not reflect pending modifications, call UpdateModel() or
|
||||
// Optimize() first.
|
||||
absl::StatusOr<int> GetNnz(int first_var, int num_vars);
|
||||
|
||||
// Calls GRBgetvars().
|
||||
//
|
||||
// Write the nonzeros of the constraint matrix for the num_vars columns
|
||||
// starting with first_var out in CSC format to (vbegin, vind, vval).
|
||||
//
|
||||
// The user is responsible for ensuring that the output Spans are exactly
|
||||
// the correct size. See the other GetVars() overload for a simpler version.
|
||||
//
|
||||
// Warning: will not reflect pending modifications, call UpdateModel() or
|
||||
// Optimize() first.
|
||||
absl::Status GetVars(absl::Span<int> vbegin, absl::Span<int> vind,
|
||||
absl::Span<double> vval, int first_var, int num_vars);
|
||||
|
||||
// Calls GRBgetvars().
|
||||
//
|
||||
// Returns the nonzeros of the constraint matrix for the num_vars columns
|
||||
// starting with first_var out in CSC format.
|
||||
//
|
||||
// Warning: will not reflect pending modifications, call UpdateModel() or
|
||||
// Optimize() first.
|
||||
absl::StatusOr<SparseMat> GetVars(int first_var, int num_vars);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Solving
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Calls GRBupdatemodel().
|
||||
absl::Status UpdateModel();
|
||||
|
||||
// Calls GRBoptimize().
|
||||
//
|
||||
// The callback, if specified, is set before solving and cleared after.
|
||||
absl::Status Optimize(Callback cb = nullptr);
|
||||
|
||||
// Calls GRBterminate().
|
||||
void Terminate();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Attributes
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool IsAttrAvailable(const char* name) const;
|
||||
|
||||
absl::StatusOr<int> GetIntAttr(const char* name) const;
|
||||
absl::Status SetIntAttr(const char* attr_name, int value);
|
||||
|
||||
absl::StatusOr<double> GetDoubleAttr(const char* name) const;
|
||||
absl::Status SetDoubleAttr(const char* attr_name, double value);
|
||||
|
||||
absl::StatusOr<std::string> GetStringAttr(const char* name) const;
|
||||
absl::Status SetStringAttr(const char* attr_name, const std::string& value);
|
||||
|
||||
absl::Status GetIntAttrArray(const char* name,
|
||||
absl::Span<int> attr_out) const;
|
||||
absl::StatusOr<std::vector<int>> GetIntAttrArray(const char* name,
|
||||
int len) const;
|
||||
absl::Status SetIntAttrArray(const char* name,
|
||||
absl::Span<const int> new_values);
|
||||
absl::Status SetIntAttrList(const char* name, absl::Span<const int> ind,
|
||||
absl::Span<const int> new_values);
|
||||
|
||||
absl::Status GetDoubleAttrArray(const char* name,
|
||||
absl::Span<double> attr_out) const;
|
||||
absl::StatusOr<std::vector<double>> GetDoubleAttrArray(const char* name,
|
||||
int len) const;
|
||||
absl::Status SetDoubleAttrArray(const char* name,
|
||||
absl::Span<const double> new_values);
|
||||
absl::Status SetDoubleAttrList(const char* name, absl::Span<const int> ind,
|
||||
absl::Span<const double> new_values);
|
||||
|
||||
absl::Status GetCharAttrArray(const char* name,
|
||||
absl::Span<char> attr_out) const;
|
||||
absl::StatusOr<std::vector<char>> GetCharAttrArray(const char* name,
|
||||
int len) const;
|
||||
absl::Status SetCharAttrArray(const char* name,
|
||||
absl::Span<const char> new_values);
|
||||
absl::Status SetCharAttrList(const char* name, absl::Span<const int> ind,
|
||||
absl::Span<const char> new_values);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// Parameters
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Calls GRBsetparam().
|
||||
//
|
||||
// Prefer the typed versions (e.g. SetIntParam()) defined below.
|
||||
absl::Status SetParam(const char* name, const std::string& value);
|
||||
|
||||
// Calls GRBsetintparam().
|
||||
absl::Status SetIntParam(const char* name, int value);
|
||||
|
||||
// Calls GRBsetdblparam().
|
||||
absl::Status SetDoubleParam(const char* name, double value);
|
||||
|
||||
// Calls GRBsetstrparam().
|
||||
absl::Status SetStringParam(const char* name, const std::string& value);
|
||||
|
||||
// Calls GRBgetintparam().
|
||||
absl::StatusOr<int> GetIntParam(const char* name);
|
||||
|
||||
// Calls GRBgetdblparam().
|
||||
absl::StatusOr<double> GetDoubleParam(const char* name);
|
||||
|
||||
// Calls GRBgetstrparam().
|
||||
absl::StatusOr<std::string> GetStringParam(const char* name);
|
||||
|
||||
// Calls GRBresetparams().
|
||||
absl::Status ResetParameters();
|
||||
|
||||
// Typically not needed.
|
||||
GRBmodel* model() const { return gurobi_model_; }
|
||||
|
||||
private:
|
||||
// optional_owned_master_env can be null, model and model_env cannot.
|
||||
Gurobi(GRBenvUniquePtr optional_owned_master_env, GRBmodel* model,
|
||||
GRBenv* model_env);
|
||||
// optional_owned_master_env can be null, master_env cannot.
|
||||
static absl::StatusOr<std::unique_ptr<Gurobi>> New(
|
||||
GRBenvUniquePtr optional_owned_master_env, GRBenv* master_env);
|
||||
|
||||
absl::Status ToStatus(
|
||||
int grb_err, absl::StatusCode code = absl::StatusCode::kInvalidArgument,
|
||||
absl::SourceLocation loc = absl::SourceLocation::current()) const;
|
||||
|
||||
const GRBenvUniquePtr owned_master_env_;
|
||||
// Invariant: Not null.
|
||||
GRBmodel* const gurobi_model_;
|
||||
// Invariant: Not null. This is the environment created by GRBnewmodel(), not
|
||||
// the master environment used to create a GRBmodel, see class documentation.
|
||||
GRBenv* const model_env_;
|
||||
};
|
||||
|
||||
} // namespace operations_research::math_opt
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_SOLVERS_GUROBI_G_GUROBI_H_
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -27,11 +28,11 @@
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "absl/types/optional.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/base/linked_hash_map.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/math_opt_proto_utils.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/core/sparse_vector_view.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
@@ -61,8 +62,6 @@ constexpr int CheckedGuroibWhere() {
|
||||
|
||||
inline int GurobiEvent(CallbackEventProto event) {
|
||||
switch (event) {
|
||||
case CALLBACK_EVENT_POLLING:
|
||||
return CheckedGuroibWhere<GRB_CB_POLLING>();
|
||||
case CALLBACK_EVENT_PRESOLVE:
|
||||
return CheckedGuroibWhere<GRB_CB_PRESOLVE>();
|
||||
case CALLBACK_EVENT_SIMPLEX:
|
||||
@@ -75,23 +74,12 @@ inline int GurobiEvent(CallbackEventProto event) {
|
||||
return CheckedGuroibWhere<GRB_CB_MIPNODE>();
|
||||
case CALLBACK_EVENT_BARRIER:
|
||||
return CheckedGuroibWhere<GRB_CB_BARRIER>();
|
||||
case CALLBACK_EVENT_MESSAGE:
|
||||
return CheckedGuroibWhere<GRB_CB_MESSAGE>();
|
||||
case CALLBACK_EVENT_UNSPECIFIED:
|
||||
default:
|
||||
LOG(FATAL) << "Unexpected callback event: " << event;
|
||||
}
|
||||
}
|
||||
|
||||
absl::Status GurobiStatus(GRBmodel* model, int error_code) {
|
||||
if (error_code == kGrbOk) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
GRBenv* const env = GRBgetenv(model);
|
||||
return absl::InternalError(
|
||||
absl::StrCat("Gurobi error ", error_code, ": ", GRBgeterrormsg(env)));
|
||||
}
|
||||
|
||||
SparseDoubleVectorProto ApplyFilter(
|
||||
const std::vector<double>& grb_solution,
|
||||
const gtl::linked_hash_map<int64_t, int>& var_ids,
|
||||
@@ -108,88 +96,29 @@ SparseDoubleVectorProto ApplyFilter(
|
||||
return result;
|
||||
}
|
||||
|
||||
class GurobiCallbackContext {
|
||||
public:
|
||||
GurobiCallbackContext(GRBmodel* model, void* cbdata, int where)
|
||||
: model_(model), cbdata_(cbdata), where_(where) {}
|
||||
|
||||
absl::StatusOr<int> get_int(int what) const {
|
||||
int result;
|
||||
RETURN_IF_ERROR(AsStatus(GRBcbget(cbdata_, where_, what, &result)));
|
||||
return result;
|
||||
absl::StatusOr<int64_t> CbGetInt64(const Gurobi::CallbackContext& context,
|
||||
int what) {
|
||||
ASSIGN_OR_RETURN(const double result, context.CbGetDouble(what));
|
||||
int64_t result64 = static_cast<int64_t>(result);
|
||||
if (result != static_cast<double>(result64)) {
|
||||
return absl::InternalError(
|
||||
absl::StrCat("Error converting double attribute: ", what,
|
||||
"with value: ", result, " to int64_t exactly."));
|
||||
}
|
||||
return result64;
|
||||
}
|
||||
|
||||
absl::StatusOr<double> get_double(int what) const {
|
||||
double result;
|
||||
RETURN_IF_ERROR(AsStatus(GRBcbget(cbdata_, where_, what, &result)));
|
||||
return result;
|
||||
absl::StatusOr<bool> CbGetBool(const Gurobi::CallbackContext& context,
|
||||
int what) {
|
||||
ASSIGN_OR_RETURN(const int result, context.CbGetInt(what));
|
||||
bool result_bool = static_cast<bool>(result);
|
||||
if (result != static_cast<int>(result_bool)) {
|
||||
return absl::InternalError(
|
||||
absl::StrCat("Error converting int attribute: ", what,
|
||||
"with value: ", result, " to bool exactly."));
|
||||
}
|
||||
|
||||
absl::StatusOr<int64_t> get_int64(int what) const {
|
||||
double result;
|
||||
RETURN_IF_ERROR(AsStatus(GRBcbget(cbdata_, where_, what, &result)));
|
||||
int64_t result64 = static_cast<int64_t>(result);
|
||||
if (result != static_cast<double>(result64)) {
|
||||
return absl::InternalError(
|
||||
absl::StrCat("Error converting double attribute: ", what,
|
||||
"with value: ", result, " to int64_t exactly."));
|
||||
}
|
||||
return result64;
|
||||
}
|
||||
|
||||
absl::StatusOr<bool> get_bool(int what) const {
|
||||
int result;
|
||||
RETURN_IF_ERROR(AsStatus(GRBcbget(cbdata_, where_, what, &result)));
|
||||
bool result_bool = static_cast<bool>(result);
|
||||
if (result != static_cast<int>(result_bool)) {
|
||||
return absl::InternalError(
|
||||
absl::StrCat("Error converting int attribute: ", what,
|
||||
"with value: ", result, " to bool exactly."));
|
||||
}
|
||||
return result_bool;
|
||||
}
|
||||
|
||||
absl::StatusOr<std::string> get_string(int what) const {
|
||||
char* result;
|
||||
RETURN_IF_ERROR(AsStatus(GRBcbget(cbdata_, where_, what, &result)));
|
||||
return result;
|
||||
}
|
||||
|
||||
// The output argument doubles_out will be modified, it is the callers
|
||||
// responsibility to ensure that it is large enough.
|
||||
absl::Status get_doubles(int what, absl::Span<double> doubles_out) const {
|
||||
double* const first = doubles_out.data();
|
||||
RETURN_IF_ERROR(AsStatus(GRBcbget(cbdata_, where_, what, first)));
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
GRBmodel* grb_model() const { return model_; }
|
||||
int where() const { return where_; }
|
||||
|
||||
absl::Status AddConstraint(absl::Span<const int> vars,
|
||||
absl::Span<const double> coefs, char sense,
|
||||
double rhs, bool is_lazy) const {
|
||||
auto cut_fn = is_lazy ? &GRBcblazy : &GRBcbcut;
|
||||
return AsStatus((*cut_fn)(cbdata_, vars.size(), vars.begin(), coefs.begin(),
|
||||
sense, rhs));
|
||||
}
|
||||
|
||||
absl::StatusOr<double> SuggestSolution(absl::Span<const double> coefs) const {
|
||||
double obj_value;
|
||||
RETURN_IF_ERROR(
|
||||
AsStatus(GRBcbsolution(cbdata_, coefs.begin(), &obj_value)));
|
||||
return obj_value;
|
||||
}
|
||||
|
||||
private:
|
||||
absl::Status AsStatus(int error_code) const {
|
||||
return GurobiStatus(model_, error_code);
|
||||
}
|
||||
|
||||
GRBmodel* const model_;
|
||||
void* const cbdata_;
|
||||
const int where_;
|
||||
};
|
||||
return result_bool;
|
||||
}
|
||||
|
||||
// Invokes setter on a non-error value in statusor or returns the error.
|
||||
//
|
||||
@@ -211,122 +140,104 @@ absl::Status SetRuntime(const GurobiCallbackInput& callback_input,
|
||||
|
||||
// Returns the data for the next callback. Returns nullopt if no callback is
|
||||
// needed.
|
||||
absl::StatusOr<absl::optional<CallbackDataProto>> CreateCallbackDataProto(
|
||||
const GurobiCallbackContext& c, const GurobiCallbackInput& callback_input,
|
||||
absl::StatusOr<std::optional<CallbackDataProto>> CreateCallbackDataProto(
|
||||
const Gurobi::CallbackContext& c, const GurobiCallbackInput& callback_input,
|
||||
MessageCallbackData& message_callback_data) {
|
||||
CallbackDataProto callback_data;
|
||||
|
||||
// Query information from Gurobi.
|
||||
switch (c.where()) {
|
||||
case GRB_CB_POLLING: {
|
||||
callback_data.set_event(CALLBACK_EVENT_POLLING);
|
||||
break;
|
||||
}
|
||||
case GRB_CB_PRESOLVE: {
|
||||
callback_data.set_event(CALLBACK_EVENT_PRESOLVE);
|
||||
CallbackDataProto::PresolveStats* const s =
|
||||
callback_data.mutable_presolve_stats();
|
||||
MO_SET_OR_RET(s->set_removed_variables, c.get_int(GRB_CB_PRE_COLDEL));
|
||||
MO_SET_OR_RET(s->set_removed_constraints, c.get_int(GRB_CB_PRE_ROWDEL));
|
||||
MO_SET_OR_RET(s->set_bound_changes, c.get_int(GRB_CB_PRE_BNDCHG));
|
||||
MO_SET_OR_RET(s->set_coefficient_changes, c.get_int(GRB_CB_PRE_COECHG));
|
||||
MO_SET_OR_RET(s->set_removed_variables, c.CbGetInt(GRB_CB_PRE_COLDEL));
|
||||
MO_SET_OR_RET(s->set_removed_constraints, c.CbGetInt(GRB_CB_PRE_ROWDEL));
|
||||
MO_SET_OR_RET(s->set_bound_changes, c.CbGetInt(GRB_CB_PRE_BNDCHG));
|
||||
MO_SET_OR_RET(s->set_coefficient_changes, c.CbGetInt(GRB_CB_PRE_COECHG));
|
||||
break;
|
||||
}
|
||||
case GRB_CB_SIMPLEX: {
|
||||
callback_data.set_event(CALLBACK_EVENT_SIMPLEX);
|
||||
CallbackDataProto::SimplexStats* const s =
|
||||
callback_data.mutable_simplex_stats();
|
||||
MO_SET_OR_RET(s->set_iteration_count, c.get_int64(GRB_CB_SPX_ITRCNT));
|
||||
MO_SET_OR_RET(s->set_is_pertubated, c.get_bool(GRB_CB_SPX_ISPERT));
|
||||
MO_SET_OR_RET(s->set_objective_value, c.get_double(GRB_CB_SPX_OBJVAL));
|
||||
MO_SET_OR_RET(s->set_iteration_count, CbGetInt64(c, GRB_CB_SPX_ITRCNT));
|
||||
MO_SET_OR_RET(s->set_is_pertubated, CbGetBool(c, GRB_CB_SPX_ISPERT));
|
||||
MO_SET_OR_RET(s->set_objective_value, c.CbGetDouble(GRB_CB_SPX_OBJVAL));
|
||||
MO_SET_OR_RET(s->set_primal_infeasibility,
|
||||
c.get_double(GRB_CB_SPX_PRIMINF));
|
||||
c.CbGetDouble(GRB_CB_SPX_PRIMINF));
|
||||
MO_SET_OR_RET(s->set_dual_infeasibility,
|
||||
c.get_double(GRB_CB_SPX_DUALINF));
|
||||
c.CbGetDouble(GRB_CB_SPX_DUALINF));
|
||||
break;
|
||||
}
|
||||
case GRB_CB_BARRIER: {
|
||||
callback_data.set_event(CALLBACK_EVENT_BARRIER);
|
||||
CallbackDataProto::BarrierStats* const s =
|
||||
callback_data.mutable_barrier_stats();
|
||||
MO_SET_OR_RET(s->set_iteration_count, c.get_int(GRB_CB_BARRIER_ITRCNT));
|
||||
MO_SET_OR_RET(s->set_iteration_count, c.CbGetInt(GRB_CB_BARRIER_ITRCNT));
|
||||
MO_SET_OR_RET(s->set_primal_objective,
|
||||
c.get_double(GRB_CB_BARRIER_PRIMOBJ));
|
||||
c.CbGetDouble(GRB_CB_BARRIER_PRIMOBJ));
|
||||
MO_SET_OR_RET(s->set_dual_objective,
|
||||
c.get_double(GRB_CB_BARRIER_DUALOBJ));
|
||||
c.CbGetDouble(GRB_CB_BARRIER_DUALOBJ));
|
||||
MO_SET_OR_RET(s->set_primal_infeasibility,
|
||||
c.get_double(GRB_CB_BARRIER_PRIMINF));
|
||||
c.CbGetDouble(GRB_CB_BARRIER_PRIMINF));
|
||||
MO_SET_OR_RET(s->set_dual_infeasibility,
|
||||
c.get_double(GRB_CB_BARRIER_DUALINF));
|
||||
MO_SET_OR_RET(s->set_complementarity, c.get_double(GRB_CB_BARRIER_COMPL));
|
||||
break;
|
||||
}
|
||||
case GRB_CB_MESSAGE: {
|
||||
const absl::StatusOr<std::string> msg = c.get_string(GRB_CB_MSG_STRING);
|
||||
RETURN_IF_ERROR(msg.status())
|
||||
<< "Error getting message string in callback";
|
||||
absl::optional<CallbackDataProto> message_data =
|
||||
message_callback_data.Parse(*msg);
|
||||
if (!message_data) {
|
||||
// We don't generate any callback when there is no message.
|
||||
return absl::nullopt;
|
||||
}
|
||||
callback_data = std::move(*message_data);
|
||||
c.CbGetDouble(GRB_CB_BARRIER_DUALINF));
|
||||
MO_SET_OR_RET(s->set_complementarity,
|
||||
c.CbGetDouble(GRB_CB_BARRIER_COMPL));
|
||||
break;
|
||||
}
|
||||
case GRB_CB_MIP: {
|
||||
callback_data.set_event(CALLBACK_EVENT_MIP);
|
||||
CallbackDataProto::MipStats* const s = callback_data.mutable_mip_stats();
|
||||
MO_SET_OR_RET(s->set_primal_bound, c.get_double(GRB_CB_MIP_OBJBST));
|
||||
MO_SET_OR_RET(s->set_dual_bound, c.get_double(GRB_CB_MIP_OBJBND));
|
||||
MO_SET_OR_RET(s->set_explored_nodes, c.get_int64(GRB_CB_MIP_NODCNT));
|
||||
MO_SET_OR_RET(s->set_open_nodes, c.get_int64(GRB_CB_MIP_NODLFT));
|
||||
MO_SET_OR_RET(s->set_simplex_iterations, c.get_int64(GRB_CB_MIP_ITRCNT));
|
||||
MO_SET_OR_RET(s->set_primal_bound, c.CbGetDouble(GRB_CB_MIP_OBJBST));
|
||||
MO_SET_OR_RET(s->set_dual_bound, c.CbGetDouble(GRB_CB_MIP_OBJBND));
|
||||
MO_SET_OR_RET(s->set_explored_nodes, CbGetInt64(c, GRB_CB_MIP_NODCNT));
|
||||
MO_SET_OR_RET(s->set_open_nodes, CbGetInt64(c, GRB_CB_MIP_NODLFT));
|
||||
MO_SET_OR_RET(s->set_simplex_iterations,
|
||||
CbGetInt64(c, GRB_CB_MIP_ITRCNT));
|
||||
MO_SET_OR_RET(s->set_number_of_solutions_found,
|
||||
c.get_int(GRB_CB_MIP_SOLCNT));
|
||||
MO_SET_OR_RET(s->set_cutting_planes_in_lp, c.get_int(GRB_CB_MIP_CUTCNT));
|
||||
c.CbGetInt(GRB_CB_MIP_SOLCNT));
|
||||
MO_SET_OR_RET(s->set_cutting_planes_in_lp, c.CbGetInt(GRB_CB_MIP_CUTCNT));
|
||||
|
||||
break;
|
||||
}
|
||||
case GRB_CB_MIPSOL: {
|
||||
callback_data.set_event(CALLBACK_EVENT_MIP_SOLUTION);
|
||||
CallbackDataProto::MipStats* const s = callback_data.mutable_mip_stats();
|
||||
MO_SET_OR_RET(s->set_primal_bound, c.get_double(GRB_CB_MIPSOL_OBJBST));
|
||||
MO_SET_OR_RET(s->set_dual_bound, c.get_double(GRB_CB_MIPSOL_OBJBND));
|
||||
MO_SET_OR_RET(s->set_explored_nodes, c.get_int64(GRB_CB_MIPSOL_NODCNT));
|
||||
MO_SET_OR_RET(s->set_primal_bound, c.CbGetDouble(GRB_CB_MIPSOL_OBJBST));
|
||||
MO_SET_OR_RET(s->set_dual_bound, c.CbGetDouble(GRB_CB_MIPSOL_OBJBND));
|
||||
MO_SET_OR_RET(s->set_explored_nodes, CbGetInt64(c, GRB_CB_MIPSOL_NODCNT));
|
||||
MO_SET_OR_RET(s->set_number_of_solutions_found,
|
||||
c.get_int(GRB_CB_MIPSOL_SOLCNT));
|
||||
c.CbGetInt(GRB_CB_MIPSOL_SOLCNT));
|
||||
std::vector<double> var_values(callback_input.num_gurobi_vars);
|
||||
RETURN_IF_ERROR(
|
||||
c.get_doubles(GRB_CB_MIPSOL_SOL, absl::MakeSpan(var_values)))
|
||||
c.CbGetDoubleArray(GRB_CB_MIPSOL_SOL, absl::MakeSpan(var_values)))
|
||||
<< "Error reading solution at event MIP_SOLUTION";
|
||||
PrimalSolutionProto* const solution =
|
||||
callback_data.mutable_primal_solution();
|
||||
*solution->mutable_variable_values() =
|
||||
*callback_data.mutable_primal_solution_vector() =
|
||||
ApplyFilter(var_values, callback_input.variable_ids,
|
||||
callback_input.mip_solution_filter);
|
||||
MO_SET_OR_RET(solution->set_objective_value,
|
||||
c.get_double(GRB_CB_MIPSOL_OBJ));
|
||||
break;
|
||||
}
|
||||
case GRB_CB_MIPNODE: {
|
||||
callback_data.set_event(CALLBACK_EVENT_MIP_NODE);
|
||||
CallbackDataProto::MipStats* const s = callback_data.mutable_mip_stats();
|
||||
MO_SET_OR_RET(s->set_primal_bound, c.get_double(GRB_CB_MIPNODE_OBJBST));
|
||||
MO_SET_OR_RET(s->set_dual_bound, c.get_double(GRB_CB_MIPNODE_OBJBND));
|
||||
MO_SET_OR_RET(s->set_explored_nodes, c.get_int64(GRB_CB_MIPNODE_NODCNT));
|
||||
MO_SET_OR_RET(s->set_primal_bound, c.CbGetDouble(GRB_CB_MIPNODE_OBJBST));
|
||||
MO_SET_OR_RET(s->set_dual_bound, c.CbGetDouble(GRB_CB_MIPNODE_OBJBND));
|
||||
MO_SET_OR_RET(s->set_explored_nodes,
|
||||
CbGetInt64(c, GRB_CB_MIPNODE_NODCNT));
|
||||
|
||||
MO_SET_OR_RET(s->set_number_of_solutions_found,
|
||||
c.get_int(GRB_CB_MIPNODE_SOLCNT));
|
||||
const absl::StatusOr<int> grb_status = c.get_int(GRB_CB_MIPNODE_STATUS);
|
||||
c.CbGetInt(GRB_CB_MIPNODE_SOLCNT));
|
||||
const absl::StatusOr<int> grb_status = c.CbGetInt(GRB_CB_MIPNODE_STATUS);
|
||||
RETURN_IF_ERROR(grb_status.status())
|
||||
<< "Error reading solution status at event MIP_NODE";
|
||||
if (*grb_status == GRB_OPTIMAL) {
|
||||
std::vector<double> var_values(callback_input.num_gurobi_vars);
|
||||
RETURN_IF_ERROR(
|
||||
c.get_doubles(GRB_CB_MIPNODE_REL, absl::MakeSpan(var_values)))
|
||||
c.CbGetDoubleArray(GRB_CB_MIPNODE_REL, absl::MakeSpan(var_values)))
|
||||
<< "Error reading solution at event MIP_NODE";
|
||||
*callback_data.mutable_primal_solution()->mutable_variable_values() =
|
||||
*callback_data.mutable_primal_solution_vector() =
|
||||
ApplyFilter(var_values, callback_input.variable_ids,
|
||||
callback_input.mip_node_filter);
|
||||
// Note: Gurobi does not offer an objective value for the LP relaxation.
|
||||
@@ -345,9 +256,10 @@ absl::StatusOr<absl::optional<CallbackDataProto>> CreateCallbackDataProto(
|
||||
|
||||
#undef MO_SET_OR_RET
|
||||
|
||||
absl::Status ApplyResult(const GurobiCallbackContext& context,
|
||||
absl::Status ApplyResult(const Gurobi::CallbackContext& context,
|
||||
const GurobiCallbackInput& callback_input,
|
||||
const CallbackResultProto& result) {
|
||||
const CallbackResultProto& result,
|
||||
SolveInterrupter& local_interrupter) {
|
||||
for (const CallbackResultProto::GeneratedLinearConstraint& cut :
|
||||
result.cuts()) {
|
||||
std::vector<int> gurobi_vars;
|
||||
@@ -367,24 +279,29 @@ absl::Status ApplyResult(const GurobiCallbackContext& context,
|
||||
}
|
||||
}
|
||||
for (const auto [sense, bound] : sense_bound_pairs) {
|
||||
RETURN_IF_ERROR(context.AddConstraint(gurobi_vars,
|
||||
cut.linear_expression().values(),
|
||||
sense, bound, cut.is_lazy()));
|
||||
if (cut.is_lazy()) {
|
||||
RETURN_IF_ERROR(context.CbLazy(
|
||||
gurobi_vars, cut.linear_expression().values(), sense, bound));
|
||||
} else {
|
||||
RETURN_IF_ERROR(context.CbCut(
|
||||
gurobi_vars, cut.linear_expression().values(), sense, bound));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const PrimalSolutionProto& solution : result.suggested_solution()) {
|
||||
for (const SparseDoubleVectorProto& solution_vector :
|
||||
result.suggested_solutions()) {
|
||||
// TODO(b/175829773): we cannot fill in auxiliary variables from range
|
||||
// constraints.
|
||||
std::vector<double> gurobi_var_values(callback_input.num_gurobi_vars,
|
||||
GRB_UNDEFINED);
|
||||
for (const auto [id, value] : MakeView(solution.variable_values())) {
|
||||
for (const auto [id, value] : MakeView(solution_vector)) {
|
||||
gurobi_var_values[callback_input.variable_ids.at(id)] = value;
|
||||
}
|
||||
RETURN_IF_ERROR(context.SuggestSolution(gurobi_var_values).status());
|
||||
RETURN_IF_ERROR(context.CbSolution(gurobi_var_values).status());
|
||||
}
|
||||
|
||||
if (result.terminate()) {
|
||||
GRBterminate(context.grb_model());
|
||||
local_interrupter.Interrupt();
|
||||
return absl::OkStatus();
|
||||
}
|
||||
return absl::OkStatus();
|
||||
@@ -401,44 +318,80 @@ std::vector<bool> EventToGurobiWhere(
|
||||
return result;
|
||||
}
|
||||
|
||||
absl::Status GurobiCallbackImpl(GRBmodel* grb_model, void* cbdata, int where,
|
||||
absl::Status GurobiCallbackImpl(const Gurobi::CallbackContext& context,
|
||||
const GurobiCallbackInput& callback_input,
|
||||
MessageCallbackData& message_callback_data) {
|
||||
if (callback_input.user_cb == nullptr || !callback_input.events[where]) {
|
||||
MessageCallbackData& message_callback_data,
|
||||
SolveInterrupter* const local_interrupter) {
|
||||
// Gurobi 9 ignores early calls to GRBterminate(). For example calling
|
||||
// GRBterminate() in the first call of a MESSAGE callback only will not
|
||||
// interrupt the solve. The rationale is that it is likely Gurobi resets its
|
||||
// own internal "terminated" flag at the beginning of the solve but do make
|
||||
// some callbacks calls first.
|
||||
//
|
||||
// Hence here we make sure to call GRBterminate() for every event once the
|
||||
// interrupter has been triggered. This in particular includes POLLING which
|
||||
// is regularly emitted by Gurobi during the solve.
|
||||
if (local_interrupter != nullptr && local_interrupter->IsInterrupted()) {
|
||||
context.gurobi()->Terminate();
|
||||
}
|
||||
|
||||
// The POLLING event is a way for interactive applications that uses Gurobi
|
||||
// but don't want to deal with threading to regain some kind of interactivity
|
||||
// while a long solve is running by being called back from time to time. No
|
||||
// data can be retrieved from this event. This event if thus not wrapped by
|
||||
// MathOpt.
|
||||
if (context.where() == GRB_CB_POLLING) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
const GurobiCallbackContext cb_context(grb_model, cbdata, where);
|
||||
ASSIGN_OR_RETURN(const absl::optional<CallbackDataProto> callback_data,
|
||||
CreateCallbackDataProto(cb_context, callback_input,
|
||||
message_callback_data));
|
||||
if (context.where() == GRB_CB_MESSAGE) {
|
||||
if (callback_input.message_cb) {
|
||||
const absl::StatusOr<std::string> msg = context.CbGetMessage();
|
||||
RETURN_IF_ERROR(msg.status())
|
||||
<< "Error getting message string in callback";
|
||||
const std::vector<std::string> lines = message_callback_data.Parse(*msg);
|
||||
if (!lines.empty()) {
|
||||
callback_input.message_cb(lines);
|
||||
}
|
||||
}
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
if (callback_input.user_cb == nullptr ||
|
||||
!callback_input.events[context.where()]) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
// At this point we know we have a user callback, thus we must have a local
|
||||
// interrupter to deal with termination.
|
||||
CHECK(local_interrupter != nullptr);
|
||||
|
||||
ASSIGN_OR_RETURN(
|
||||
const std::optional<CallbackDataProto> callback_data,
|
||||
CreateCallbackDataProto(context, callback_input, message_callback_data));
|
||||
if (!callback_data) {
|
||||
return absl::OkStatus();
|
||||
}
|
||||
const absl::StatusOr<CallbackResultProto> result =
|
||||
callback_input.user_cb(*callback_data);
|
||||
if (!result.ok()) {
|
||||
GRBterminate(grb_model);
|
||||
local_interrupter->Interrupt();
|
||||
return result.status();
|
||||
}
|
||||
RETURN_IF_ERROR(ApplyResult(cb_context, callback_input, *result));
|
||||
RETURN_IF_ERROR(
|
||||
ApplyResult(context, callback_input, *result, *local_interrupter));
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
absl::Status GurobiCallbackImplFlush(
|
||||
const GurobiCallbackInput& callback_input,
|
||||
MessageCallbackData& message_callback_data) {
|
||||
absl::optional<CallbackDataProto> callback_data =
|
||||
message_callback_data.Flush();
|
||||
if (!callback_data) {
|
||||
return absl::OkStatus();
|
||||
void GurobiCallbackImplFlush(const GurobiCallbackInput& callback_input,
|
||||
MessageCallbackData& message_callback_data) {
|
||||
const std::vector<std::string> lines = message_callback_data.Flush();
|
||||
if (lines.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RETURN_IF_ERROR(SetRuntime(callback_input, *callback_data))
|
||||
<< "Error encoding runtime when flushing the remaining callbacks";
|
||||
|
||||
// No need to terminate here, we are already done. On top of that we are after
|
||||
// the solve, so nothing in the CallbackResultProto matters.
|
||||
return callback_input.user_cb(*callback_data).status();
|
||||
// Here we know that message_callback_data has only been filled-in if
|
||||
// message_cb was not nullptr. Hence it is safe to make this call without
|
||||
// testing.
|
||||
callback_input.message_cb(lines);
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
#include "absl/time/time.h"
|
||||
#include "ortools/base/linked_hash_map.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/solvers/gurobi/g_gurobi.h"
|
||||
#include "ortools/math_opt/solvers/message_callback_data.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
|
||||
@@ -33,6 +35,7 @@ namespace math_opt {
|
||||
|
||||
struct GurobiCallbackInput {
|
||||
SolverInterface::Callback user_cb;
|
||||
SolverInterface::MessageCallback message_cb;
|
||||
const gtl::linked_hash_map<int64_t, int>& variable_ids;
|
||||
int num_gurobi_vars = 0;
|
||||
// events[i] indicates if we should run user_cb when Gurobi's callback is
|
||||
@@ -54,16 +57,17 @@ struct GurobiCallbackInput {
|
||||
std::vector<bool> EventToGurobiWhere(
|
||||
const absl::flat_hash_set<CallbackEventProto>& events);
|
||||
|
||||
absl::Status GurobiCallbackImpl(GRBmodel* grb_model, void* cbdata, int where,
|
||||
absl::Status GurobiCallbackImpl(const Gurobi::CallbackContext& context,
|
||||
const GurobiCallbackInput& callback_input,
|
||||
MessageCallbackData& message_callback_data);
|
||||
MessageCallbackData& message_callback_data,
|
||||
SolveInterrupter* local_interrupter);
|
||||
|
||||
// Makes the final calls to the user callback with any buffered event if
|
||||
// necessary. It must be called once at the end of the solve, and only if all
|
||||
// previous callbacks succeeded (and the solve succeeded).
|
||||
absl::Status GurobiCallbackImplFlush(
|
||||
const GurobiCallbackInput& callback_input,
|
||||
MessageCallbackData& message_callback_data);
|
||||
// Makes the final calls to the message callback with any unfinished line if
|
||||
// necessary. It must be called once at the end of the solve, even when the
|
||||
// solve or one callback failed (in case the last unfinished line contains some
|
||||
// details about that).
|
||||
void GurobiCallbackImplFlush(const GurobiCallbackInput& callback_input,
|
||||
MessageCallbackData& message_callback_data);
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
38
ortools/math_opt/solvers/gurobi_init_arguments.cc
Normal file
38
ortools/math_opt/solvers/gurobi_init_arguments.cc
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "ortools/math_opt/solvers/gurobi_init_arguments.h"
|
||||
|
||||
#include <optional>
|
||||
#include "absl/status/statusor.h"
|
||||
#include "ortools/math_opt/solvers/gurobi.pb.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
absl::StatusOr<GRBenvUniquePtr> NewMasterEnvironment(
|
||||
std::optional<GurobiInitializerProto::ISVKey> proto_isv_key) {
|
||||
std::optional<GurobiIsvKey> isv_key;
|
||||
if (proto_isv_key.has_value()) {
|
||||
GurobiIsvKey key;
|
||||
key.name = proto_isv_key->name();
|
||||
key.application_name = proto_isv_key->application_name();
|
||||
key.expiration = proto_isv_key->expiration();
|
||||
key.key = proto_isv_key->key();
|
||||
isv_key = key;
|
||||
}
|
||||
return GurobiNewMasterEnv(isv_key);
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
117
ortools/math_opt/solvers/gurobi_init_arguments.h
Normal file
117
ortools/math_opt/solvers/gurobi_init_arguments.h
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright 2010-2021 Google LLC
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef OR_TOOLS_MATH_OPT_SOLVERS_GUROBI_INIT_ARGUMENTS_H_
|
||||
#define OR_TOOLS_MATH_OPT_SOLVERS_GUROBI_INIT_ARGUMENTS_H_
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "absl/status/statusor.h"
|
||||
#include "ortools/math_opt/core/non_streamable_solver_init_arguments.h"
|
||||
#include "ortools/math_opt/parameters.pb.h"
|
||||
#include "ortools/math_opt/solvers/gurobi.pb.h"
|
||||
#include "ortools/math_opt/solvers/gurobi/g_gurobi.h"
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
// Returns a new master environment.
|
||||
//
|
||||
// The typical use of this function is to share the same environment between
|
||||
// multiple solver instances. This is necessary when a single-use license is
|
||||
// used since only one master environment can exists in that case.
|
||||
//
|
||||
// A single master environment is not thread-safe and thus it should only be
|
||||
// used in a single thread. Even if the user has a license that authorizes
|
||||
// multiple master environments, Gurobi still recommends to use only one and to
|
||||
// share it as it is more efficient (see GRBloadenv() documentation).
|
||||
//
|
||||
// Of course, if the user wants to run multiple solves in parallel and has a
|
||||
// license that authorizes that, one environment should be used per thread.
|
||||
//
|
||||
// The master environment can be passed to MathOpt via the
|
||||
// NonStreamableGurobiInitArguments structure and its master_env field.
|
||||
//
|
||||
// The optional ISV key can be used to build the environment from an ISV key
|
||||
// instead of using the default license file. See
|
||||
// http://www.gurobi.com/products/licensing-pricing/isv-program for details.
|
||||
//
|
||||
// Example with default license file:
|
||||
//
|
||||
// // Solving two models on the same thread, sharing the same master
|
||||
// // environment.
|
||||
// Model model_1;
|
||||
// Model model_2;
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// ASSIGN_OR_RETURN(const GRBenvUniquePtr master_env,
|
||||
// NewMasterEnvironment());
|
||||
//
|
||||
// NonStreamableGurobiInitArguments gurobi_args;
|
||||
// gurobi_args.master_env = master_env.get();
|
||||
//
|
||||
// ASSIGN_OR_RETURN(
|
||||
// const std::unique_ptr<IncrementalSolver> incremental_solve_1,
|
||||
// IncrementalSolver::New(model, SOLVER_TYPE_GUROBI,
|
||||
// SolverInitArguments(gurobi_args)));
|
||||
// ASSIGN_OR_RETURN(
|
||||
// const std::unique_ptr<IncrementalSolver> incremental_solve_2,
|
||||
// IncrementalSolver::New(model, SOLVER_TYPE_GUROBI,
|
||||
// SolverInitArguments(gurobi_args)));
|
||||
//
|
||||
// ASSIGN_OR_RETURN(const SolveResult result_1, incremental_solve_1->Solve());
|
||||
// ASSIGN_OR_RETURN(const SolveResult result_2, incremental_solve_2->Solve());
|
||||
//
|
||||
//
|
||||
// With ISV key:
|
||||
//
|
||||
// ASSIGN_OR_RETURN(const GRBenvUniquePtr master_env,
|
||||
// NewMasterEnvironment(GurobiISVKey{
|
||||
// .name = "the name",
|
||||
// .application_name = "the application",
|
||||
// .expiration = 0,
|
||||
// .key = "...",
|
||||
// }.Proto()));
|
||||
//
|
||||
absl::StatusOr<GRBenvUniquePtr> NewMasterEnvironment(
|
||||
std::optional<GurobiInitializerProto::ISVKey> proto_isv_key = {});
|
||||
|
||||
// Non-streamable Gurobi specific parameters for solver instantiation.
|
||||
//
|
||||
// See NonStreamableSolverInitArguments for details.
|
||||
struct NonStreamableGurobiInitArguments
|
||||
: public NonStreamableSolverInitArgumentsHelper<
|
||||
NonStreamableGurobiInitArguments, SOLVER_TYPE_GUROBI> {
|
||||
// Master environment to use. This is only useful to pass when either the
|
||||
// default master environment created by the solver implementation is not
|
||||
// enough or when multiple Gurobi solvers are used with a single-use
|
||||
// license. In the latter case, only one master environment can be created so
|
||||
// it must be shared.
|
||||
//
|
||||
// The solver does not take ownership of the environment, it is the
|
||||
// responsibility of the caller to properly dispose of it after all solvers
|
||||
// that used it have been destroyed.
|
||||
GRBenv* master_env = nullptr;
|
||||
|
||||
const NonStreamableGurobiInitArguments* ToNonStreamableGurobiInitArguments()
|
||||
const override {
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace math_opt
|
||||
} // namespace operations_research
|
||||
|
||||
#endif // OR_TOOLS_MATH_OPT_SOLVERS_GUROBI_INIT_ARGUMENTS_H_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,16 +17,20 @@
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "ortools/base/logging.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/time/time.h"
|
||||
#include "absl/types/span.h"
|
||||
#include "ortools/base/linked_hash_map.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include "ortools/math_opt/core/solve_interrupter.h"
|
||||
#include "ortools/math_opt/core/solver_interface.h"
|
||||
#include "ortools/math_opt/model.pb.h"
|
||||
#include "ortools/math_opt/model_parameters.pb.h"
|
||||
@@ -34,6 +38,7 @@
|
||||
#include "ortools/math_opt/parameters.pb.h"
|
||||
#include "ortools/math_opt/result.pb.h"
|
||||
#include "ortools/math_opt/solution.pb.h"
|
||||
#include "ortools/math_opt/solvers/gurobi/g_gurobi.h"
|
||||
#include "ortools/math_opt/solvers/gurobi_callback.h"
|
||||
#include "ortools/math_opt/solvers/message_callback_data.h"
|
||||
#include "ortools/math_opt/sparse_containers.pb.h"
|
||||
@@ -46,28 +51,42 @@ namespace math_opt {
|
||||
class GurobiSolver : public SolverInterface {
|
||||
public:
|
||||
static absl::StatusOr<std::unique_ptr<GurobiSolver>> New(
|
||||
const ModelProto& input_model, const SolverInitializerProto& initializer);
|
||||
|
||||
~GurobiSolver() override;
|
||||
const ModelProto& input_model,
|
||||
const SolverInterface::InitArgs& init_args);
|
||||
|
||||
absl::StatusOr<SolveResultProto> Solve(
|
||||
const SolveParametersProto& parameters,
|
||||
const ModelSolveParametersProto& model_parameters,
|
||||
const CallbackRegistrationProto& callback_registration,
|
||||
Callback cb) override;
|
||||
MessageCallback message_cb,
|
||||
const CallbackRegistrationProto& callback_registration, Callback cb,
|
||||
SolveInterrupter* interrupter) override;
|
||||
absl::Status Update(const ModelUpdateProto& model_update) override;
|
||||
bool CanUpdate(const ModelUpdateProto& model_update) override;
|
||||
|
||||
private:
|
||||
struct GurobiCallbackData {
|
||||
explicit GurobiCallbackData(GurobiCallbackInput callback_input)
|
||||
: callback_input(std::move(callback_input)) {}
|
||||
explicit GurobiCallbackData(GurobiCallbackInput callback_input,
|
||||
SolveInterrupter* const local_interrupter)
|
||||
: callback_input(std::move(callback_input)),
|
||||
local_interrupter(local_interrupter) {}
|
||||
const GurobiCallbackInput callback_input;
|
||||
|
||||
// Interrupter triggered when either the user interrupter passed to Solve()
|
||||
// is triggered or after one user callback returned a true `terminate`.
|
||||
//
|
||||
// This is not the user interrupter though so it safe for callbacks to
|
||||
// trigger it.
|
||||
//
|
||||
// It is optional; it is not null when either we have a LP/MIP callback or a
|
||||
// user interrupter. But it can be null if we only have a message callback.
|
||||
SolveInterrupter* const local_interrupter;
|
||||
|
||||
MessageCallbackData message_callback_data;
|
||||
|
||||
absl::Status status = absl::OkStatus();
|
||||
};
|
||||
|
||||
GurobiSolver() = default;
|
||||
explicit GurobiSolver(std::unique_ptr<Gurobi> g_gurobi);
|
||||
|
||||
// For easing reading the code, we declare these types:
|
||||
using VariableId = int64_t;
|
||||
@@ -100,19 +119,61 @@ class GurobiSolver : public SolverInterface {
|
||||
: id(input_id), constraint_data(input_constraint) {}
|
||||
};
|
||||
|
||||
using IdHashMap = gtl::linked_hash_map<int64_t, int>;
|
||||
using ConstraintMap = gtl::linked_hash_map<int64_t, ConstraintData>;
|
||||
struct SolutionClaims {
|
||||
bool primal_feasible_solution_exists;
|
||||
bool dual_feasible_solution_exists;
|
||||
};
|
||||
|
||||
// Returns a termination reason and a detailed explanation string.
|
||||
static absl::StatusOr<
|
||||
std::pair<SolveResultProto::TerminationReason, std::string>>
|
||||
ConvertTerminationReason(int gurobi_status, bool has_feasible_solution);
|
||||
absl::Status ExtractSolveResultProto(
|
||||
bool is_maximize, SolveResultProto& result,
|
||||
struct SolutionsAndClaims {
|
||||
std::vector<SolutionProto> solutions;
|
||||
SolutionClaims solution_claims;
|
||||
};
|
||||
|
||||
template <typename SolutionType>
|
||||
struct SolutionAndClaim {
|
||||
std::optional<SolutionType> solution;
|
||||
bool feasible_solution_exists = false;
|
||||
};
|
||||
|
||||
using IdHashMap = gtl::linked_hash_map<int64_t, int>;
|
||||
|
||||
absl::StatusOr<ProblemStatusProto> GetProblemStatus(
|
||||
const int grb_termination, const SolutionClaims solution_claims);
|
||||
absl::StatusOr<SolveResultProto> ExtractSolveResultProto(
|
||||
absl::Time start, const ModelSolveParametersProto& model_parameters);
|
||||
absl::Status FillRays(const ModelSolveParametersProto& model_parameters,
|
||||
SolveResultProto& result);
|
||||
absl::StatusOr<GurobiSolver::SolutionsAndClaims> GetSolutions(
|
||||
const ModelSolveParametersProto& model_parameters);
|
||||
absl::Status ResetParameters();
|
||||
absl::Status SetParameter(const std::string& param_name,
|
||||
const std::string& param_value);
|
||||
absl::StatusOr<SolveStatsProto> GetSolveStats(absl::Time start,
|
||||
SolutionClaims solution_claims);
|
||||
|
||||
absl::StatusOr<double> GetBestDualBound();
|
||||
absl::StatusOr<double> GetBestPrimalBound(bool has_primal_feasible_solution);
|
||||
bool PrimalSolutionQualityAvailable() const;
|
||||
absl::StatusOr<double> GetPrimalSolutionQuality() const;
|
||||
|
||||
// Warning: is read from gurobi, take care with gurobi update.
|
||||
absl::StatusOr<bool> IsMaximize() const;
|
||||
|
||||
static absl::StatusOr<TerminationProto> ConvertTerminationReason(
|
||||
int gurobi_status, SolutionClaims solution_claims);
|
||||
|
||||
absl::StatusOr<SolutionsAndClaims> GetQpSolution(
|
||||
const ModelSolveParametersProto& model_parameters);
|
||||
absl::StatusOr<SolutionsAndClaims> GetLpSolution(
|
||||
const ModelSolveParametersProto& model_parameters);
|
||||
absl::StatusOr<SolutionsAndClaims> GetMipSolutions(
|
||||
const ModelSolveParametersProto& model_parameters);
|
||||
|
||||
// return bool field should be true if a primal solution exists.
|
||||
absl::StatusOr<SolutionAndClaim<PrimalSolutionProto>>
|
||||
GetConvexPrimalSolutionIfAvailable(
|
||||
const ModelSolveParametersProto& model_parameters);
|
||||
absl::StatusOr<SolutionAndClaim<DualSolutionProto>>
|
||||
GetLpDualSolutionIfAvailable(
|
||||
const ModelSolveParametersProto& model_parameters);
|
||||
absl::StatusOr<std::optional<BasisProto>> GetBasisIfAvailable();
|
||||
|
||||
// Returns a list of errors for failures only (and the empty list when all
|
||||
// parameters succeed).
|
||||
@@ -122,28 +183,26 @@ class GurobiSolver : public SolverInterface {
|
||||
absl::Status AddNewVariables(const VariablesProto& new_variables);
|
||||
absl::Status AddNewSlacks(const std::vector<SlackInfo>& new_slacks);
|
||||
absl::Status ChangeCoefficients(const SparseDoubleMatrixProto& matrix);
|
||||
absl::Status GurobiCodeToUtilStatus(int error_code, const char* source_file,
|
||||
int source_line,
|
||||
const char* statement) const;
|
||||
absl::Status LoadEnvironment();
|
||||
// NOTE: Clears any existing quadratic objective terms.
|
||||
absl::Status ResetQuadraticObjectiveTerms(
|
||||
const SparseDoubleMatrixProto& terms);
|
||||
// Updates objective so that it is the sum of everything in terms, plus all
|
||||
// other terms prexisting in the objective that are not overwritten by terms.
|
||||
absl::Status UpdateQuadraticObjectiveTerms(
|
||||
const SparseDoubleMatrixProto& terms);
|
||||
absl::Status LoadModel(const ModelProto& input_model);
|
||||
std::string GurobiErrorMessage(int error_code) const;
|
||||
std::string LogGurobiCode(int error_code, const char* source_file,
|
||||
int source_line, const char* statement,
|
||||
absl::string_view extra_message) const;
|
||||
|
||||
absl::Status UpdateDoubleListAttribute(const SparseDoubleVectorProto& update,
|
||||
const char* attribute_name,
|
||||
const IdHashMap& id_hash_map);
|
||||
absl::Status UpdateInt32ListAttribute(const SparseInt32VectorProto& update,
|
||||
const char* attribute_name,
|
||||
const IdHashMap& id_hash_map);
|
||||
absl::Status UpdateGurobiIndices();
|
||||
absl::Status UpdateLinearConstraints(
|
||||
const LinearConstraintUpdatesProto& update,
|
||||
std::vector<GurobiVariableIndex>& deleted_variables_index);
|
||||
absl::StatusOr<int> GetIntAttr(const char* name) const;
|
||||
absl::StatusOr<double> GetDoubleAttr(const char* name) const;
|
||||
absl::Status GetIntAttrArray(const char* name,
|
||||
absl::Span<int> attr_out) const;
|
||||
absl::Status GetDoubleAttrArray(const char* name,
|
||||
absl::Span<double> attr_out) const;
|
||||
|
||||
int num_gurobi_constraints() const;
|
||||
int get_model_index(GurobiVariableIndex index) const { return index; }
|
||||
int get_model_index(const ConstraintData& index) const {
|
||||
@@ -165,32 +224,15 @@ class GurobiSolver : public SolverInterface {
|
||||
const SparseVectorFilterProto& linear_constraints_filter,
|
||||
const SparseVectorFilterProto& variables_filter, bool is_maximize);
|
||||
absl::StatusOr<bool> IsLP() const;
|
||||
absl::StatusOr<bool> IsQP() const;
|
||||
|
||||
absl::StatusOr<std::unique_ptr<GurobiCallbackData>> RegisterCallback(
|
||||
const CallbackRegistrationProto& registration, Callback cb,
|
||||
absl::Time start);
|
||||
static int GurobiCallback(GRBmodel* model, void* cbdata, int where,
|
||||
void* usrdata);
|
||||
const MessageCallback message_cb, absl::Time start,
|
||||
SolveInterrupter* interrupter);
|
||||
|
||||
const std::unique_ptr<Gurobi> gurobi_;
|
||||
|
||||
// Note: Gurobi environments CAN be shared across several models, however
|
||||
// there are some caveats:
|
||||
// - Environments are not thread-safe.
|
||||
// - Once a gurobi_model_ is created, it makes an internal copy of the
|
||||
// "master" environment, so, later changes to that environment will not
|
||||
// be reflected in the gurobi_model_, for that reason we also keep
|
||||
// around a pointer to the gurobi_model_ environment in the
|
||||
// `active_env_` (which should not be freed).
|
||||
// - Every "master" environment counts as a "use" of a Gurobi License.
|
||||
// This means that if you have a limited usage count of licenses, this
|
||||
// implementation will be consuming more licenses. On the other hand, if
|
||||
// you have a machine license, a site license, or an academic license,
|
||||
// this disadvantage goes away.
|
||||
//
|
||||
// TODO(user) implement a sharing master Gurobi environment mode.
|
||||
// This would be akin to the `default environment` of Gurobi in python.
|
||||
GRBenv* master_env_ = nullptr;
|
||||
GRBenv* active_env_ = nullptr;
|
||||
GRBmodel* gurobi_model_ = nullptr;
|
||||
// Note that we use linked_hash_map because the index of the gurobi_model_
|
||||
// variables/constraints is exactly the order in which they are added to the
|
||||
// model.
|
||||
@@ -225,6 +267,13 @@ class GurobiSolver : public SolverInterface {
|
||||
// variables and constraints that need deletion. Finally flush changes at
|
||||
// the gurobi model level (if any deletion was performed).
|
||||
int num_gurobi_variables_ = 0;
|
||||
// Gurobi does not expose a way to query quadratic objective terms from the
|
||||
// model, so we track them. Notes:
|
||||
// * Keys are in upper triangular order (.first <= .second)
|
||||
// * Terms not in the map have zero coefficients
|
||||
// Note also that the map may also have entries with zero coefficient value.
|
||||
absl::flat_hash_map<std::pair<VariableId, VariableId>, double>
|
||||
quadratic_objective_coefficients_;
|
||||
|
||||
static constexpr int kGrbBasicConstraint = 0;
|
||||
static constexpr int kGrbNonBasicConstraint = -1;
|
||||
|
||||
@@ -14,32 +14,31 @@
|
||||
#include "ortools/math_opt/solvers/message_callback_data.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/types/optional.h"
|
||||
#include "ortools/math_opt/callback.pb.h"
|
||||
#include <vector>
|
||||
|
||||
namespace operations_research {
|
||||
namespace math_opt {
|
||||
|
||||
absl::optional<CallbackDataProto> MessageCallbackData::Parse(
|
||||
const absl::string_view message) {
|
||||
CallbackDataProto data;
|
||||
std::vector<std::string> MessageCallbackData::Parse(
|
||||
const std::string_view message) {
|
||||
std::vector<std::string> strings;
|
||||
|
||||
// Iterate on all complete lines (lines ending with a '\n').
|
||||
absl::string_view remainder = message;
|
||||
std::string_view remainder = message;
|
||||
for (std::size_t end = 0; end = remainder.find('\n'), end != remainder.npos;
|
||||
remainder = remainder.substr(end + 1)) {
|
||||
const auto line = remainder.substr(0, end);
|
||||
if (!unfinished_line_.empty()) {
|
||||
std::string& new_message = *data.add_messages();
|
||||
new_message = std::move(unfinished_line_);
|
||||
std::string new_message = std::move(unfinished_line_);
|
||||
unfinished_line_.clear();
|
||||
new_message += line;
|
||||
strings.push_back(std::move(new_message));
|
||||
} else {
|
||||
data.add_messages(std::string(line));
|
||||
strings.emplace_back(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,27 +47,17 @@ absl::optional<CallbackDataProto> MessageCallbackData::Parse(
|
||||
// contain '\n'.
|
||||
unfinished_line_ += remainder;
|
||||
|
||||
// It is an error to call the user callback without any message.
|
||||
if (data.messages().empty()) {
|
||||
return absl::nullopt;
|
||||
}
|
||||
|
||||
// We only need to set that if we have messages.
|
||||
data.set_event(CALLBACK_EVENT_MESSAGE);
|
||||
|
||||
return data;
|
||||
return strings;
|
||||
}
|
||||
|
||||
absl::optional<CallbackDataProto> MessageCallbackData::Flush() {
|
||||
std::vector<std::string> MessageCallbackData::Flush() {
|
||||
if (unfinished_line_.empty()) {
|
||||
return absl::nullopt;
|
||||
return {};
|
||||
}
|
||||
|
||||
CallbackDataProto data;
|
||||
data.set_event(CALLBACK_EVENT_MESSAGE);
|
||||
*data.add_messages() = std::move(unfinished_line_);
|
||||
std::vector<std::string> strings = {std::move(unfinished_line_)};
|
||||
unfinished_line_.clear();
|
||||
return data;
|
||||
return strings;
|
||||
}
|
||||
|
||||
} // namespace math_opt
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user