Bump math_opt

This commit is contained in:
Corentin Le Molgat
2022-01-12 16:01:42 +01:00
parent fc1c7e2718
commit c3deab6b89
126 changed files with 16865 additions and 4755 deletions

View File

@@ -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",
],

View File

@@ -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

View File

@@ -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"],
)

View File

@@ -11,8 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef OR_TOOLS_MATH_OPT_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_

View File

@@ -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

View File

@@ -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

View File

@@ -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.
////////////////////////////////////////////////////////////////////////////////

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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_

View 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

View 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_

View File

@@ -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

View File

@@ -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

View 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

View 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_

View File

@@ -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_) {

View File

@@ -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 { \

View File

@@ -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"

View 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

View 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_

View 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_

View File

@@ -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>

View File

@@ -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",
],
)

View File

@@ -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) {

View File

@@ -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;

View 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_

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View 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

View 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_

View File

@@ -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

View File

@@ -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_

View 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

View 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_

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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_

View 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

View 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_

View File

@@ -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

View File

@@ -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_

View 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

View 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_

View 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

View 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_

View 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

View 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_

View 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

View 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_

View 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

View 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_

View File

@@ -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

View File

@@ -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 =

View File

@@ -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_

View File

@@ -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" =

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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"],

View File

@@ -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

View 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;
}

View 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;
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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"],
)

View File

@@ -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);

View File

@@ -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_;

View File

@@ -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());

View File

@@ -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_;

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,94 @@
// Copyright 2010-2021 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef 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_

View 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;
}

View 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",
],
)

View 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

View 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_

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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;

View File

@@ -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