diff --git a/ortools/math_opt/BUILD.bazel b/ortools/math_opt/BUILD.bazel index 0a38945c22..0fca9e46ad 100644 --- a/ortools/math_opt/BUILD.bazel +++ b/ortools/math_opt/BUILD.bazel @@ -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", ], diff --git a/ortools/math_opt/callback.proto b/ortools/math_opt/callback.proto index d19d735cf0..7c330bae31 100644 --- a/ortools/math_opt/callback.proto +++ b/ortools/math_opt/callback.proto @@ -24,60 +24,82 @@ syntax = "proto3"; package operations_research.math_opt; import "google/protobuf/duration.proto"; -import "ortools/math_opt/solution.proto"; import "ortools/math_opt/sparse_containers.proto"; +option java_package = "com.google.ortools.mathopt"; +option java_multiple_files = true; + // This is the list of supported events. enum CallbackEventProto { CALLBACK_EVENT_UNSPECIFIED = 0; - // Quick ping from the solver saying “I am still working, and not stuck”. - CALLBACK_EVENT_POLLING = 1; + // The solver is currently running presolve. - CALLBACK_EVENT_PRESOLVE = 2; + // + // This event is supported for MIP & LP models by SOLVER_TYPE_GUROBI. Other + // solvers don't support this event. + CALLBACK_EVENT_PRESOLVE = 1; + // The solver is currently running the simplex method. - CALLBACK_EVENT_SIMPLEX = 3; + // + // This event is supported for MIP & LP models by SOLVER_TYPE_GUROBI. Other + // solvers don't support this event. + CALLBACK_EVENT_SIMPLEX = 2; + // The solver is in the MIP loop (called periodically before starting a new // node). Useful for early termination. Note that this event does not provide // information on LP relaxations nor about new incumbent solutions. - CALLBACK_EVENT_MIP = 4; + // + // This event is supported for MIP models only by SOLVER_TYPE_GUROBI. Other + // solvers don't support this event. + CALLBACK_EVENT_MIP = 3; + // Called every time a new MIP incumbent is found. - CALLBACK_EVENT_MIP_SOLUTION = 5; + // + // This event is fully supported for MIP models by SOLVER_TYPE_GUROBI. CP-SAT + // has partial support: you can view the solutions and request termination, + // but you cannot add lazy constraints. Other solvers don't support this + // event. + CALLBACK_EVENT_MIP_SOLUTION = 4; + // Called inside a MIP node. Note that there is no guarantee that the // callback function will be called on every node. That behavior is // solver-dependent. - CALLBACK_EVENT_MIP_NODE = 6; + // + // Disabling cuts using CommonSolveParametersProto may interfere with this + // event being called and/or adding cuts at this event, the behavior is solver + // specific. + // + // This event is supported for MIP models only by SOLVER_TYPE_GUROBI. Other + // solvers don't support this event. + CALLBACK_EVENT_MIP_NODE = 5; + // Called in each iterate of an interior point/barrier method. - CALLBACK_EVENT_BARRIER = 7; - // Called when the solver wants to log a message. - CALLBACK_EVENT_MESSAGE = 8; + // + // This event is supported for LP models only by SOLVER_TYPE_GUROBI. Other + // solvers don't support this event. + CALLBACK_EVENT_BARRIER = 6; } // The callback function input data. // Note that depending on the event, some information might be unavailable. message CallbackDataProto { CallbackEventProto event = 1; - // if event == CALLBACK_EVENT_MIP_NODE, the primal_solution contains the - // primal solution to the current LP-node relaxation. In some cases, no - // solution will be available (e.g. because LP was infeasible or the solve - // was imprecise). - // if event == CALLBACK_EVENT_MIP_SOLUTION, the primal_solution contains the - // newly found primal (integer) feasible solution. - // Otherwise, the primal_solution is not available. + // if event == CALLBACK_EVENT_MIP_NODE, the primal_solution_vector contains + // the variable values of the primal solution for the current LP-node + // relaxation. In some cases, no solution will be available (e.g. because + // LP was infeasible or the solve was imprecise). + // if event == CALLBACK_EVENT_MIP_SOLUTION, the primal_solution_vector + // contains variable values for the newly found primal (integer) feasible + // solution. + // Otherwise, the primal_solution_vector is not available. // // Note that, because of variable filters, it is possible that when a solution // is found, it is empty. The message will be set but left empty in this case, // while it will be unset when no solution is available. - // - // TODO(b/186740537): change the type to SparseDoubleVectorProto. - PrimalSolutionProto primal_solution = 2; - - // If event == CALLBACK_EVENT_MESSAGE, return the messages from the solver. - // Each message represents a single output line from the solver, and each - // message does not contain any '\n' character in it. - repeated string messages = 3; + SparseDoubleVectorProto primal_solution_vector = 2; // Running time since the `Solve` call. - google.protobuf.Duration runtime = 4; + google.protobuf.Duration runtime = 3; // Presolve stats. Only available during CALLBACK_EVENT_PRESOLVE. message PresolveStats { @@ -86,7 +108,7 @@ message CallbackDataProto { optional int64 bound_changes = 3; optional int64 coefficient_changes = 4; } - PresolveStats presolve_stats = 5; + PresolveStats presolve_stats = 4; // Simplex stats. Only available during CALLBACK_EVENT_SIMPLEX. message SimplexStats { @@ -96,7 +118,7 @@ message CallbackDataProto { optional double dual_infeasibility = 4; optional bool is_pertubated = 5; } - SimplexStats simplex_stats = 6; + SimplexStats simplex_stats = 5; // Barrier stats. Only available during CALLBACK_EVENT_BARRIER. message BarrierStats { @@ -107,9 +129,10 @@ message CallbackDataProto { optional double primal_infeasibility = 5; optional double dual_infeasibility = 6; } - BarrierStats barrier_stats = 7; + BarrierStats barrier_stats = 6; // MIP B&B stats. Only available during CALLBACK_EVENT_MIPxxxx events. + // Not supported for CP-SAT. message MipStats { optional double primal_bound = 1; optional double dual_bound = 2; @@ -119,7 +142,7 @@ message CallbackDataProto { optional int32 number_of_solutions_found = 6; optional int32 cutting_planes_in_lp = 7; } - MipStats mip_stats = 8; + MipStats mip_stats = 7; } // Return value of a callback function. @@ -142,6 +165,7 @@ message CallbackResultProto { // Ends the solve early. bool terminate = 1; + // TODO(b/172214608): SCIP allows to reject a feasible solution without // providing a cut. This is something we might support at a later stage. @@ -149,21 +173,26 @@ message CallbackResultProto { // GeneratedLinearConstraint::is_lazy for details. repeated GeneratedLinearConstraint cuts = 4; + // Use only for CALLBACK_EVENT_MIP_NODE. + // // Note that some solvers (e.g. Gurobi) support partially-defined solutions. // The most common use case is to specify a value for each variable in the // model. If a variable is not present in the primal solution, its value is // taken to be undefined, and is up to the underlying solver to deal with it. // For example, Gurobi will try to solve a Sub-MIP to get a fully feasible // solution if necessary. - // - // TODO(b/183631989) rename to suggested_solutions. - // TODO(b/186740537): change the type to SparseDoubleVectorProto. - repeated PrimalSolutionProto suggested_solution = 5; + repeated SparseDoubleVectorProto suggested_solutions = 5; } message CallbackRegistrationProto { - // What events you want to get a callback at. - // By default, there are no callbacks on any event. + // The events the solver should invoke the callback at. + // + // A solver will return an InvalidArgument status when called with registered + // events that are not supported for the selected solver and the type of + // model. For example registring for CALLBACK_EVENT_MIP with a model that only + // contains continuous variables will fail for most solvers (see the + // documentation of each event to see which solvers support them and in which + // case). repeated CallbackEventProto request_registration = 1; // If CALLBACK_EVENT_MIP_SOLUTION is in `request_registration`, then diff --git a/ortools/math_opt/core/BUILD.bazel b/ortools/math_opt/core/BUILD.bazel index f824879f40..2b23e99458 100644 --- a/ortools/math_opt/core/BUILD.bazel +++ b/ortools/math_opt/core/BUILD.bazel @@ -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"], +) diff --git a/ortools/math_opt/cpp/arrow_operator_proxy.h b/ortools/math_opt/core/arrow_operator_proxy.h similarity index 88% rename from ortools/math_opt/cpp/arrow_operator_proxy.h rename to ortools/math_opt/core/arrow_operator_proxy.h index 3b5398d80e..dcbd58c661 100644 --- a/ortools/math_opt/cpp/arrow_operator_proxy.h +++ b/ortools/math_opt/core/arrow_operator_proxy.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_MATH_OPT_CPP_ARROW_OPERATOR_PROXY_H_ -#define OR_TOOLS_MATH_OPT_CPP_ARROW_OPERATOR_PROXY_H_ +#ifndef OR_TOOLS_MATH_OPT_CORE_ARROW_OPERATOR_PROXY_H_ +#define OR_TOOLS_MATH_OPT_CORE_ARROW_OPERATOR_PROXY_H_ #include // 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_ diff --git a/ortools/math_opt/core/indexed_model.cc b/ortools/math_opt/core/indexed_model.cc deleted file mode 100644 index e7b377bb2a..0000000000 --- a/ortools/math_opt/core/indexed_model.cc +++ /dev/null @@ -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 -#include -#include -#include -#include - -#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 -std::vector MapKeys(const absl::flat_hash_map& in_map) { - std::vector keys; - keys.reserve(in_map.size()); - for (const auto& key_pair : in_map) { - keys.push_back(key_pair.first); - } - return keys; -} - -template -std::vector SortedMapKeys(const absl::flat_hash_map& in_map) { - std::vector keys = MapKeys(in_map); - std::sort(keys.begin(), keys.end()); - return keys; -} - -template -std::vector SortedSetKeys(const absl::flat_hash_set& in_set) { - std::vector 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 -void AppendFromMapOrDefault(const absl::Span ids, - const absl::flat_hash_map& 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 -void AppendFromMapIfPresent(const IdIterable& ids, - const absl::flat_hash_map& 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 -void AppendFromMap(const absl::flat_hash_set& dirty_keys, - const absl::flat_hash_map& 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 -absl::flat_hash_map SparseBasisVectorToMap( - const SparseBasisStatusVector& sparse_vector) { - absl::flat_hash_map 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(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 IndexedModel::variables() const { - return MapKeys(variables_); -} - -std::vector 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 IndexedModel::linear_constraints() const { - return MapKeys(linear_constraints_); -} - -std::vector IndexedModel::SortedLinearConstraints() const { - return SortedMapKeys(linear_constraints_); -} - -std::vector 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> 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( - 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 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( - SortedSetKeys(dirty_linear_objective_coefficients_), linear_objective_, - *obj_updates->mutable_linear_coefficients()); - // TODO(b/182567749): Once StrongInt is in absl, use - // AppendFromMapIfPresent( - // 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> - 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(); - 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(); - 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(); - d.dual_values = - MakeView(dual_solution.dual_values()).as_map(); - 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(); - dr.dual_values = - MakeView(dual_ray.dual_values()).as_map(); - solutions.dual_rays.push_back(std::move(dr)); - } - for (const BasisProto& basis : solve_result.basis()) { - IndexedBasis indexed_basis; - indexed_basis.constraint_status = - SparseBasisVectorToMap(basis.constraint_status()); - indexed_basis.variable_status = - SparseBasisVectorToMap(basis.variable_status()); - solutions.basis.push_back(std::move(indexed_basis)); - } - return solutions; -} - -std::unique_ptr 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 -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 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(); - 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 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 update = - indexed_model_.ExportSharedModelUpdate(); - if (update) { - const auto shared_update = - std::make_shared(*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 diff --git a/ortools/math_opt/core/math_opt_proto_utils.cc b/ortools/math_opt/core/math_opt_proto_utils.cc index 09de148154..2c3ee290d2 100644 --- a/ortools/math_opt/core/math_opt_proto_utils.cc +++ b/ortools/math_opt/core/math_opt_proto_utils.cc @@ -75,5 +75,26 @@ absl::flat_hash_set 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 diff --git a/ortools/math_opt/core/math_opt_proto_utils.h b/ortools/math_opt/core/math_opt_proto_utils.h index 84a445152c..5241e091c6 100644 --- a/ortools/math_opt/core/math_opt_proto_utils.h +++ b/ortools/math_opt/core/math_opt_proto_utils.h @@ -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 EventSet( const CallbackRegistrationProto& callback_registration); +TerminationProto TerminateForLimit(LimitProto limit, + absl::string_view detail = {}); + +TerminationProto TerminateForReason(TerminationReasonProto reason, + absl::string_view detail = {}); + //////////////////////////////////////////////////////////////////////////////// // Inline functions implementations. //////////////////////////////////////////////////////////////////////////////// diff --git a/ortools/math_opt/core/model_storage.cc b/ortools/math_opt/core/model_storage.cc new file mode 100644 index 0000000000..552a4395a1 --- /dev/null +++ b/ortools/math_opt/core/model_storage.cc @@ -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 +#include +#include +#include +#include +#include +#include + +#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 +std::vector MapKeys(const absl::flat_hash_map& in_map) { + std::vector keys; + keys.reserve(in_map.size()); + for (const auto& key_pair : in_map) { + keys.push_back(key_pair.first); + } + return keys; +} + +template +std::vector SortedMapKeys(const absl::flat_hash_map& in_map) { + std::vector keys = MapKeys(in_map); + std::sort(keys.begin(), keys.end()); + return keys; +} + +template +std::vector SortedSetKeys(const absl::flat_hash_set& in_set) { + std::vector 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 +void AppendFromMapOrDefault(const absl::Span ids, + const absl::flat_hash_map& 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 +void AppendFromMapIfPresent(const IdIterable& ids, + const absl::flat_hash_map& 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 +void AppendFromMap(const absl::flat_hash_set& dirty_keys, + const absl::flat_hash_map& 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 +absl::flat_hash_map SparseBasisVectorToMap( + const SparseBasisStatusVector& sparse_vector) { + absl::flat_hash_map 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(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 +SparseDoubleMatrixProto ExportMatrix( + const absl::flat_hash_map, double>& coefficients, + const std::vector>& 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> 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(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::Clone() const { + absl::StatusOr> 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 ModelStorage::variables() const { + return MapKeys(variables_); +} + +std::vector 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 ModelStorage::linear_constraints() const { + return MapKeys(linear_constraints_); +} + +std::vector ModelStorage::SortedLinearConstraints() const { + return SortedMapKeys(linear_constraints_); +} + +std::vector 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( + 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( + linear_constraint_matrix_, SortedMapKeys(linear_constraint_matrix_)); + return result; +} + +std::optional 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( + SortedSetKeys(dirty_linear_objective_coefficients_), linear_objective_, + *obj_updates->mutable_linear_coefficients()); + // TODO(b/182567749): Once StrongInt is in absl, use + // AppendFromMapIfPresent( + // 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> 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> + 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()) + .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 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& 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 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(); + 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 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& 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 update = ExportSharedModelUpdate(); + if (update) { + const auto shared_update = + std::make_shared(*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 diff --git a/ortools/math_opt/core/indexed_model.h b/ortools/math_opt/core/model_storage.h similarity index 64% rename from ortools/math_opt/core/indexed_model.h rename to ortools/math_opt/core/model_storage.h index 61d664436d..89aa103cd3 100644 --- a/ortools/math_opt/core/indexed_model.h +++ b/ortools/math_opt/core/model_storage.h @@ -11,9 +11,46 @@ // See the License for the specific language governing permissions and // limitations under the License. -// An index based C++ API for building optimization problems. +#ifndef OR_TOOLS_MATH_OPT_CORE_MODEL_STORAGE_H_ +#define OR_TOOLS_MATH_OPT_CORE_MODEL_STORAGE_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/base/thread_annotations.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/meta/type_traits.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/mutex.h" +#include "ortools/base/map_util.h" +#include "ortools/base/int_type.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_update.pb.h" + +namespace operations_research { +namespace math_opt { + +DEFINE_INT_TYPE(VariableId, int64_t); +DEFINE_INT_TYPE(LinearConstraintId, int64_t); +DEFINE_INT_TYPE(UpdateTrackerId, int64_t); + +// An index based C++ API for building & storing optimization problems. // -// TODO(b/188550843): move/rename to core/model_storage.h +// Note that this API should usually not be used by C++ users that should prefer +// the math_opt/cpp/model.h API. +// +// It supports the efficient creation and modification of an optimization model, +// and the export of ModelProto and ModelUpdateProto protos. +// +// All methods run in amortized O(1) (as amortized over calls to that exact +// function) unless otherwise specified. // // Models problems of the form: // min sum_{j in J} c_j * x_j + d @@ -43,13 +80,13 @@ // x in {0.0, 1.0} // 0 <= y <= 2.5 // -// using ::operations_research::math_opt::IndexedModel; +// using ::operations_research::math_opt::ModelStorage; // using ::operations_research::math_opt::VariableId; // using ::operations_research::math_opt::LinearConstraintId; // using ::operations_research::math_opt::ModelProto; // using ::operations_research::math_opt::ModelProtoUpdate; // -// IndexedModel model("my_model"); +// ModelStorage model("my_model"); // const VariableId x = model.AddVariable(0.0, 1.0, true, "x"); // const VariableId y = model.AddVariable(0.0, 2.5, false, "y"); // const LinearConstraintId c = model.AddLinearConstraint( @@ -66,12 +103,11 @@ // // Modify the problem and get a model update proto: // -// const std::unique_ptr update_tracker = -// model.NewUpdateTracker(); +// const UpdateTrackerId update_tracker = model.NewUpdateTracker(); // c.set_upper_bound(2.0); -// const absl::optional update_proto = -// update_tracker->ExportModelUpdate(); -// update_tracker->Checkpoint(); +// const std::optional update_proto = +// model.ExportModelUpdate(update_tracker); +// model.Checkpoint(update_tracker); // // Reading and writing model properties: // @@ -88,23 +124,22 @@ // // Exporting the Model proto: // -// The Model proto is an equivalent representation to IndexedModel. It has a +// The Model proto is an equivalent representation to ModelStorage. It has a // smaller memory footprint and optimized for storage/transport, rather than // efficient modification. It is also the format consumed by solvers in this // library. The Model proto can be generated by calling -// IndexedModel::ExportModel(). +// ModelStorage::ExportModel(). // // Incrementalism, the ModelUpdate proto, and Checkpoints: // // To update an existing model as specified by a Model proto, solvers consume a // ModelUpdate proto, which describes the changes to a model (e.g. new variables -// or a change in a variable bound). IndexedModel::UpdateTracker tracks the +// or a change in a variable bound). ModelStorage::NewUpdateTracker() tracks the // changes made and produces a ModelUpdate proto describing these changes with -// the method UpdateTracker::ExportModelUpdate(). The changes returned will be -// the modifications since the previous call to -// UpdateTracker::Checkpoint(). Note that, for newly initialized models, before -// the first checkpoint, there is no additional memory overhead from tracking -// changes. See +// the method ModelStorage::ExportModelUpdate(). The changes returned will be +// the modifications since the previous call to ModelStorage::Checkpoint(). Note +// that, for newly initialized models, before the first checkpoint, there is no +// additional memory overhead from tracking changes. See // g3doc/ortools/math_opt/g3doc/model_building_complexity.md // for details. // @@ -117,141 +152,31 @@ // model is invalid (e.g. a variable lower bound is infinite, exceeds an upper // bound, or is NaN). The exported models are validated instead, see // model_validator.h. - -#ifndef OR_TOOLS_MATH_OPT_CORE_INDEXED_MODEL_H_ -#define OR_TOOLS_MATH_OPT_CORE_INDEXED_MODEL_H_ - -#include -#include -#include -#include -#include -#include -#include - -#include "ortools/base/integral_types.h" -#include "absl/base/thread_annotations.h" -#include "absl/container/flat_hash_map.h" -#include "absl/container/flat_hash_set.h" -#include "absl/meta/type_traits.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/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 { - -DEFINE_INT_TYPE(VariableId, int64_t); -DEFINE_INT_TYPE(LinearConstraintId, int64_t); - -// A mathematical optimization model. -// -// Supports the efficient creation and modification of an optimization model, -// and the export of Model and ModelUpdate protos. -// -// All methods run in amortized O(1) (as amortized over calls to that exact -// function) unless otherwise specified. -class IndexedModel { +class ModelStorage { public: - // Tracks the changes of the model. + // Returns a storage from the input proto. Returns a failure status if the + // input proto is invalid. // - // For each update tracker we define a checkpoint that is the starting point - // used to compute the ModelUpdateProto. + // Variable/constraint names can be repeated in the input proto but will be + // considered invalid when solving. // - // Lifecycle: the IndexedModel must outlive all the UpdateTracker instances - // since they keep a reference to it. They use this reference in their - // destructor so it is not safe to destroy an UpdateTracker after the - // destruction of the IndexedModel. - // - // Thread-safety: UpdateTracker methods must not be used while modifying the - // IndexedModel. 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()). - // - // Example: - // IndexedModel model; - // ... - // ASSIGN_OR_RETURN(const auto solver, - // Solver::New(solver_type, model.ExportModel(), - // /*initializer=*/{})); - // const std::unique_ptr update_tracker = - // model.NewUpdatesTracker(); - // - // ASSIGN_OR_RETURN(const auto result_1, - // solver->Solve(/*parameters=*/{}); - // - // model.AddVariable(0.0, 1.0, true, "y"); - // model.set_maximize(true); - // - // const absl::optional update_proto = - // update_tracker.ExportModelUpdate(); - // update_tracker.Checkpoint(); - // - // if (update_proto) { - // ASSIGN_OR_RETURN(const bool updated, solver->Update(*update_proto)); - // if (!updated) { - // // The update is not supported by the solver, we create a new one. - // ASSIGN_OR_RETURN(const auto new_model_proto, model.ExportModel()); - // ASSIGN_OR_RETURN(solver, - // Solver::New(solver_type, new_model_proto, - // /*initializer=*/{})); - // } - // } - // ASSIGN_OR_RETURN(const auto result_2, - // solver->Solve(/*parameters=*/{}); - // - class UpdateTracker { - public: - ~UpdateTracker() ABSL_LOCKS_EXCLUDED(indexed_model_.update_trackers_lock_); - - // 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. - absl::optional ExportModelUpdate() - ABSL_LOCKS_EXCLUDED(indexed_model_.update_trackers_lock_); - - // Uses the current model state as the starting point to calculate the - // ModelUpdateProto next time ExportModelUpdate() is called. - void Checkpoint() ABSL_LOCKS_EXCLUDED(indexed_model_.update_trackers_lock_); - - private: - friend class IndexedModel; - - // Registers in update_trackers_ and calls Checkpoint() to synchronize the - // checkpoint to the current state of the model. - explicit UpdateTracker(IndexedModel& indexed_model) - ABSL_LOCKS_EXCLUDED(indexed_model.update_trackers_lock_); - - // Same as Checkpoint() but the caller must have acquired the - // update_trackers_lock_ mutex. - void CheckpointLocked() - ABSL_EXCLUSIVE_LOCKS_REQUIRED(indexed_model_.update_trackers_lock_); - - IndexedModel& indexed_model_; - - // All incremental updates that occurred since last checkpoint. It is - // filled-in each time Checkpoint() is called on any update tracker. When an - // ExportModelUpdate() is requested on a tracker, all these are merged along - // with the remaining updates. - std::vector> updates_ - ABSL_GUARDED_BY(indexed_model_.update_trackers_lock_); - }; + // See ApplyUpdateProto() for dealing with subsequent updates. + static absl::StatusOr> FromModelProto( + const ModelProto& model_proto); // Creates an empty minimization problem. - explicit IndexedModel(absl::string_view name = "") : name_(name) {} + explicit ModelStorage(absl::string_view name = "") : name_(name) {} - // TODO(b/185892243): add `std::unique_ptr Clone()` method. - IndexedModel(const IndexedModel&) = delete; - IndexedModel& operator=(const IndexedModel&) = delete; + ModelStorage(const ModelStorage&) = delete; + ModelStorage& operator=(const ModelStorage&) = delete; + + // Returns a clone of the model. + // + // The variables and constraints have the same ids. The clone will also not + // reused any id of variable/constraint that was deleted in the original. + // + // Note that the returned model does not have any update tracker. + std::unique_ptr Clone() const; inline const std::string& name() const { return name_; } @@ -415,8 +340,14 @@ class IndexedModel { inline double objective_offset() const; // Returns 0.0 if this variable has no linear objective coefficient. inline double linear_objective_coefficient(VariableId variable) const; + // The ordering of the input variables does not matter. + inline double quadratic_objective_coefficient( + VariableId first_variable, VariableId second_variable) const; inline bool is_linear_objective_coefficient_nonzero( VariableId variable) const; + // The ordering of the input variables does not matter. + inline bool is_quadratic_objective_coefficient_nonzero( + VariableId first_variable, VariableId second_variable) const; inline void set_is_maximize(bool is_maximize); inline void set_maximize(); @@ -427,17 +358,28 @@ class IndexedModel { // representation (and has no effect if the variable is not present). inline void set_linear_objective_coefficient(VariableId variable, double value); + // 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 ordering of the input variables does not matter. + inline void set_quadratic_objective_coefficient(VariableId first_variable, + VariableId second_variable, + double value); // Equivalent to calling set_linear_objective_coefficient(v, 0.0) for every // variable with nonzero objective coefficient. // - // Runs in O(#variables with nonzero objective coefficient). + // Runs in O(# nonzero linear/quadratic objective terms). inline void clear_objective(); // The variables with nonzero linear objective coefficients. inline const absl::flat_hash_map& linear_objective() const; + // The variable pairs with nonzero quadratic objective coefficients. The keys + // are ordered such that .first <= .second. + inline const absl::flat_hash_map, double>& + quadratic_objective() const; + // Returns a sorted vector of all variables in the model with nonzero linear // objective coefficients. // @@ -449,20 +391,104 @@ class IndexedModel { ////////////////////////////////////////////////////////////////////////////// // Returns a proto representation of the optimization model. + // + // See FromModelProto() to build a ModelStorage from a proto. ModelProto ExportModel() const; - // Returns a tracker that can be used to generate a ModelUpdateProto with the + // Creates 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 IndexedModel. Thus the - // IndexedModel must not be destroyed before the destruction of all its - // UpdateTracker instances. + // Thread-safety: this method must not be used while modifying the + // ModelStorage. The user is expected to use proper synchronization primitive + // to serialize changes to the model and the use of this method. It can be + // called concurrently to create multiple trackers though. + // + // For each update tracker we define a checkpoint that is the starting point + // used to compute the ModelUpdateProto. + // + // Example: + // ModelStorage model; + // ... + // ASSIGN_OR_RETURN(const auto solver, + // Solver::New(solver_type, model.ExportModel(), + // /*initializer=*/{})); + // const UpdateTrackerId update_tracker = model.NewUpdatesTracker(); + // + // ASSIGN_OR_RETURN(const auto result_1, + // solver->Solve(/*parameters=*/{}); + // + // model.AddVariable(0.0, 1.0, true, "y"); + // model.set_maximize(true); + // + // const std::optional update_proto = + // model.ExportModelUpdate(update_tracker); + // model.Checkpoint(update_tracker); + // + // if (update_proto) { + // ASSIGN_OR_RETURN(const bool updated, solver->Update(*update_proto)); + // if (!updated) { + // // The update is not supported by the solver, we create a new one. + // ASSIGN_OR_RETURN(const auto new_model_proto, model.ExportModel()); + // ASSIGN_OR_RETURN(solver, + // Solver::New(solver_type, new_model_proto, + // /*initializer=*/{})); + // } + // } + // ASSIGN_OR_RETURN(const auto result_2, + // solver->Solve(/*parameters=*/{}); + // + UpdateTrackerId NewUpdateTracker(); + + // Deletes the input tracker. + // + // It must not be used anymore after its destruction. It can be deleted once, + // trying to delete it a second time or use it will raise an assertion + // (CHECK). + // + // The update trackers are automatically deleted when the ModelStorage is + // destroyed. Calling this function is thus only useful for performance + // reasons, to ensure the ModelStorage does not keep data for update trackers + // that are not needed anymore. // // Thread-safety: this method must not be used while modifying the - // IndexedModel. The user is expected to use proper synchronization primitive + // ModelStorage. The user is expected to use proper synchronization primitive // to serialize changes to the model and the use of this method. - std::unique_ptr NewUpdateTracker(); + // + // It can be called concurrently to delete multiple trackers though. + void DeleteUpdateTracker(UpdateTrackerId update_tracker); + + // 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. + // + // Thread-safety: this method must not be used while modifying the + // ModelStorage. The user is expected to use proper synchronization + // primitive to serialize changes to the model and the use of this method. + // + // It can be called concurrently for different update trackers though. + std::optional ExportModelUpdate( + UpdateTrackerId update_tracker); + + // Uses the current model state as the starting point to calculate the + // ModelUpdateProto next time ExportModelUpdate() is called. + // + // Thread-safety: this method must not be used while modifying the + // ModelStorage. The user is expected to use proper synchronization + // primitive to serialize changes to the model and the use of this method. + // + // It can be called concurrently for different update trackers though. + void Checkpoint(UpdateTrackerId update_tracker); + + // Apply the provided update to this model. Returns a failure if the update is + // not valid. + // + // As with FromModelProto(), duplicated names are ignored. + // + // 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); private: struct VariableData { @@ -471,12 +497,24 @@ class IndexedModel { bool is_integer = false; std::string name = ""; }; + struct LinearConstraintData { double lower_bound = -std::numeric_limits::infinity(); double upper_bound = std::numeric_limits::infinity(); std::string name = ""; }; + struct UpdateTrackerData { + // All incremental updates that occurred since last checkpoint. It is + // filled-in each time Checkpoint() is called on any update tracker. When an + // ExportModelUpdate() is requested on a tracker, all these are merged along + // with the remaining updates. + // + // Guarded by: ModelStorage::update_trackers_lock_. There does not seem to + // be a way to use ABSL_GUARDED_BY here. + std::vector> updates; + }; + template void set_variable_property(VariableId id, T value, T VariableData::*field, absl::flat_hash_set& dirty_set); @@ -486,6 +524,48 @@ class IndexedModel { double LinearConstraintData::*field, absl::flat_hash_set& dirty_set); + // Adds a variable at the given id, updating next_variable_id_ and the lazy + // collections as side effect. It CHECKs that the provided id is not less than + // next_variable_id_. + // + // This is used internally by AddVariable() and AddVariables(). + void AddVariableInternal(VariableId id, double lower_bound, + double upper_bound, bool is_integer, + absl::string_view name); + + // Adds all variables from the given proto using AddVariableInternal(). Thus + // Ids must be greater or equal to next_variable_id_. + void AddVariables(const VariablesProto& variables); + + // Adds a linear constraint at the given id, updating next_variable_id_ and + // the lazy collections as side effect. It CHECKs that the provided id is not + // less than next_linear_constraint_id_. + // + // This is used internally by AddLinearConstraint() and + // AddLinearConstraints(). + void AddLinearConstraintInternal(LinearConstraintId id, double lower_bound, + double upper_bound, absl::string_view name); + + // Adds all constraints from the given proto using + // AddLinearConstraintInternal(). Thus Ids must be greater or equal to + // next_linear_constraint_id_. + void AddLinearConstraints(const LinearConstraintsProto& linear_constraints); + + // Updates the objective linear coefficients. The coefficients of variables + // not in the input are kept as-is. + void UpdateLinearObjectiveCoefficients( + const SparseDoubleVectorProto& coefficients); + + // Updates the objective quadratic coefficients. The coefficients of the pairs + // of variables not in the input are kept as-is. + void UpdateQuadraticObjectiveCoefficients( + const SparseDoubleMatrixProto& coefficients); + + // Updates the linear constraints' coefficients. The coefficients of + // (constraint, variable) pairs not in the input are kept as-is. + void UpdateLinearConstraintCoefficients( + const SparseDoubleMatrixProto& coefficients); + // Initializes lazy_matrix_columns_ (column major storage of the linear // constraint matrix) if it is still empty and there is at least one variable // in the model. @@ -496,6 +576,10 @@ class IndexedModel { // the model. void EnsureLazyMatrixRows(); + // Initializes lazy_quadratic_objective_by_variable_ if it is still empty and + // there is at least one variable in the model. + void EnsureLazyQuadraticObjective(); + // Export a single variable to proto. void AppendVariable(VariableId id, VariablesProto& variables_proto) const; @@ -504,25 +588,23 @@ class IndexedModel { LinearConstraintId id, LinearConstraintsProto& linear_constraints_proto) const; - // If an element in entries is not found in linear_constraint_matrix_, it is - // set to 0.0 in matrix. Entries should be sorted by constraint then by - // variable. - void ExportLinearConstraintMatrix( - absl::Span> entries, - SparseDoubleMatrixProto& matrix) const; - // Returns a proto representation of the changes to the model since the most // recent call to SharedCheckpoint() or nullopt if no changes happened. // // Thread-safety: this method must not be called concurrently (due to // EnsureLazyMatrixXxx() functions). - absl::optional ExportSharedModelUpdate() + std::optional ExportSharedModelUpdate() ABSL_EXCLUSIVE_LOCKS_REQUIRED(update_trackers_lock_); // Use the current model state as the starting point to calculate the // ModelUpdateProto next time ExportSharedModelUpdate() is called. void SharedCheckpoint() ABSL_EXCLUSIVE_LOCKS_REQUIRED(update_trackers_lock_); + // Same as Checkpoint() but the caller must have acquired the + // update_trackers_lock_ mutex. + void CheckpointLocked(UpdateTrackerId update_tracker) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(update_trackers_lock_); + std::string name_; VariableId next_variable_id_ = VariableId(0); LinearConstraintId next_linear_constraint_id_ = LinearConstraintId(0); @@ -535,6 +617,10 @@ class IndexedModel { linear_constraints_; // The values of the map must never include zero. absl::flat_hash_map linear_objective_; + // The values of the map must never include zero. The keys must be upper + // triangular, i.e. .first <= .second. + absl::flat_hash_map, double> + quadratic_objective_; // The values of the map must never include zero. absl::flat_hash_map, double> linear_constraint_matrix_; @@ -542,6 +628,15 @@ class IndexedModel { lazy_matrix_columns_; absl::flat_hash_map> lazy_matrix_rows_; + // To handle deletions we need to have an efficient way to look up which + // quadratic objective terms involve a given variable. This map stores this + // information where the key corresponds to a variable and the value is the + // set of all variables appearing in a quadratic objective term with the key. + // This data structure is only initialized after a call to + // EnsureLazyQuadraticObjective. As of 11/17/2021, this will have occurred if + // a nonzero quadratic objective term has ever been added to the model. + absl::flat_hash_map> + lazy_quadratic_objective_by_variable_; // Update information // @@ -559,6 +654,11 @@ class IndexedModel { absl::flat_hash_set dirty_variable_is_integer_; absl::flat_hash_set dirty_linear_objective_coefficients_; + // NOTE: quadratic objective coefficients are considered dirty, and therefore + // tracked in this set, if and only if both variables in the term are "old", + // i.e. not added since the last checkpoint. + absl::flat_hash_set> + dirty_quadratic_objective_coefficients_; absl::flat_hash_set dirty_linear_constraint_deletes_; absl::flat_hash_set dirty_linear_constraint_lower_bounds_; @@ -572,106 +672,20 @@ class IndexedModel { dirty_linear_constraint_matrix_keys_; // Lock used to serialize access to update_trackers_ and to the all fields of - // UpdateTracker. We use only one lock since trackers are modified in group - // (they share a chain of ModelUpdateProto and the update of one tracker + // UpdateTrackerData. We use only one lock since trackers are modified in + // group (they share a chain of ModelUpdateProto and the update of one tracker // usually requires the update of some of the others). absl::Mutex update_trackers_lock_; - // The UpdateTracker instances tracking the changes of to this model. This - // collection is updated by UpdateTracker constructor and destructor. It is - // used internally by UpdateTracker instances to know about other instances. - absl::flat_hash_set update_trackers_ - ABSL_GUARDED_BY(update_trackers_lock_); -}; + // Next index to use in NewUpdateTracker(). + UpdateTrackerId next_update_tracker_ + ABSL_GUARDED_BY(update_trackers_lock_) = {}; -// A solution to the problem modeled in IndexedModel. -// -// IndexedPrimalSolution will satisfy (see top of file notation): -// objective_value = c * variable_values + d -// In addition, if feasible (as is typical), it should additionally satisfy: -// vlb <= variable_values <= vub -// clb <= A * variable_values <= cub -// variable_values_i integer for i in I. -// -// For details, see go/mathopt-solutions#primal-solution. -struct IndexedPrimalSolution { - absl::flat_hash_map variable_values; - double objective_value; + // The UpdateTracker instances tracking the changes of to this model. + absl::flat_hash_map> + update_trackers_ ABSL_GUARDED_BY(update_trackers_lock_); }; -// A direction of improving objective value that maintains feasibility. -// -// Indexed primal ray will satisfy (see top of file notation): -// * c * x < 0 for minimization problems, c * x > 0 for maximization problems. -// * A_j * x <= 0 for finite cub_j -// * A_j * x >= 0 for finite clb_j -// * x_i <= 0 for finite vub_i -// * x_i >= 0 for finite vlb_i -// where A_j are the row of matrix coefficients for the jth linear constraint. -// -// -// Note that this alone does not prove unboundedness, you also need a feasible -// point. This is a certificate of dual infeasibility for LPs, see -// go/mathopt-dual#dual-inf-cert. -// -// For details, see go/mathopt-solutions#primal-ray. -struct IndexedPrimalRay { - absl::flat_hash_map variable_values; -}; - -// A solution to the dual of the problem modeled in IndexedModel. Applies only -// when the problem has a dual (e.g. LP). -// -// For details, see go/mathopt-dual#dual-solution. -// For an primal interpretation as objective-value/optimality certificates see: -// go/mathopt-solutions#opt-certificate -struct IndexedDualSolution { - absl::flat_hash_map dual_values; - absl::flat_hash_map reduced_costs; - double objective_value; -}; - -// A direction of improving objective value that maintains feasibility for the -// dual problem. Applies only when the problem has a dual (e.g. LP). -// -// Note that this alone does not prove dual unboundedness, you also need a dual -// feasible point. -// -// Also, a certificate of primal infeasibility. -// -// For details, see -// go/mathopt-dual#dual-ray -// go/mathopt-solutions#primal-inf-cert -struct IndexedDualRay { - absl::flat_hash_map dual_values; - absl::flat_hash_map reduced_costs; -}; - -// A basis for a solution of the problem modeled in IndexedModel. Applies only -// when the problem is an LP. -// -// For details, see go/mathopt-basis#basis. -// TODO(b/171883688): consider improving consistency for matchers in -// solver_matchers.h/cc (e.g. removing PrimalDualPair from that file) -struct IndexedBasis { - absl::flat_hash_map constraint_status; - absl::flat_hash_map variable_status; -}; - -// The solution (or potentially multiple solutions) to an LP or MIP. For LP, -// if the problem is solved to completion (i.e. we don't have an error or hit a -// limit), then at least one of these should be populated. -struct IndexedSolutions { - std::vector primal_solutions; - std::vector primal_rays; - std::vector dual_solutions; - std::vector dual_rays; - std::vector basis; -}; - -IndexedSolutions IndexedSolutionsFromProto( - const SolveResultProto& solve_result); - //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // Inlined function implementations @@ -682,29 +696,29 @@ IndexedSolutions IndexedSolutionsFromProto( // Variables //////////////////////////////////////////////////////////////////////////////// -VariableId IndexedModel::AddVariable(absl::string_view name) { +VariableId ModelStorage::AddVariable(absl::string_view name) { return AddVariable(-std::numeric_limits::infinity(), std::numeric_limits::infinity(), false, name); } -double IndexedModel::variable_lower_bound(const VariableId id) const { +double ModelStorage::variable_lower_bound(const VariableId id) const { return variables_.at(id).lower_bound; } -double IndexedModel::variable_upper_bound(const VariableId id) const { +double ModelStorage::variable_upper_bound(const VariableId id) const { return variables_.at(id).upper_bound; } -bool IndexedModel::is_variable_integer(VariableId id) const { +bool ModelStorage::is_variable_integer(VariableId id) const { return variables_.at(id).is_integer; } -const std::string& IndexedModel::variable_name(const VariableId id) const { +const std::string& ModelStorage::variable_name(const VariableId id) const { return variables_.at(id).name; } template -void IndexedModel::set_variable_property( +void ModelStorage::set_variable_property( const VariableId id, const T value, T VariableData::*const field, absl::flat_hash_set& dirty_set) { VariableData& var_data = variables_.at(id); @@ -716,37 +730,37 @@ void IndexedModel::set_variable_property( } } -void IndexedModel::set_variable_lower_bound(const VariableId id, +void ModelStorage::set_variable_lower_bound(const VariableId id, const double lower_bound) { set_variable_property(id, lower_bound, &VariableData::lower_bound, dirty_variable_lower_bounds_); } -void IndexedModel::set_variable_upper_bound(const VariableId id, +void ModelStorage::set_variable_upper_bound(const VariableId id, const double upper_bound) { set_variable_property(id, upper_bound, &VariableData::upper_bound, dirty_variable_upper_bounds_); } -void IndexedModel::set_variable_is_integer(const VariableId id, +void ModelStorage::set_variable_is_integer(const VariableId id, const bool is_integer) { set_variable_property(id, is_integer, &VariableData::is_integer, dirty_variable_is_integer_); } -void IndexedModel::set_variable_as_integer(VariableId id) { +void ModelStorage::set_variable_as_integer(VariableId id) { set_variable_is_integer(id, true); } -void IndexedModel::set_variable_as_continuous(VariableId id) { +void ModelStorage::set_variable_as_continuous(VariableId id) { set_variable_is_integer(id, false); } -int IndexedModel::num_variables() const { return variables_.size(); } +int ModelStorage::num_variables() const { return variables_.size(); } -VariableId IndexedModel::next_variable_id() const { return next_variable_id_; } +VariableId ModelStorage::next_variable_id() const { return next_variable_id_; } -bool IndexedModel::has_variable(const VariableId id) const { +bool ModelStorage::has_variable(const VariableId id) const { return variables_.contains(id); } @@ -754,27 +768,27 @@ bool IndexedModel::has_variable(const VariableId id) const { // Linear Constraints //////////////////////////////////////////////////////////////////////////////// -LinearConstraintId IndexedModel::AddLinearConstraint(absl::string_view name) { +LinearConstraintId ModelStorage::AddLinearConstraint(absl::string_view name) { return AddLinearConstraint(-std::numeric_limits::infinity(), std::numeric_limits::infinity(), name); } -double IndexedModel::linear_constraint_lower_bound( +double ModelStorage::linear_constraint_lower_bound( const LinearConstraintId id) const { return linear_constraints_.at(id).lower_bound; } -double IndexedModel::linear_constraint_upper_bound( +double ModelStorage::linear_constraint_upper_bound( const LinearConstraintId id) const { return linear_constraints_.at(id).upper_bound; } -const std::string& IndexedModel::linear_constraint_name( +const std::string& ModelStorage::linear_constraint_name( const LinearConstraintId id) const { return linear_constraints_.at(id).name; } -void IndexedModel::set_linear_constraint_property( +void ModelStorage::set_linear_constraint_property( const LinearConstraintId id, const double value, double LinearConstraintData::*const field, absl::flat_hash_set& dirty_set) { @@ -787,29 +801,29 @@ void IndexedModel::set_linear_constraint_property( } } -void IndexedModel::set_linear_constraint_lower_bound( +void ModelStorage::set_linear_constraint_lower_bound( const LinearConstraintId id, const double lower_bound) { set_linear_constraint_property(id, lower_bound, &LinearConstraintData::lower_bound, dirty_linear_constraint_lower_bounds_); } -void IndexedModel::set_linear_constraint_upper_bound( +void ModelStorage::set_linear_constraint_upper_bound( const LinearConstraintId id, const double upper_bound) { set_linear_constraint_property(id, upper_bound, &LinearConstraintData::upper_bound, dirty_linear_constraint_upper_bounds_); } -int IndexedModel::num_linear_constraints() const { +int ModelStorage::num_linear_constraints() const { return linear_constraints_.size(); } -LinearConstraintId IndexedModel::next_linear_constraint_id() const { +LinearConstraintId ModelStorage::next_linear_constraint_id() const { return next_linear_constraint_id_; } -bool IndexedModel::has_linear_constraint(const LinearConstraintId id) const { +bool ModelStorage::has_linear_constraint(const LinearConstraintId id) const { return linear_constraints_.contains(id); } @@ -817,18 +831,18 @@ bool IndexedModel::has_linear_constraint(const LinearConstraintId id) const { // Linear Constraint Matrix //////////////////////////////////////////////////////////////////////////////// -double IndexedModel::linear_constraint_coefficient( +double ModelStorage::linear_constraint_coefficient( LinearConstraintId constraint, VariableId variable) const { return gtl::FindWithDefault(linear_constraint_matrix_, {constraint, variable}); } -bool IndexedModel::is_linear_constraint_coefficient_nonzero( +bool ModelStorage::is_linear_constraint_coefficient_nonzero( LinearConstraintId constraint, VariableId variable) const { return linear_constraint_matrix_.contains({constraint, variable}); } -void IndexedModel::set_linear_constraint_coefficient( +void ModelStorage::set_linear_constraint_coefficient( LinearConstraintId constraint, VariableId variable, double value) { bool was_updated = false; if (value == 0.0) { @@ -864,18 +878,18 @@ void IndexedModel::set_linear_constraint_coefficient( } const absl::flat_hash_map, double>& -IndexedModel::linear_constraint_matrix() const { +ModelStorage::linear_constraint_matrix() const { return linear_constraint_matrix_; } const absl::flat_hash_set& -IndexedModel::variables_in_linear_constraint(LinearConstraintId constraint) { +ModelStorage::variables_in_linear_constraint(LinearConstraintId constraint) { EnsureLazyMatrixRows(); return lazy_matrix_rows_.at(constraint); } const absl::flat_hash_set& -IndexedModel::linear_constraints_with_variable(VariableId variable) { +ModelStorage::linear_constraints_with_variable(VariableId variable) { EnsureLazyMatrixColumns(); return lazy_matrix_columns_.at(variable); } @@ -884,38 +898,60 @@ IndexedModel::linear_constraints_with_variable(VariableId variable) { // Objective //////////////////////////////////////////////////////////////////////////////// -bool IndexedModel::is_maximize() const { return is_maximize_; } +namespace internal { -double IndexedModel::objective_offset() const { return objective_offset_; } +inline std::pair MakeOrderedPair(const VariableId a, + const VariableId b) { + return a < b ? std::make_pair(a, b) : std::make_pair(b, a); +} -double IndexedModel::linear_objective_coefficient(VariableId variable) const { +} // namespace internal + +bool ModelStorage::is_maximize() const { return is_maximize_; } + +double ModelStorage::objective_offset() const { return objective_offset_; } + +double ModelStorage::linear_objective_coefficient(VariableId variable) const { return gtl::FindWithDefault(linear_objective_, variable); } -bool IndexedModel::is_linear_objective_coefficient_nonzero( +double ModelStorage::quadratic_objective_coefficient( + const VariableId first_variable, const VariableId second_variable) const { + return gtl::FindWithDefault( + quadratic_objective_, + internal::MakeOrderedPair(first_variable, second_variable)); +} + +bool ModelStorage::is_linear_objective_coefficient_nonzero( VariableId variable) const { return linear_objective_.contains(variable); } -void IndexedModel::set_is_maximize(bool is_maximize) { +bool ModelStorage::is_quadratic_objective_coefficient_nonzero( + const VariableId first_variable, const VariableId second_variable) const { + return quadratic_objective_.contains( + internal::MakeOrderedPair(first_variable, second_variable)); +} + +void ModelStorage::set_is_maximize(bool is_maximize) { if (is_maximize_ != is_maximize) { dirty_objective_direction_ = true; is_maximize_ = is_maximize; } } -void IndexedModel::set_maximize() { set_is_maximize(true); } +void ModelStorage::set_maximize() { set_is_maximize(true); } -void IndexedModel::set_minimize() { set_is_maximize(false); } +void ModelStorage::set_minimize() { set_is_maximize(false); } -void IndexedModel::set_objective_offset(double value) { +void ModelStorage::set_objective_offset(double value) { if (value != objective_offset_) { dirty_objective_offset_ = true; objective_offset_ = value; } } -void IndexedModel::set_linear_objective_coefficient(VariableId variable, +void ModelStorage::set_linear_objective_coefficient(VariableId variable, double value) { bool was_updated = false; if (value == 0.0) { @@ -937,19 +973,62 @@ void IndexedModel::set_linear_objective_coefficient(VariableId variable, } } -void IndexedModel::clear_objective() { +void ModelStorage::set_quadratic_objective_coefficient( + const VariableId first_variable, const VariableId second_variable, + double value) { + const std::pair key = + internal::MakeOrderedPair(first_variable, second_variable); + bool was_updated = false; + if (value == 0.0) { + if (quadratic_objective_.erase(key) > 0) { + was_updated = true; + } + } else { + const auto [iterator, inserted] = + quadratic_objective_.try_emplace(key, value); + if (inserted) { + was_updated = true; + } else if (iterator->second != value) { + iterator->second = value; + was_updated = true; + } + } + if (was_updated) { + if (!lazy_quadratic_objective_by_variable_.empty()) { + lazy_quadratic_objective_by_variable_.at(first_variable) + .insert(second_variable); + lazy_quadratic_objective_by_variable_.at(second_variable) + .insert(first_variable); + } + if (key.second < variables_checkpoint_) { + dirty_quadratic_objective_coefficients_.insert(key); + } + } +} + +void ModelStorage::clear_objective() { set_objective_offset(0.0); while (!linear_objective_.empty()) { set_linear_objective_coefficient(linear_objective_.begin()->first, 0.0); } + while (!quadratic_objective_.empty()) { + set_quadratic_objective_coefficient( + quadratic_objective_.begin()->first.first, + quadratic_objective_.begin()->first.second, 0.0); + } } -const absl::flat_hash_map& IndexedModel::linear_objective() +const absl::flat_hash_map& ModelStorage::linear_objective() const { return linear_objective_; } +const absl::flat_hash_map, double>& +ModelStorage::quadratic_objective() const { + return quadratic_objective_; +} + } // namespace math_opt } // namespace operations_research -#endif // OR_TOOLS_MATH_OPT_CORE_INDEXED_MODEL_H_ +#endif // OR_TOOLS_MATH_OPT_CORE_MODEL_STORAGE_H_ diff --git a/ortools/math_opt/core/model_summary.h b/ortools/math_opt/core/model_summary.h index ea3bdfc290..35169e410d 100644 --- a/ortools/math_opt/core/model_summary.h +++ b/ortools/math_opt/core/model_summary.h @@ -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::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> 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& 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 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 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::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 diff --git a/ortools/math_opt/core/model_update_merge.cc b/ortools/math_opt/core/model_update_merge.cc index 6d99709469..05ae052913 100644 --- a/ortools/math_opt/core/model_update_merge.cc +++ b/ortools/math_opt/core/model_update_merge.cc @@ -16,10 +16,13 @@ #include #include #include +#include #include #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 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 + 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()); + 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()); + 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& from, - google::protobuf::RepeatedField& into) { +void RemoveDeletedIds(google::protobuf::RepeatedField& ids, + const google::protobuf::RepeatedField& 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& from_new, + google::protobuf::RepeatedField& into_old, + const google::protobuf::RepeatedField& deleted) { google::protobuf::RepeatedField 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& deleted_rows, + const google::protobuf::RepeatedField& 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 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 diff --git a/ortools/math_opt/core/model_update_merge.h b/ortools/math_opt/core/model_update_merge.h index b1ad76d960..164dc2fd1c 100644 --- a/ortools/math_opt/core/model_update_merge.h +++ b/ortools/math_opt/core/model_update_merge.h @@ -18,47 +18,92 @@ #include #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& from, - google::protobuf::RepeatedField& 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& ids, + const google::protobuf::RepeatedField& 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& from_new, + google::protobuf::RepeatedField& into_old, + const google::protobuf::RepeatedField& 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 -inline void MergeIntoSparseVector(const SparseVector& from, SparseVector& into); +void MergeIntoSparseVector(const SparseVector& from_new, SparseVector& into_old, + const google::protobuf::RepeatedField& 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& deleted_rows, + const google::protobuf::RepeatedField& 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 to deal with the `names` property. +template +void UpdateNewElementProperty(const google::protobuf::RepeatedField& ids, + RepeatedField& values, + const google::protobuf::RepeatedField& deleted, + const SparseVector& updates); } // namespace internal @@ -69,57 +114,94 @@ void MergeIntoSparseDoubleMatrix(const SparseDoubleMatrixProto& from, namespace internal { template -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& 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& 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 +void UpdateNewElementProperty(const google::protobuf::RepeatedField& ids, + RepeatedField& values, + const google::protobuf::RepeatedField& 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. + values.SwapElements(next_insertion_point, i); + } + ++next_insertion_point; + } + + // We can't use value.Truncate() here since RepeatedPtrField does + // not implement it. + google::protobuf::util::Truncate(&values, next_insertion_point); } } // namespace internal diff --git a/ortools/math_opt/core/non_streamable_solver_init_arguments.h b/ortools/math_opt/core/non_streamable_solver_init_arguments.h new file mode 100644 index 0000000000..58f1344877 --- /dev/null +++ b/ortools/math_opt/core/non_streamable_solver_init_arguments.h @@ -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 + +#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 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 +struct NonStreamableSolverInitArgumentsHelper + : public NonStreamableSolverInitArguments { + SolverTypeProto solver_type() const final { return impl_solver_type; } + + std::unique_ptr Clone() const final { + return std::make_unique( + *static_cast(this)); + } +}; + +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_CORE_NON_STREAMABLE_SOLVER_INIT_ARGUMENTS_H_ diff --git a/ortools/math_opt/core/solve_interrupter.cc b/ortools/math_opt/core/solve_interrupter.cc new file mode 100644 index 0000000000..7f53b60995 --- /dev/null +++ b/ortools/math_opt/core/solve_interrupter.cc @@ -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 +#include +#include +#include +#include + +#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 diff --git a/ortools/math_opt/core/solve_interrupter.h b/ortools/math_opt/core/solve_interrupter.h new file mode 100644 index 0000000000..2fb265ba37 --- /dev/null +++ b/ortools/math_opt/core/solve_interrupter.h @@ -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 +#include +#include +#include +#include + +#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; + + 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 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 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 callback_id_; +}; + +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_CORE_SOLVE_INTERRUPTER_H_ diff --git a/ortools/math_opt/core/solver.cc b/ortools/math_opt/core/solver.cc index f72ca3886d..ca9fc62245 100644 --- a/ortools/math_opt/core/solver.cc +++ b/ortools/math_opt/core/solver.cc @@ -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 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 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::New(solver_type, model, init_args)); + return solver->Solve(solve_args); +} + Solver::Solver(std::unique_ptr 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> 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 Solver::Solve( - const SolveParametersProto& parameters, - const ModelSolveParametersProto& model_parameters, - const CallbackRegistrationProto& callback_registration, - const Callback user_cb) { +absl::StatusOr 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 { 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 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 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 diff --git a/ortools/math_opt/core/solver.h b/ortools/math_opt/core/solver.h index 9ffc01bce2..0675fde220 100644 --- a/ortools/math_opt/core/solver.h +++ b/ortools/math_opt/core/solver.h @@ -17,9 +17,12 @@ #include #include +#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; + // 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 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> 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 Solve( - const SolveParametersProto& parameters, - const ModelSolveParametersProto& model_parameters = {}, - const CallbackRegistrationProto& callback_registration = {}, - Callback user_cb = nullptr); + absl::StatusOr 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 underlying_solver, ModelSummary model_summary); + // Mutex used to ensure that Solve() and Update() are not called concurrently. + absl::Mutex mutex_; + const std::unique_ptr 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 diff --git a/ortools/math_opt/core/solver_debug.cc b/ortools/math_opt/core/solver_debug.cc new file mode 100644 index 0000000000..b78f050d14 --- /dev/null +++ b/ortools/math_opt/core/solver_debug.cc @@ -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 +#include + +namespace operations_research { +namespace math_opt { +namespace internal { + +std::atomic debug_num_solver = 0; + +} // namespace internal +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/core/solver_debug.h b/ortools/math_opt/core/solver_debug.h new file mode 100644 index 0000000000..2d2d899e73 --- /dev/null +++ b/ortools/math_opt/core/solver_debug.h @@ -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 +#include + +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 debug_num_solver; + +} // namespace internal +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_CORE_SOLVER_DEBUG_H_ diff --git a/ortools/math_opt/core/solver_interface.cc b/ortools/math_opt/core/solver_interface.cc index ce74fba3d8..b6369c9811 100644 --- a/ortools/math_opt/core/solver_interface.cc +++ b/ortools/math_opt/core/solver_interface.cc @@ -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> 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> 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 AllSolversRegistry::RegisteredSolvers() const { - std::vector result; +std::vector AllSolversRegistry::RegisteredSolvers() const { + std::vector result; { const absl::MutexLock lock(&mutex_); for (const auto& kv_pair : registered_solvers_) { diff --git a/ortools/math_opt/core/solver_interface.h b/ortools/math_opt/core/solver_interface.h index e7439ae808..91a18eedfe 100644 --- a/ortools/math_opt/core/solver_interface.h +++ b/ortools/math_opt/core/solver_interface.h @@ -19,11 +19,15 @@ #include #include +#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&)>; + // 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>( - 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 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> 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 RegisteredSolvers() const; + std::vector 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 registered_solvers_; + absl::flat_hash_map + 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 { \ diff --git a/ortools/math_opt/core/sparse_collection_matchers.h b/ortools/math_opt/core/sparse_collection_matchers.h index 0c607c5af4..3dc66fe8a1 100644 --- a/ortools/math_opt/core/sparse_collection_matchers.h +++ b/ortools/math_opt/core/sparse_collection_matchers.h @@ -21,8 +21,8 @@ #include #include -#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" diff --git a/ortools/math_opt/core/sparse_submatrix.cc b/ortools/math_opt/core/sparse_submatrix.cc new file mode 100644 index 0000000000..96c610d39c --- /dev/null +++ b/ortools/math_opt/core/sparse_submatrix.cc @@ -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 +#include +#include +#include +#include + +#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 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 end_row_id, const int64_t start_col_id, + const std::optional 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>> 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> filtered_columns; + for (const auto& [row_id, column_values] : submatrix_by_rows) { + for (const auto [column_id, value] : column_values) { + SparseVector& 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>> 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>& lhs, + const std::pair>& rhs) { + return lhs.first < rhs.first; + }); + + return sorted_filtered_columns; +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/core/sparse_submatrix.h b/ortools/math_opt/core/sparse_submatrix.h new file mode 100644 index 0000000000..706897af08 --- /dev/null +++ b/ortools/math_opt/core/sparse_submatrix.h @@ -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 +#include +#include +#include + +#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>>; + +// 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 end_row_id, int64_t start_col_id, + std::optional 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>> TransposeSparseSubmatrix( + const SparseSubmatrixRowsView& submatrix_by_rows); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_CORE_SPARSE_SUBMATRIX_H_ diff --git a/ortools/math_opt/core/sparse_vector.h b/ortools/math_opt/core/sparse_vector.h new file mode 100644 index 0000000000..962af727b5 --- /dev/null +++ b/ortools/math_opt/core/sparse_vector.h @@ -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 +#include + +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 +struct SparseVector { + // Should be sorted (in increasing order) with all elements distinct. + std::vector ids; + + // Must have equal length to ids. + std::vector values; +}; + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_CORE_SPARSE_VECTOR_H_ diff --git a/ortools/math_opt/core/sparse_vector_view.h b/ortools/math_opt/core/sparse_vector_view.h index 3345081ff2..680c6c8fc7 100644 --- a/ortools/math_opt/core/sparse_vector_view.h +++ b/ortools/math_opt/core/sparse_vector_view.h @@ -57,6 +57,8 @@ #include "absl/container/flat_hash_map.h" #include "absl/types/span.h" #include "ortools/base/map_util.h" +#include "ortools/math_opt/core/arrow_operator_proxy.h" // IWYU pragma: export +#include "ortools/math_opt/core/sparse_vector.h" #include "ortools/math_opt/sparse_containers.pb.h" namespace operations_research { @@ -96,8 +98,10 @@ class SparseVectorView { using difference_type = int; using iterator_category = std::forward_iterator_tag; - value_type operator*() const; + reference operator*() const; + inline internal::ArrowOperatorProxy 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 MakeView(const SparseVectorProto& sparse_vector) { return SparseVectorView(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 +SparseVectorView MakeView(const SparseVector& sparse_vector) { + return SparseVectorView(sparse_vector.ids, sparse_vector.values); +} + //////////////////////////////////////////////////////////////////////////////// // Inline implementations //////////////////////////////////////////////////////////////////////////////// @@ -174,11 +185,18 @@ SparseVectorView::const_iterator::const_iterator( } template -typename SparseVectorView::const_iterator::value_type +typename SparseVectorView::const_iterator::reference SparseVectorView::const_iterator::operator*() const { return {view_->ids(index_), view_->values(index_)}; } +template +internal::ArrowOperatorProxy< + typename SparseVectorView::const_iterator::reference> +SparseVectorView::const_iterator::operator->() const { + return internal::ArrowOperatorProxy(**this); +} + template typename SparseVectorView::const_iterator& SparseVectorView::const_iterator::operator++() { @@ -188,10 +206,16 @@ SparseVectorView::const_iterator::operator++() { } template -bool SparseVectorView::const_iterator::operator!=( +bool SparseVectorView::const_iterator::operator==( const const_iterator& other) const { DCHECK_EQ(view_, other.view_); - return index_ != other.index_; + return index_ == other.index_; +} + +template +bool SparseVectorView::const_iterator::operator!=( + const const_iterator& other) const { + return !(*this == other); } template diff --git a/ortools/math_opt/cpp/BUILD.bazel b/ortools/math_opt/cpp/BUILD.bazel index 78e9788140..76b1075242 100644 --- a/ortools/math_opt/cpp/BUILD.bazel +++ b/ortools/math_opt/cpp/BUILD.bazel @@ -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", + ], +) diff --git a/ortools/math_opt/cpp/callback.cc b/ortools/math_opt/cpp/callback.cc index c023a8c660..1bf53c8de2 100644 --- a/ortools/math_opt/cpp/callback.cc +++ b/ortools/math_opt/cpp/callback.cc @@ -15,18 +15,18 @@ #include #include -#include #include #include #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> 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 -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 Enum::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 Enum::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( - model, MakeView(proto.primal_solution().variable_values()) - .as_map()); + storage, MakeView(proto.primal_solution_vector()).as_map()); } 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& 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) { diff --git a/ortools/math_opt/cpp/callback.h b/ortools/math_opt/cpp/callback.h index ada012e27e..629983f051 100644 --- a/ortools/math_opt/cpp/callback.h +++ b/ortools/math_opt/cpp/callback.h @@ -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 = opt.Solve({}, {}, cb_reb, cb); +// absl::StatusOr 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 +#include +#include #include #include #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; - // 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> 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 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 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 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 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 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> 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; diff --git a/ortools/math_opt/cpp/enums.h b/ortools/math_opt/cpp/enums.h new file mode 100644 index 0000000000..40098da3c9 --- /dev/null +++ b/ortools/math_opt/cpp/enums.h @@ -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 +// Enum::ToOptString(MyEnum value) { +// switch (value) { +// case MyEnum::kFirstValue: +// return "first_value"; +// case MyEnum::kSecondValue: +// return "second_value"; +// } +// return std::nullopt; +// } +// +// absl::Span Enum::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 Enum::AllValues() +// * optional EnumFromString(string_view) +// * string_view EnumToString(MyEnum) +// * optional EnumToOptString(MyEnum) +// * optional EnumFromProto(MyEnumProto) +// * MyEnumProto EnumToProto(optional) +// * MyEnumProto EnumToProto(MyEnum) +// * operator<<(MyEnum) +// * operator<<(std::optional) +// +// 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 +#include + +#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::AllValues()) { +// ... +// } +// +// // Parsing a flag as an enum. +// const std::optional 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 opt_solver_type = ...; +// const SolverTypeProto solver_type_proto = EnumToProto(opt_solver_type); +// +// // Conversion from Proto. +// const SolverTypeProto solver_type_proto = ...; +// const std::optional opt_solver_type = +// EnumFromProto(solver_type_proto); +// +// Implementation note: don't specialize directly and instead use the +// MATH_OPT_DEFINE_ENUM macro. +template +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 ToOptString(E value); + + // Returns all possible values of the enum. + static absl::Span 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 +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::kProtoUnspecifiedValue if the input is std::nullopt. +template +typename Enum::Proto EnumToProto(const std::optional 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 with the other overload. +template +typename Enum::Proto EnumToProto(const E value); + +// Returns the C++ enum that matches the input Proto enum, returns +// std::nullopt if the input is kProtoUnspecifiedValue. +template +std::optional::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 +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 +std::optional 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 +std::optional EnumFromString(const absl::string_view str); + +// Overload of operator<< for enum types that implements Enum. +// +// It calls EnumToOptString(), printing the returned value if not nullopt. When +// nullopt it prints the enum numeric value instead. +template +std::ostream& operator<<(std::ostream& out, const E value); + +// Overload of operator<< for std::optional when Enum is implemented. +// +// When the value is nullopt, it prints "", else it prints the enum +// value. +template +std::ostream& operator<<(std::ostream& out, const std::optional value); + +//////////////////////////////////////////////////////////////////////////////// +// Template functions implementations after this point. +//////////////////////////////////////////////////////////////////////////////// + +template +typename Enum::Proto EnumToProto(const std::optional value) { + return value ? static_cast::Proto>(*value) + : Enum::kProtoUnspecifiedValue; +} + +template +typename Enum::Proto EnumToProto(const E value) { + return EnumToProto(std::make_optional(value)); +} + +template +std::optional::Cpp> EnumFromProto(const P proto_value) { + if (proto_value == Enum::Cpp>::kProtoUnspecifiedValue) { + return std::nullopt; + } + return static_cast::Cpp>(proto_value); +} + +template +absl::string_view EnumToString(const E value) { + std::optional opt_str = Enum::ToOptString(value); + CHECK(opt_str.has_value()) + << "invalid value: " << static_cast>(value); + return *opt_str; +} + +template +std::optional EnumToOptString(const E value) { + return Enum::ToOptString(value); +} + +template +std::optional EnumFromString(const absl::string_view str) { + for (const E value : Enum::AllValues()) { + if (EnumToOptString(value) == str) { + return value; + } + } + return std::nullopt; +} + +template . + typename = std::enable_if_t::kIsImplemented>> +std::ostream& operator<<(std::ostream& out, const E value) { + const std::optional opt_str = EnumToOptString(value); + if (opt_str.has_value()) { + out << *opt_str; + } else { + out << ">(value) + << ")>"; + } + return out; +} + +template . + typename = std::enable_if_t::kIsImplemented>> +std::ostream& operator<<(std::ostream& out, const std::optional opt_value) { + if (opt_value.has_value()) { + out << *opt_value; + } else { + out << ""; + } + 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::ToOptString(CppEnum value) { +// absl::Span Enum::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 { \ + static constexpr bool kIsImplemented = true; \ + using Proto = CppEnum##Proto; \ + static constexpr Proto kProtoUnspecifiedValue = proto_unspecified_value; \ + static std::optional ToOptString(CppEnum value); \ + static absl::Span AllValues(); \ + }; \ + \ + template <> \ + struct EnumProto { \ + 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_ diff --git a/ortools/math_opt/cpp/id_map.h b/ortools/math_opt/cpp/id_map.h index b7854b4fe2..851d69a578 100644 --- a/ortools/math_opt/cpp/id_map.h +++ b/ortools/math_opt/cpp/id_map.h @@ -17,7 +17,6 @@ #include #include -#include #include #include @@ -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 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 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 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& a, IdMap& b) { template typename IdMap::reference IdMap::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::const_iterator::const_iterator(const iterator& non_const_iterator) template typename IdMap::const_iterator::reference IdMap::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::const_iterator::const_iterator( //////////////////////////////////////////////////////////////////////////////// template -IdMap::IdMap(IndexedModel* model, StorageType values) - : model_(model), map_(std::move(values)) { +IdMap::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::iterator IdMap::end() { template void IdMap::clear() { - model_ = nullptr; + storage_ = nullptr; map_.clear(); } @@ -447,7 +446,7 @@ int IdMap::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 void IdMap::erase(const const_iterator pos) { map_.erase(pos.storage_iterator_); if (map_.empty()) { - model_ = nullptr; + storage_ = nullptr; } } @@ -465,7 +464,7 @@ typename IdMap::iterator IdMap::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::iterator IdMap::erase(const const_iterator first, template void IdMap::swap(IdMap& other) { using std::swap; - swap(model_, other.model_); + swap(storage_, other.storage_); swap(map_, other.map_); } @@ -581,7 +580,7 @@ std::vector IdMap::SortedKeys() const { std::vector 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 IdMap::SortedIds() const { template void IdMap::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 void IdMap::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 void IdMap::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()); } } diff --git a/ortools/math_opt/cpp/id_set.h b/ortools/math_opt/cpp/id_set.h index e909421b5f..7f72bdecf7 100644 --- a/ortools/math_opt/cpp/id_set.h +++ b/ortools/math_opt/cpp/id_set.h @@ -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 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& a, IdSet& b) { template typename IdSet::const_iterator::reference IdSet::const_iterator::operator*() const { - return K(set_->model_, *storage_iterator_); + return K(set_->storage_, *storage_iterator_); } template @@ -216,10 +216,10 @@ IdSet::const_iterator::const_iterator( //////////////////////////////////////////////////////////////////////////////// template -IdSet::IdSet(IndexedModel* model, StorageType values) - : model_(model), set_(std::move(values)) { +IdSet::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::const_iterator IdSet::end() const { template void IdSet::clear() { - model_ = nullptr; + storage_ = nullptr; set_.clear(); } @@ -293,7 +293,7 @@ int IdSet::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 void IdSet::erase(const const_iterator pos) { set_.erase(pos.storage_iterator_); if (set_.empty()) { - model_ = nullptr; + storage_ = nullptr; } } @@ -311,7 +311,7 @@ typename IdSet::const_iterator IdSet::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::const_iterator IdSet::erase(const const_iterator first, template void IdSet::swap(IdSet& other) { using std::swap; - swap(model_, other.model_); + swap(storage_, other.storage_); swap(set_, other.set_); } @@ -353,18 +353,18 @@ IdSet::equal_range(const K& k) const { template void IdSet::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 void IdSet::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; } } diff --git a/ortools/math_opt/cpp/key_types.h b/ortools/math_opt/cpp/key_types.h index ccdd0fc0ed..0db0bb0004 100644 --- a/ortools/math_opt/cpp/key_types.h +++ b/ortools/math_opt/cpp/key_types.h @@ -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 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 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; } } } diff --git a/ortools/math_opt/cpp/linear_constraint.cc b/ortools/math_opt/cpp/linear_constraint.cc deleted file mode 100644 index 709db38894..0000000000 --- a/ortools/math_opt/cpp/linear_constraint.cc +++ /dev/null @@ -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 -#include - -#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 LinearConstraint::RowNonzeros() const { - std::vector 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 diff --git a/ortools/math_opt/cpp/linear_constraint.h b/ortools/math_opt/cpp/linear_constraint.h index f16c151436..fed08dece7 100644 --- a/ortools/math_opt/cpp/linear_constraint.h +++ b/ortools/math_opt/cpp/linear_constraint.h @@ -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 #include -#include #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 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 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 diff --git a/ortools/math_opt/cpp/map_filter.h b/ortools/math_opt/cpp/map_filter.h index 7ed0f2f2a8..01f6265f2d 100644 --- a/ortools/math_opt/cpp/map_filter.h +++ b/ortools/math_opt/cpp/map_filter.h @@ -16,10 +16,10 @@ #include #include +#include -#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> filtered_keys; + std::optional> 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 MakeKeepKeysFilter(std::initializer_list keys) { //////////////////////////////////////////////////////////////////////////////// template -IndexedModel* MapFilter::model() const { - return filtered_keys ? filtered_keys->model() : nullptr; +const ModelStorage* MapFilter::storage() const { + return filtered_keys ? filtered_keys->storage() : nullptr; } template diff --git a/ortools/math_opt/cpp/matchers.cc b/ortools/math_opt/cpp/matchers.cc new file mode 100644 index 0000000000..8c28fb7321 --- /dev/null +++ b/ortools/math_opt/cpp/matchers.cc @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#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 +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 +Printer Print(const T& t) { + return Printer(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 +class IdMapMatcher : public MatcherInterface> { + public: + IdMapMatcher(IdMap 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 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 expected_; + const bool all_keys_; + const double tolerance_; +}; + +} // namespace + +Matcher> IsNearlySubsetOf(VariableMap expected, + double tolerance) { + return Matcher>(new IdMapMatcher( + std::move(expected), /*all_keys=*/false, tolerance)); +} + +Matcher> IsNear(VariableMap expected, + const double tolerance) { + return Matcher>(new IdMapMatcher( + std::move(expected), /*all_keys=*/true, tolerance)); +} + +Matcher> IsNearlySubsetOf( + LinearConstraintMap expected, double tolerance) { + return Matcher>( + new IdMapMatcher(std::move(expected), + /*all_keys=*/false, tolerance)); +} + +Matcher> IsNear( + LinearConstraintMap expected, const double tolerance) { + return Matcher>( + new IdMapMatcher(std::move(expected), /*all_keys=*/true, + tolerance)); +} + +//////////////////////////////////////////////////////////////////////////////// +// Matcher helpers +//////////////////////////////////////////////////////////////////////////////// + +namespace { + +template +class RayMatcher : public MatcherInterface { + 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 IsNear(double expected, const double tolerance) { + return DoubleNear(expected, tolerance); +} + +template +Matcher> IsNear(std::optional expected, + const double tolerance) { + if (expected.has_value()) { + return Optional(IsNear(*expected, tolerance)); + } + return testing::Eq(std::nullopt); +} + +// Custom std::optional for basis. +Matcher> BasisIs(const std::optional& expected) { + if (expected.has_value()) { + return Optional(BasisIs(*expected)); + } + return testing::Eq(std::nullopt); +} + +testing::Matcher> IsNear( + const std::vector& expected_solutions, + const SolutionMatcherOptions options) { + if (expected_solutions.empty()) { + return IsEmpty(); + } + std::vector> matchers; + for (const Solution& sol : expected_solutions) { + matchers.push_back(IsNear(sol, options)); + } + return ::testing::ElementsAreArray(matchers); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +// Matchers for Solutions +//////////////////////////////////////////////////////////////////////////////// + +Matcher 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 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 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 IsNear(Solution expected, + const SolutionMatcherOptions options) { + std::vector> 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 +double InfinityNorm(const IdMap& 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 { + 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 IsNear(PrimalRay expected, const double tolerance) { + return Matcher( + new PrimalRayMatcher(std::move(expected), tolerance)); +} + +Matcher PrimalRayIsNear(VariableMap 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 { + 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 IsNear(DualRay expected, const double tolerance) { + return Matcher(new DualRayMatcher(std::move(expected), tolerance)); +} + +//////////////////////////////////////////////////////////////////////////////// +// SolveResult termination reason matchers +//////////////////////////////////////////////////////////////////////////////// + +Matcher TerminatesWithOneOf( + const std::vector& allowed, const bool check_warnings) { + std::vector> 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 TerminatesWith(const TerminationReason expected, + const bool check_warnings) { + std::vector> 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 TerminatesWithLimit(Limit expected, + bool allow_limit_undetermined, + bool check_warnings) { + std::vector> 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 +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 +std::string MatcherToString(const Matcher& matcher, bool negate) { + return MatcherToStringImpl(matcher, negate); +} + +// Polymorphic matchers do not always define DescribeTo, see +// The type may not be a matcher, but it will implement DescribeTo. +template +std::string MatcherToString(const ::testing::PolymorphicMatcher& 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 IsOptimal(const std::optional expected_objective, + const bool check_warnings, + const double tolerance) { + std::vector> 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 IsOptimalWithSolution( + const double expected_objective, + const VariableMap 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 IsOptimalWithDualSolution( + const double expected_objective, + const LinearConstraintMap expected_dual_values, + const VariableMap 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 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 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 HasPrimalRay(PrimalRay expected, const double tolerance) { + return ::testing::Field("primal_rays", &SolveResult::primal_rays, + Contains(IsNear(std::move(expected), tolerance))); +} + +Matcher HasPrimalRay(VariableMap expected_vars, + const double tolerance) { + PrimalRay ray; + ray.variable_values = std::move(expected_vars); + return HasPrimalRay(std::move(ray), tolerance); +} + +Matcher 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 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> CheckSolutions( + const std::vector& 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 +Matcher> AnyRayNear( + const std::vector& expected_rays, const double tolerance) { + std::vector> matchers; + for (const RayType& ray : expected_rays) { + matchers.push_back(IsNear(ray, tolerance)); + } + return ::testing::Contains(::testing::AnyOfArray(matchers)); +} + +template +Matcher> AllRaysNear( + const std::vector& expected_rays, const double tolerance) { + std::vector> matchers; + for (const RayType& ray : expected_rays) { + matchers.push_back(IsNear(ray, tolerance)); + } + return ::testing::UnorderedElementsAreArray(matchers); +} + +template +Matcher> CheckRays( + const std::vector& 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 IsConsistentWith( + const SolveResult& expected, const SolveResultMatcherOptions& options) { + std::vector> 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 DidUpdate() { + return ::testing::Field("did_update", + &IncrementalSolver::UpdateResult::did_update, + ::testing::IsTrue()); +} + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/matchers.h b/ortools/math_opt/cpp/matchers.h new file mode 100644 index 0000000000..28455d5a09 --- /dev/null +++ b/ortools/math_opt/cpp/matchers.h @@ -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, 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 +#include + +#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 and IdMap +//////////////////////////////////////////////////////////////////////////////// + +// Checks that the maps have identical keys and values within tolerance. +testing::Matcher> IsNear( + VariableMap 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> IsNearlySubsetOf( + VariableMap expected, double tolerance = kMatcherDefaultTolerance); + +// Checks that the maps have identical keys and values within tolerance. +testing::Matcher> IsNear( + LinearConstraintMap 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> IsNearlySubsetOf( + LinearConstraintMap 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 IsNear(Solution expected, + SolutionMatcherOptions options = {}); + +// Checks variables match and variable/objective values are within tolerance and +// feasibility statuses are identical. +testing::Matcher IsNear( + PrimalSolution expected, double tolerance = kMatcherDefaultTolerance); + +// Checks dual variables, reduced costs and objective are within tolerance and +// feasibility statuses are identical. +testing::Matcher IsNear( + DualSolution expected, double tolerance = kMatcherDefaultTolerance); + +testing::Matcher BasisIs(const Basis& expected); + +//////////////////////////////////////////////////////////////////////////////// +// Matchers for a Rays +//////////////////////////////////////////////////////////////////////////////// + +// Checks variables match and that after rescaling, variable values are within +// tolerance. +testing::Matcher IsNear(PrimalRay expected, + double tolerance = kMatcherDefaultTolerance); + +// Checks variables match and that after rescaling, variable values are within +// tolerance. +testing::Matcher PrimalRayIsNear( + VariableMap 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 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 IsOptimal( + std::optional expected_objective = std::nullopt, + bool check_warnings = true, double tolerance = kMatcherDefaultTolerance); + +testing::Matcher IsOptimalWithSolution( + double expected_objective, VariableMap expected_variable_values, + bool check_warnings = true, double tolerance = kMatcherDefaultTolerance); + +testing::Matcher IsOptimalWithDualSolution( + double expected_objective, LinearConstraintMap expected_dual_values, + VariableMap 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 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 TerminatesWithOneOf( + const std::vector& 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 TerminatesWithLimit( + Limit expected, bool allow_limit_undetermined = false, + bool check_warnings = true); + +// SolveResult has a primal solution matching expected within tolerance. +testing::Matcher HasSolution( + PrimalSolution expected, double tolerance = kMatcherDefaultTolerance); + +// SolveResult has a dual solution matching expected within +// tolerance. +testing::Matcher HasDualSolution( + DualSolution expected, double tolerance = kMatcherDefaultTolerance); + +// Actual SolveResult contains a primal ray that matches expected within +// tolerance. +testing::Matcher 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 HasPrimalRay( + VariableMap expected_vars, + double tolerance = kMatcherDefaultTolerance); + +// Actual SolveResult contains a dual ray that matches expected within +// tolerance. +testing::Matcher 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 IsConsistentWith( + const SolveResult& expected, const SolveResultMatcherOptions& options = {}); + +//////////////////////////////////////////////////////////////////////////////// +// Rarely used +//////////////////////////////////////////////////////////////////////////////// + +// Actual UpdateResult.did_update is true. +testing::Matcher 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 +void PrintTo(const IdMap& 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_ diff --git a/ortools/math_opt/cpp/math_opt.cc b/ortools/math_opt/cpp/math_opt.cc deleted file mode 100644 index ef8439b269..0000000000 --- a/ortools/math_opt/cpp/math_opt.cc +++ /dev/null @@ -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 -#include -#include -#include -#include - -#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 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 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 MathOpt::Variables() { - std::vector 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 MathOpt::SortedVariables() { - std::vector 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 MathOpt::ColumnNonzeros(const Variable variable) { - std::vector result; - for (const LinearConstraintId constraint : - model_->linear_constraints_with_variable(variable.typed_id())) { - result.push_back(LinearConstraint(model_.get(), constraint)); - } - return result; -} - -std::vector MathOpt::LinearConstraints() { - std::vector 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 MathOpt::SortedLinearConstraints() { - std::vector 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 diff --git a/ortools/math_opt/cpp/math_opt.h b/ortools/math_opt/cpp/math_opt.h index 647e95fa5d..2ea13704bc 100644 --- a/ortools/math_opt/cpp/math_opt.h +++ b/ortools/math_opt/cpp/math_opt.h @@ -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::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 -#include -#include -#include -#include - -#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; - - 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 Variables(); - - // Returns all the existing (created and not deleted) variables in the model, - // sorted by id. - std::vector SortedVariables(); - - std::vector 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 LinearConstraints(); - - // Returns all the existing (created and not deleted) linear constraints in - // the model sorted by id. - std::vector 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 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 model_; - std::unique_ptr solver_; - std::unique_ptr 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(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_ diff --git a/ortools/math_opt/cpp/model.cc b/ortools/math_opt/cpp/model.cc new file mode 100644 index 0000000000..f707c6648c --- /dev/null +++ b/ortools/math_opt/cpp/model.cc @@ -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 +#include +#include +#include + +#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> Model::FromModelProto( + const ModelProto& model_proto) { + ASSIGN_OR_RETURN(std::unique_ptr storage, + ModelStorage::FromModelProto(model_proto)); + return std::make_unique(std::move(storage)); +} + +Model::Model(const absl::string_view name) + : storage_(std::make_shared(name)) {} + +Model::Model(std::unique_ptr storage) + : storage_(std::move(storage)) {} + +std::unique_ptr Model::Clone() const { + return std::make_unique(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 Model::Variables() const { + std::vector result; + result.reserve(storage()->num_variables()); + for (const VariableId var_id : storage()->variables()) { + result.push_back(Variable(storage(), var_id)); + } + return result; +} + +std::vector Model::SortedVariables() const { + std::vector 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 Model::ColumnNonzeros(const Variable variable) { + CheckModel(variable.storage()); + std::vector result; + for (const LinearConstraintId constraint : + storage()->linear_constraints_with_variable(variable.typed_id())) { + result.push_back(LinearConstraint(storage(), constraint)); + } + return result; +} + +std::vector Model::RowNonzeros(const LinearConstraint constraint) { + CheckModel(constraint.storage()); + std::vector 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 Model::LinearConstraints() const { + std::vector 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 Model::SortedLinearConstraints() const { + std::vector 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 Model::NewUpdateTracker() const { + return std::make_unique(storage_); +} + +absl::Status Model::ApplyUpdateProto(const ModelUpdateProto& update_proto) { + return storage()->ApplyUpdateProto(update_proto); +} + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/model.h b/ortools/math_opt/cpp/model.h new file mode 100644 index 0000000000..edecacb61e --- /dev/null +++ b/ortools/math_opt/cpp/model.h @@ -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 +#include +#include + +#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::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::FromModelProto(model_proto)); + static absl::StatusOr> 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 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 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 Variables() const; + + // Returns all the existing (created and not deleted) variables in the model, + // sorted by id. + std::vector SortedVariables() const; + + std::vector 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 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 LinearConstraints() const; + + // Returns all the existing (created and not deleted) linear constraints in + // the model sorted by id. + std::vector 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 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 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_ diff --git a/ortools/math_opt/cpp/model_solve_parameters.cc b/ortools/math_opt/cpp/model_solve_parameters.cc index 4259e25173..5f1af03250 100644 --- a/ortools/math_opt/cpp/model_solve_parameters.cc +++ b/ortools/math_opt/cpp/model_solve_parameters.cc @@ -20,10 +20,10 @@ #include #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(); - parameters.dual_variables_filter = MakeSkipAllFilter(); + parameters.dual_values_filter = MakeSkipAllFilter(); + parameters.reduced_costs_filter = MakeSkipAllFilter(); return parameters; } @@ -47,25 +46,24 @@ ModelSolveParameters ModelSolveParameters::OnlySomePrimalVariables( return OnlySomePrimalVariables>(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* 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* 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* const variable_ids = + hint.mutable_variable_values()->mutable_ids(); + RepeatedField* 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* const variable_ids = + ret.mutable_branching_priorities()->mutable_ids(); + RepeatedField* 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; diff --git a/ortools/math_opt/cpp/model_solve_parameters.h b/ortools/math_opt/cpp/model_solve_parameters.h index 065ec353fe..8aab2de1ae 100644 --- a/ortools/math_opt/cpp/model_solve_parameters.h +++ b/ortools/math_opt/cpp/model_solve_parameters.h @@ -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 + #include #include +#include -#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 or and // std::initializer_list (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 variables); - // The filter that is applied to variable_values of both - // Result::PrimalSolution and Result::PrimalRay. - MapFilter primal_variables_filter; + // The filter that is applied to variable_values of both PrimalSolution and + // PrimalRay. + MapFilter variable_values_filter; - // The filter that is applied to dual_values of Result::DualSolution and - // Result::DualRay. - MapFilter dual_linear_constraints_filter; + // The filter that is applied to dual_values of DualSolution and DualRay. + MapFilter dual_values_filter; - // The filter that is applied to reduced_costs of Result::DualSolution and - // Result::DualRay. - MapFilter dual_variables_filter; + // The filter that is applied to reduced_costs of DualSolution and DualRay. + MapFilter reduced_costs_filter; // Optional initial basis for warm starting simplex LP solvers. If set, it is // expected to be valid. - absl::optional initial_basis; + std::optional initial_basis; + + struct SolutionHint { + SolutionHint() = default; + VariableMap 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 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 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 ModelSolveParameters ModelSolveParameters::OnlySomePrimalVariables( const Collection& variables) { ModelSolveParameters parameters = OnlyPrimalVariables(); - parameters.primal_variables_filter = MakeKeepKeysFilter(variables); + parameters.variable_values_filter = MakeKeepKeysFilter(variables); return parameters; } diff --git a/ortools/math_opt/cpp/objective.cc b/ortools/math_opt/cpp/objective.cc deleted file mode 100644 index 4607899b1e..0000000000 --- a/ortools/math_opt/cpp/objective.cc +++ /dev/null @@ -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 diff --git a/ortools/math_opt/cpp/objective.h b/ortools/math_opt/cpp/objective.h deleted file mode 100644 index 37b5cdf6ef..0000000000 --- a/ortools/math_opt/cpp/objective.h +++ /dev/null @@ -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_ diff --git a/ortools/math_opt/cpp/parameters.cc b/ortools/math_opt/cpp/parameters.cc new file mode 100644 index 0000000000..f06993148e --- /dev/null +++ b/ortools/math_opt/cpp/parameters.cc @@ -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 +#include + +#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 Enum::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 Enum::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(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 Enum::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 Enum::AllValues() { + static constexpr LPAlgorithm kLPAlgorithmValues[] = { + LPAlgorithm::kPrimalSimplex, + LPAlgorithm::kDualSimplex, + LPAlgorithm::kBarrier, + }; + return absl::MakeConstSpan(kLPAlgorithmValues); +} + +std::optional Enum::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 Enum::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::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 diff --git a/ortools/math_opt/cpp/parameters.h b/ortools/math_opt/cpp/parameters.h new file mode 100644 index 0000000000..f19e6cc590 --- /dev/null +++ b/ortools/math_opt/cpp/parameters.h @@ -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 +#include + +#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. 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 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 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 relative_gap_limit; + std::optional absolute_gap_limit; + + // If unset, use the solver default. If set, it must be >= 1. + std::optional 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 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 lp_algorithm; + + // Effort on simplifying the problem before starting the main algorithm, or + // the solver default effort level if unset. + std::optional 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 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 heuristics; + + // Effort in rescaling the problem to improve numerical stability, or the + // solver default effort level if unset. + std::optional 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 FromProto( + const SolveParametersProto& proto); +}; + +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_CPP_PARAMETERS_H_ diff --git a/ortools/math_opt/cpp/result.cc b/ortools/math_opt/cpp/result.cc deleted file mode 100644 index a9429ac7aa..0000000000 --- a/ortools/math_opt/cpp/result.cc +++ /dev/null @@ -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 -#include -#include - -#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 diff --git a/ortools/math_opt/cpp/result.h b/ortools/math_opt/cpp/result.h deleted file mode 100644 index 0d607c8f20..0000000000 --- a/ortools/math_opt/cpp/result.h +++ /dev/null @@ -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 -#include - -#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 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 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 dual_values; - VariableMap 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 dual_values; - VariableMap 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 constraint_status; - VariableMap 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& 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& 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& 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& 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& 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& 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& constraint_status() const { - CHECK(has_basis()); - return basis[0].constraint_status; - } - - // The variable basis status for the first primal/dual pair. - const VariableMap& variable_status() const { - CHECK(has_basis()); - return basis[0].variable_status; - } - - std::vector 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 primal_solutions; - std::vector primal_rays; - - // Dual solutions should be ordered best objective value first. - std::vector dual_solutions; - std::vector 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; - - // 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_ diff --git a/ortools/math_opt/cpp/solution.cc b/ortools/math_opt/cpp/solution.cc new file mode 100644 index 0000000000..d0091939e4 --- /dev/null +++ b/ortools/math_opt/cpp/solution.cc @@ -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 +#include + +#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 +IdMap ValuesFrom(const ModelStorage* const model, + const SparseDoubleVectorProto& vars_proto) { + return IdMap( + model, MakeView(vars_proto).as_map()); +} + +template +IdMap BasisValues( + const ModelStorage* const model, + const SparseBasisStatusVector& basis_proto) { + absl::flat_hash_map 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 basis_status = + EnumFromProto(static_cast(basis_status_proto)); + CHECK(basis_status.has_value()); + id_map[static_cast(id)] = *basis_status; + } + return IdMap(model, std::move(id_map)); +} + +} // namespace + +std::optional Enum::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 Enum::AllValues() { + static constexpr SolutionStatus kSolutionStatusValues[] = { + SolutionStatus::kFeasible, + SolutionStatus::kInfeasible, + SolutionStatus::kUndetermined, + }; + return absl::MakeConstSpan(kSolutionStatusValues); +} + +std::optional Enum::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 Enum::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(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 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(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(model, dual_solution_proto.dual_values()); + dual_solution.reduced_costs = + ValuesFrom(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 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(model, dual_ray_proto.dual_values()), + .reduced_costs = + ValuesFrom(model, dual_ray_proto.reduced_costs())}; +} + +Basis Basis::FromProto(const ModelStorage* model, + const BasisProto& basis_proto) { + Basis basis; + basis.constraint_status = + BasisValues(model, basis_proto.constraint_status()); + basis.variable_status = + BasisValues(model, basis_proto.variable_status()); + // TODO(b/209014770): consider adding a function to simplify this pattern. + const std::optional 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 diff --git a/ortools/math_opt/cpp/solution.h b/ortools/math_opt/cpp/solution.h new file mode 100644 index 0000000000..8c5df30da0 --- /dev/null +++ b/ortools/math_opt/cpp/solution.h @@ -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 + +#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 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 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 dual_values; + VariableMap reduced_costs; + std::optional 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 dual_values; + VariableMap 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 constraint_status; + VariableMap 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 primal_solution; + std::optional dual_solution; + std::optional basis; +}; + +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_CPP_SOLUTION_H_ diff --git a/ortools/math_opt/cpp/solve.cc b/ortools/math_opt/cpp/solve.cc new file mode 100644 index 0000000000..2ee67aa66e --- /dev/null +++ b/ortools/math_opt/cpp/solve.cc @@ -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 +#include +#include +#include +#include +#include + +#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 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& 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 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::New(EnumToProto(solver_type), model.ExportModel(), + ToSolverInitArgs(init_args))); + return CallSolve(*solver, model.storage(), solve_args); +} + +absl::StatusOr> IncrementalSolver::New( + Model& model, const SolverType solver_type, SolverInitArguments arguments) { + std::unique_ptr update_tracker = model.NewUpdateTracker(); + ASSIGN_OR_RETURN( + std::unique_ptr solver, + Solver::New(EnumToProto(solver_type), update_tracker->ExportModel(), + ToSolverInitArgs(arguments))); + return absl::WrapUnique( + 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 update_tracker, + std::unique_ptr 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 IncrementalSolver::Solve( + const SolveArguments& arguments) { + RETURN_IF_ERROR(Update().status()); + return SolveWithoutUpdate(arguments); +} + +absl::StatusOr IncrementalSolver::Update() { + std::optional 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 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(output_stream, prefix); + return + [=](const std::vector& messages) { impl->Call(messages); }; +} + +MessageCallback InfoLoggerMessageCallback(const absl::string_view prefix, + const absl::SourceLocation loc) { + return [=](const std::vector& 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& messages) { + for (const std::string& message : messages) { + VLOG(level).AtLocation(loc) << prefix << message; + } + }; +} + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/solve.h b/ortools/math_opt/cpp/solve.h new file mode 100644 index 0000000000..521d54f24d --- /dev/null +++ b/ortools/math_opt/cpp/solve.h @@ -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 +#include +#include +#include +#include +#include +#include + +#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&)>; + +// 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 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(); + // + // // 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 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 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 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 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> 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 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 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 SolveWithoutUpdate( + const SolveArguments& arguments = {}) const; + + private: + IncrementalSolver(SolverType solver_type, SolverInitArguments init_args, + const ModelStorage* expected_storage, + std::unique_ptr update_tracker, + std::unique_ptr solver); + + const SolverType solver_type_; + const SolverInitArguments init_args_; + const ModelStorage* const expected_storage_; + const std::unique_ptr update_tracker_; + std::unique_ptr solver_; +}; + +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_CPP_SOLVE_H_ diff --git a/ortools/math_opt/cpp/solve_result.cc b/ortools/math_opt/cpp/solve_result.cc new file mode 100644 index 0000000000..bae20a0de4 --- /dev/null +++ b/ortools/math_opt/cpp/solve_result.cc @@ -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 +#include +#include +#include + +#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 +absl::flat_hash_map BasisStatusMapFromProto( + const absl::flat_hash_map& proto_map) { + absl::flat_hash_map cpp_map; + for (const auto& [id, proto_value] : proto_map) { + const std::optional opt_status = EnumFromProto(proto_value); + CHECK(opt_status.has_value()); + cpp_map.emplace(id, *opt_status); + } + return cpp_map; +} + +} // namespace + +std::optional Enum::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 Enum::AllValues() { + static constexpr FeasibilityStatus kFeasibilityStatus[] = { + FeasibilityStatus::kUndetermined, + FeasibilityStatus::kFeasible, + FeasibilityStatus::kInfeasible, + }; + return absl::MakeConstSpan(kFeasibilityStatus); +} + +std::optional Enum::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 Enum::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 Enum::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 Enum::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 opt_limit = + EnumFromProto(termination_proto.limit()); + CHECK(opt_limit.has_value()); + return Termination(*opt_limit, termination_proto.detail()); + } + + const std::optional 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 opt_primal_status = + EnumFromProto(problem_status_proto.primal_status()); + const std::optional 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& SolveResult::variable_values() const { + CHECK(has_primal_feasible_solution()); + return solutions[0].primal_solution->variable_values; +} + +const VariableMap& 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& SolveResult::dual_values() const { + CHECK(has_dual_feasible_solution()); + return solutions[0].dual_solution->dual_values; +} + +const VariableMap& SolveResult::reduced_costs() const { + CHECK(has_dual_feasible_solution()); + return solutions[0].dual_solution->reduced_costs; +} +const LinearConstraintMap& SolveResult::ray_dual_values() const { + CHECK(has_dual_ray()); + return dual_rays[0].dual_values; +} + +const VariableMap& 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& SolveResult::constraint_status() const { + CHECK(has_basis()); + return solutions[0].basis->constraint_status; +} + +const VariableMap& SolveResult::variable_status() const { + CHECK(has_basis()); + return solutions[0].basis->variable_status; +} + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/solve_result.h b/ortools/math_opt/cpp/solve_result.h new file mode 100644 index 0000000000..2c59da83c7 --- /dev/null +++ b/ortools/math_opt/cpp/solve_result.h @@ -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 +#include +#include + +#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; + + // 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 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 solutions; + + // Directions of unbounded primal improvement, or equivalently, dual + // infeasibility certificates. Typically provided for TerminationReasons + // kUnbounded and kInfeasibleOrUnbounded. + std::vector primal_rays; + + // Directions of unbounded dual improvement, or equivalently, primal + // infeasibility certificates. Typically provided for TerminationReason + // kInfeasible. + std::vector 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& 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& 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& dual_values() const; + + // The reduced from the best dual solution. Will CHECK fail if there + // are no dual solutions. + const VariableMap& 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& ray_dual_values() const; + + // The reduced from the first dual ray. Will CHECK fail if there + // are no dual rays. + const VariableMap& 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& constraint_status() const; + + // The variable basis status for the first primal/dual pair. + const VariableMap& variable_status() const; +}; + +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_CPP_SOLVE_RESULT_H_ diff --git a/ortools/math_opt/cpp/streamable_solver_init_arguments.cc b/ortools/math_opt/cpp/streamable_solver_init_arguments.cc new file mode 100644 index 0000000000..0e03bc59ca --- /dev/null +++ b/ortools/math_opt/cpp/streamable_solver_init_arguments.cc @@ -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 +#include + +#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 diff --git a/ortools/math_opt/cpp/streamable_solver_init_arguments.h b/ortools/math_opt/cpp/streamable_solver_init_arguments.h new file mode 100644 index 0000000000..fa85fcba14 --- /dev/null +++ b/ortools/math_opt/cpp/streamable_solver_init_arguments.h @@ -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 +#include +#include + +#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 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 cp_sat; + std::optional gscip; + std::optional glop; + std::optional glpk; + std::optional 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_ diff --git a/ortools/math_opt/cpp/update_tracker.cc b/ortools/math_opt/cpp/update_tracker.cc new file mode 100644 index 0000000000..1a60022ec0 --- /dev/null +++ b/ortools/math_opt/cpp/update_tracker.cc @@ -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 +#include + +#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 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& storage) + : storage_(ABSL_DIE_IF_NULL(storage)), + update_tracker_(storage->NewUpdateTracker()) {} + +std::optional UpdateTracker::ExportModelUpdate() { + const std::shared_ptr storage = storage_.lock(); + CHECK(storage != nullptr) << internal::kModelIsDestroyed; + return storage->ExportModelUpdate(update_tracker_); +} + +void UpdateTracker::Checkpoint() { + const std::shared_ptr storage = storage_.lock(); + CHECK(storage != nullptr) << internal::kModelIsDestroyed; + storage->Checkpoint(update_tracker_); +} + +ModelProto UpdateTracker::ExportModel() const { + const std::shared_ptr storage = storage_.lock(); + CHECK(storage != nullptr) << internal::kModelIsDestroyed; + return storage->ExportModel(); +} + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/cpp/update_tracker.h b/ortools/math_opt/cpp/update_tracker.h new file mode 100644 index 0000000000..59bab1fff0 --- /dev/null +++ b/ortools/math_opt/cpp/update_tracker.h @@ -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 +#include + +#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 update_tracker = +// model.NewUpdateTracker(); +// +// model.AddVariable(0.0, 1.0, true, "y"); +// model.set_maximize(true); +// +// const std::optional 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& 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 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 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_ diff --git a/ortools/math_opt/cpp/variable_and_expressions.cc b/ortools/math_opt/cpp/variable_and_expressions.cc index 0d4f7f44e1..e70878cdc8 100644 --- a/ortools/math_opt/cpp/variable_and_expressions.cc +++ b/ortools/math_opt/cpp/variable_and_expressions.cc @@ -18,15 +18,16 @@ #include #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& 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& 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& 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& 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 diff --git a/ortools/math_opt/cpp/variable_and_expressions.h b/ortools/math_opt/cpp/variable_and_expressions.h index eaf3198f8e..405b50c9b9 100644 --- a/ortools/math_opt/cpp/variable_and_expressions.h +++ b/ortools/math_opt/cpp/variable_and_expressions.h @@ -11,12 +11,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -// An object oriented wrapper for variables in IndexedModel with support for -// arithmetic operations to build linear expressions and express linear -// constraints. +// An object oriented wrapper for variables in ModelStorage (used internally by +// Model) with support for arithmetic operations to build linear expressions and +// express linear constraints. // // Types are: -// - Variable: a reference to a variable of an IndexedModel. +// - Variable: a reference to a variable of an ModelStorage. // // - LinearExpression: a weighted sum of variables with an optional offset; // something like `3*x + 2*y + 5`. @@ -33,6 +33,16 @@ // expression with two bounds, an upper bound and a lower bound. For example // `2 <= 3*x + 2*y + 5 <= 3`; or `4 >= 3*x + 2*y + 5 >= 1`. // +// - QuadraticTermKey: a key used internally to represent a pair of Variables. +// +// - QuadraticTerm: a term representing the product of a scalar coefficient +// and two Variables (possibly the same); something like `2*x*y` or `3*x*x`. +// It is used as an intermediate in the arithmetic operations that build +// quadratic expressions. +// +// - QuadraticExpression: a sum of a quadratic terms, linear terms, and a +// scalar offset; something like `3*x*y + 2*x*x + 4x + 5`. +// // - VariablesEquality: the result of comparing two Variable instances with // the == operator. For example `a == b`. This intermediate class support // implicit conversion to both bool and BoundedLinearExpression types. This @@ -90,22 +100,26 @@ #include "ortools/base/logging.h" #include "absl/container/flat_hash_map.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" // IWYU pragma: export namespace operations_research { namespace math_opt { -// A value type that references a variable from IndexedModel. Usually this type +// Forward declaration needed by Variable. +class LinearExpression; + +// A value type that references a variable from ModelStorage. Usually this type // is passed by copy. class Variable { public: // The typed integer used for ids. using IdType = VariableId; - // Usually users will obtain variables using MathOpt::AddVariable(). There - // should be little for users to build this object from an IndexedModel. - inline Variable(IndexedModel* model, VariableId id); + // Usually users will obtain variables using Model::AddVariable(). There + // should be little for users to build this object from an ModelStorage. + inline Variable(const ModelStorage* storage, VariableId id); // Each call to AddVariable will produce Variables id() increasing by one, // starting at zero. Deleted ids are NOT reused. Thus, if no variables are @@ -113,25 +127,21 @@ class Variable { inline int64_t id() const; inline VariableId typed_id() const; - inline IndexedModel* model() const; + inline const ModelStorage* storage() const; inline double lower_bound() const; inline double upper_bound() const; inline bool is_integer() 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; - inline void set_is_integer(bool is_integer) const; - inline void set_integer() const; - inline void set_continuous() const; - template friend H AbslHashValue(H h, const Variable& variable); friend std::ostream& operator<<(std::ostream& ostr, const Variable& variable); + inline LinearExpression operator-() const; + private: - IndexedModel* model_; + const ModelStorage* storage_; VariableId id_; }; @@ -164,11 +174,14 @@ inline LinearTerm operator*(Variable variable, double coefficient); inline LinearTerm operator/(LinearTerm term, double coefficient); inline LinearTerm operator/(Variable variable, double coefficient); +// Forward declaration so that we may add it as a friend to LinearExpression +class QuadraticExpression; + // This class represents a sum of variables multiplied by coefficient and an // optional offset constant. For example: "3*x + 2*y + 5". // // All operations, including constructor, will raise an assertion if the -// operands involve variables from different MathOpt objects. +// operands involve variables from different Model objects. // // Contrary to Variable type, expressions owns the linear expression their // represent. Hence they are usually passed by reference to prevent unnecessary @@ -181,14 +194,14 @@ class LinearExpression { public: // For unit testing purpose, we define optional counters. We have to // explicitly define default constructors in that case. -#ifndef USE_LINEAR_EXPRESSION_COUNTERS +#ifndef MATH_OPT_USE_EXPRESSION_COUNTERS LinearExpression() = default; -#else // USE_LINEAR_EXPRESSION_COUNTERS +#else // MATH_OPT_USE_EXPRESSION_COUNTERS LinearExpression(); LinearExpression(const LinearExpression& other); LinearExpression(LinearExpression&& other); LinearExpression& operator=(const LinearExpression& other); -#endif // USE_LINEAR_EXPRESSION_COUNTERS +#endif // MATH_OPT_USE_EXPRESSION_COUNTERS // Usually users should use the overloads of operators to build linear // expressions. For example, assuming `x` and `y` are Variable, then `x + 2*y // + 5` will build a LinearExpression automatically. @@ -285,33 +298,34 @@ class LinearExpression { // Compute the numeric value of this expression when variables are substituted // by their values in variable_values. // - // Will CHECK fail the underlying model is different or if a variable in - // terms() is missing from variables_values. + // Will CHECK fail the underlying model storage is different or if a variable + // in terms() is missing from variables_values. double Evaluate(const VariableMap& variable_values) const; // Compute the numeric value of this expression when variables are substituted // by their values in variable_values, or zero if missing from the map. // - // Will CHECK fail the underlying model is different. + // Will CHECK fail the underlying model storage is different. double EvaluateWithDefaultZero( const VariableMap& variable_values) const; - inline IndexedModel* model() const; + inline const ModelStorage* storage() const; inline const absl::flat_hash_map& raw_terms() const; -#ifdef USE_LINEAR_EXPRESSION_COUNTERS +#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS static thread_local int num_calls_default_constructor_; static thread_local int num_calls_copy_constructor_; static thread_local int num_calls_move_constructor_; static thread_local int num_calls_initializer_list_constructor_; // Reset all counters in the current thread to 0. static void ResetCounters(); -#endif // USE_LINEAR_EXPRESSION_COUNTERS +#endif // MATH_OPT_USE_EXPRESSION_COUNTERS private: friend LinearExpression operator-(LinearExpression expr); friend std::ostream& operator<<(std::ostream& ostr, const LinearExpression& expression); + friend QuadraticExpression; VariableMap terms_; double offset_ = 0.0; @@ -581,49 +595,318 @@ inline BoundedLinearExpression operator==(double lhs, const LinearTerm& rhs); inline BoundedLinearExpression operator==(Variable lhs, double rhs); inline BoundedLinearExpression operator==(double lhs, Variable rhs); +// Id type used for quadratic terms, i.e. products of two variables. +using QuadraticProductId = std::pair; + +// Couples a QuadraticProductId with a ModelStorage, for use with IdMaps. +// Namely, this key type satisfies the requirements stated in key_types.h. +// Invariant: +// * variable_ids_.first <= variable_ids_.second. The constructor will +// silently correct this if not satisfied by the inputs. +class QuadraticTermKey { + public: + // NOTE: this definition is for use by IdMap; clients should not rely upon it. + using IdType = QuadraticProductId; + + // NOTE: This constructor will silently re-order the passed id so that, upon + // exiting the constructor, variable_ids_.first <= variable_ids_.second. + inline QuadraticTermKey(const ModelStorage* storage, QuadraticProductId id); + // NOTE: This constructor will CHECK fail if the variable models do not agree, + // i.e. first_variable.storage() != second_variable.storage(). It will also + // silently re-order the passed id so that, upon exiting the constructor, + // variable_ids_.first <= variable_ids_.second. + inline QuadraticTermKey(Variable first_variable, Variable second_variable); + + inline QuadraticProductId typed_id() const; + inline const ModelStorage* storage() const; + + template + friend H AbslHashValue(H h, const QuadraticTermKey& key); + + private: + const ModelStorage* storage_; + QuadraticProductId variable_ids_; +}; + +inline bool operator==(const QuadraticTermKey lhs, const QuadraticTermKey rhs); +inline bool operator!=(const QuadraticTermKey lhs, const QuadraticTermKey rhs); + +// Represents a quadratic term in a sum: coefficient * variable_1 * variable_2. +// Invariant: +// * first_variable.storage() == second_variable.storage(). The constructor +// will CHECK fail if not satisfied. +class QuadraticTerm { + public: + QuadraticTerm() = delete; + // NOTE: This will CHECK fail if + // first_variable.storage() != second_variable.storage(). + inline QuadraticTerm(Variable first_variable, Variable second_variable, + double coefficient); + + inline double coefficient() const; + inline Variable first_variable() const; + inline Variable second_variable() const; + + // This is useful for working with IdMaps + inline QuadraticTermKey GetKey() const; + + inline QuadraticTerm& operator*=(double value); + inline QuadraticTerm& operator/=(double value); + + private: + friend QuadraticTerm operator-(QuadraticTerm term); + friend QuadraticTerm operator*(double lhs, QuadraticTerm rhs); + friend QuadraticTerm operator*(QuadraticTerm lhs, double rhs); + friend QuadraticTerm operator/(QuadraticTerm lhs, double rhs); + + Variable first_variable_; + Variable second_variable_; + double coefficient_; +}; +// We declare those operator overloads that result in a QuadraticTerm, stated in +// lexicographic ordering based on lhs type, rhs type): +inline QuadraticTerm operator-(QuadraticTerm term); +inline QuadraticTerm operator*(double lhs, QuadraticTerm rhs); +inline QuadraticTerm operator*(Variable lhs, Variable rhs); +inline QuadraticTerm operator*(Variable lhs, LinearTerm rhs); +inline QuadraticTerm operator*(LinearTerm lhs, Variable rhs); +inline QuadraticTerm operator*(LinearTerm lhs, LinearTerm rhs); +inline QuadraticTerm operator*(QuadraticTerm lhs, double rhs); +inline QuadraticTerm operator/(QuadraticTerm lhs, double rhs); + +// Implements the API of std::unordered_map, but forbids +// QuadraticTermKeys from different models in the same map. +template +using QuadraticTermMap = IdMap; + +// This class represents a sum of quadratic terms, linear terms, and constant +// offset. For example: "3*x*y + 2*x + 1". +// +// Mixing terms involving variables from different ModelStorage objects will +// lead to CHECK fails, including from the constructors. +// +// The type owns the associated data representing the terms, and so should +// usually be passed by (const) reference to avoid unnecessary copies. +// +// Note for implementers: Care must be taken to ensure that +// linear_terms_.storage() and quadratic_terms_.storage() do not disagree. That +// is, it is forbidden that both are non-null and not equal. Use +// CheckModelsAgree() and the initializer_list constructor to enforce this +// invariant in any class or friend method. +class QuadraticExpression { + public: +#ifndef MATH_OPT_USE_EXPRESSION_COUNTERS + QuadraticExpression() = default; +#else // MATH_OPT_USE_EXPRESSION_COUNTERS + QuadraticExpression(); + QuadraticExpression(const QuadraticExpression& other); + QuadraticExpression(QuadraticExpression&& other); + QuadraticExpression& operator=(const QuadraticExpression& other); +#endif // MATH_OPT_USE_EXPRESSION_COUNTERS + // Users should prefer the default constructor and operator overloads to build + // expressions. + inline QuadraticExpression( + std::initializer_list quadratic_terms, + std::initializer_list linear_terms, double offset); + inline QuadraticExpression(double offset); // NOLINT + inline QuadraticExpression(Variable variable); // NOLINT + inline QuadraticExpression(const LinearTerm& term); // NOLINT + inline QuadraticExpression(LinearExpression expr); // NOLINT + inline QuadraticExpression(const QuadraticTerm& term); // NOLINT + + inline double offset() const; + inline const VariableMap& linear_terms() const; + inline const QuadraticTermMap& quadratic_terms() const; + + inline const absl::flat_hash_map& raw_linear_terms() + const; + inline const absl::flat_hash_map& + raw_quadratic_terms() const; + + inline QuadraticExpression& operator+=(double value); + inline QuadraticExpression& operator+=(Variable variable); + inline QuadraticExpression& operator+=(const LinearTerm& term); + inline QuadraticExpression& operator+=(const LinearExpression& expr); + inline QuadraticExpression& operator+=(const QuadraticTerm& term); + inline QuadraticExpression& operator+=(const QuadraticExpression& expr); + inline QuadraticExpression& operator-=(double value); + inline QuadraticExpression& operator-=(Variable variable); + inline QuadraticExpression& operator-=(const LinearTerm& term); + inline QuadraticExpression& operator-=(const LinearExpression& expr); + inline QuadraticExpression& operator-=(const QuadraticTerm& term); + inline QuadraticExpression& operator-=(const QuadraticExpression& expr); + inline QuadraticExpression& operator*=(double value); + inline QuadraticExpression& operator/=(double value); + + // Compute the numeric value of this expression when variables are substituted + // by their values in variable_values. + // + // Will CHECK fail if the underlying model storage is different, or if a + // variable in linear_terms() or quadratic_terms() is missing from + // variables_values. + double Evaluate(const VariableMap& variable_values) const; + + // Compute the numeric value of this expression when variables are substituted + // by their values in variable_values, or zero if missing from the map. + // + // Will CHECK fail the underlying model storage is different. + double EvaluateWithDefaultZero( + const VariableMap& variable_values) const; + + inline const ModelStorage* storage() const; + +#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS + static thread_local int num_calls_default_constructor_; + static thread_local int num_calls_copy_constructor_; + static thread_local int num_calls_move_constructor_; + static thread_local int num_calls_initializer_list_constructor_; + static thread_local int num_calls_linear_expression_constructor_; + // Reset all counters in the current thread to 0. + static void ResetCounters(); +#endif // MATH_OPT_USE_EXPRESSION_COUNTERS + + private: + friend QuadraticExpression operator-(QuadraticExpression expr); + friend std::ostream& operator<<(std::ostream& ostr, + const QuadraticExpression& expr); + inline void CheckModelsAgree(); + + QuadraticTermMap quadratic_terms_; + VariableMap linear_terms_; + double offset_ = 0.0; +}; + +// We have 6 types that we must consider arithmetic among: +// 1. double (scalar value) +// 2. Variable (affine value) +// 3. LinearTerm (affine value) +// 4. LinearExpression (affine value) +// 5. QuadraticTerm (quadratic value) +// 6. QuadraticExpression (quadratic value) +// We care only about those methods that result in a QuadraticExpression. For +// example, multiplying a linear value with a linear value, or adding a scalar +// to a quadratic value. The single unary method is: +inline QuadraticExpression operator-(QuadraticExpression expr); + +// The binary methods, listed in lexicographic order based on +// (operator, lhs type #, rhs type #), with the type #s are listed above, are: +inline QuadraticExpression operator+(double lhs, const QuadraticTerm& rhs); +inline QuadraticExpression operator+(double lhs, QuadraticExpression rhs); +inline QuadraticExpression operator+(Variable lhs, const QuadraticTerm& rhs); +inline QuadraticExpression operator+(Variable lhs, QuadraticExpression rhs); +inline QuadraticExpression operator+(const LinearTerm& lhs, + const QuadraticTerm& rhs); +inline QuadraticExpression operator+(const LinearTerm& lhs, + QuadraticExpression rhs); +inline QuadraticExpression operator+(LinearExpression lhs, + const QuadraticTerm& rhs); +inline QuadraticExpression operator+(const LinearExpression& lhs, + QuadraticExpression rhs); +inline QuadraticExpression operator+(const QuadraticTerm& lhs, double rhs); +inline QuadraticExpression operator+(const QuadraticTerm& lhs, Variable rhs); +inline QuadraticExpression operator+(const QuadraticTerm& lhs, + const LinearTerm& rhs); +inline QuadraticExpression operator+(const QuadraticTerm& lhs, + LinearExpression rhs); +inline QuadraticExpression operator+(const QuadraticTerm& lhs, + const QuadraticTerm& rhs); +inline QuadraticExpression operator+(const QuadraticTerm& lhs, + QuadraticExpression rhs); +inline QuadraticExpression operator+(QuadraticExpression lhs, double rhs); +inline QuadraticExpression operator+(QuadraticExpression lhs, Variable rhs); +inline QuadraticExpression operator+(QuadraticExpression lhs, + const LinearTerm& rhs); +inline QuadraticExpression operator+(QuadraticExpression lhs, + const LinearExpression& rhs); +inline QuadraticExpression operator+(QuadraticExpression lhs, + const QuadraticTerm& rhs); +inline QuadraticExpression operator+(QuadraticExpression lhs, + const QuadraticExpression& rhs); + +inline QuadraticExpression operator-(double lhs, const QuadraticTerm& rhs); +inline QuadraticExpression operator-(double lhs, QuadraticExpression rhs); +inline QuadraticExpression operator-(Variable lhs, const QuadraticTerm& rhs); +inline QuadraticExpression operator-(Variable lhs, QuadraticExpression rhs); +inline QuadraticExpression operator-(const LinearTerm& lhs, + const QuadraticTerm& rhs); +inline QuadraticExpression operator-(const LinearTerm& lhs, + QuadraticExpression rhs); +inline QuadraticExpression operator-(LinearExpression lhs, + const QuadraticTerm& rhs); +inline QuadraticExpression operator-(const LinearExpression& lhs, + QuadraticExpression rhs); +inline QuadraticExpression operator-(const QuadraticTerm& lhs, double rhs); +inline QuadraticExpression operator-(const QuadraticTerm& lhs, Variable rhs); +inline QuadraticExpression operator-(const QuadraticTerm& lhs, + const LinearTerm& rhs); +inline QuadraticExpression operator-(const QuadraticTerm& lhs, + LinearExpression rhs); +inline QuadraticExpression operator-(const QuadraticTerm& lhs, + const QuadraticTerm& rhs); +inline QuadraticExpression operator-(const QuadraticTerm& lhs, + QuadraticExpression rhs); +inline QuadraticExpression operator-(QuadraticExpression lhs, double rhs); +inline QuadraticExpression operator-(QuadraticExpression lhs, Variable rhs); +inline QuadraticExpression operator-(QuadraticExpression lhs, + const LinearTerm& rhs); +inline QuadraticExpression operator-(QuadraticExpression lhs, + const LinearExpression& rhs); +inline QuadraticExpression operator-(QuadraticExpression lhs, + const QuadraticTerm& rhs); +inline QuadraticExpression operator-(QuadraticExpression lhs, + const QuadraticExpression& rhs); + +inline QuadraticExpression operator*(double lhs, QuadraticExpression rhs); +inline QuadraticExpression operator*(Variable lhs, const LinearExpression& rhs); +inline QuadraticExpression operator*(LinearTerm lhs, + const LinearExpression& rhs); +inline QuadraticExpression operator*(const LinearExpression& lhs, Variable rhs); +inline QuadraticExpression operator*(const LinearExpression& lhs, + LinearTerm rhs); +inline QuadraticExpression operator*(const LinearExpression& lhs, + const LinearExpression& rhs); +inline QuadraticExpression operator*(QuadraticExpression lhs, double rhs); + +inline QuadraticExpression operator/(QuadraticExpression lhs, double rhs); + +//////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +// Inline function implementations ///////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// -// Inline function implementations //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // Variable //////////////////////////////////////////////////////////////////////////////// -Variable::Variable(IndexedModel* const model, const VariableId id) - : model_(model), id_(id) { - DCHECK(model != nullptr); +Variable::Variable(const ModelStorage* const storage, const VariableId id) + : storage_(storage), id_(id) { + DCHECK(storage != nullptr); } int64_t Variable::id() const { return id_.value(); } VariableId Variable::typed_id() const { return id_; } -IndexedModel* Variable::model() const { return model_; } +const ModelStorage* Variable::storage() const { return storage_; } double Variable::lower_bound() const { - return model_->variable_lower_bound(id_); + return storage_->variable_lower_bound(id_); } -double Variable::upper_bound() const { - return model_->variable_upper_bound(id_); -} -bool Variable::is_integer() const { return model_->is_variable_integer(id_); } -const std::string& Variable::name() const { return model_->variable_name(id_); } -void Variable::set_lower_bound(const double lower_bound) const { - model_->set_variable_lower_bound(id_, lower_bound); +double Variable::upper_bound() const { + return storage_->variable_upper_bound(id_); } -void Variable::set_upper_bound(const double upper_bound) const { - model_->set_variable_upper_bound(id_, upper_bound); + +bool Variable::is_integer() const { return storage_->is_variable_integer(id_); } + +const std::string& Variable::name() const { + return storage_->variable_name(id_); } -void Variable::set_is_integer(const bool is_integer) const { - model_->set_variable_is_integer(id_, is_integer); -} -void Variable::set_integer() const { set_is_integer(true); } -void Variable::set_continuous() const { set_is_integer(false); } template H AbslHashValue(H h, const Variable& variable) { - return H::combine(std::move(h), variable.id_.value(), variable.model_); + return H::combine(std::move(h), variable.id_.value(), variable.storage_); } std::ostream& operator<<(std::ostream& ostr, const Variable& variable) { @@ -633,6 +916,10 @@ std::ostream& operator<<(std::ostream& ostr, const Variable& variable) { return ostr; } +LinearExpression Variable::operator-() const { + return LinearExpression({LinearTerm(*this, -1.0)}, 0.0); +} + //////////////////////////////////////////////////////////////////////////////// // LinearTerm //////////////////////////////////////////////////////////////////////////////// @@ -688,9 +975,9 @@ LinearTerm operator/(Variable variable, const double coefficient) { LinearExpression::LinearExpression(std::initializer_list terms, const double offset) : offset_(offset) { -#ifdef USE_LINEAR_EXPRESSION_COUNTERS +#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS ++num_calls_initializer_list_constructor_; -#endif // USE_LINEAR_EXPRESSION_COUNTERS +#endif // MATH_OPT_USE_EXPRESSION_COUNTERS for (const auto& term : terms) { // The same variable may appear multiple times in the input list; we must // accumulate the coefficients. @@ -962,7 +1249,9 @@ const VariableMap& LinearExpression::terms() const { return terms_; } double LinearExpression::offset() const { return offset_; } -IndexedModel* LinearExpression::model() const { return terms_.model(); } +const ModelStorage* LinearExpression::storage() const { + return terms_.storage(); +} const absl::flat_hash_map& LinearExpression::raw_terms() const { @@ -979,7 +1268,7 @@ VariablesEquality::VariablesEquality(Variable lhs, Variable rhs) : lhs(std::move(lhs)), rhs(std::move(rhs)) {} inline VariablesEquality::operator bool() const { - return lhs.typed_id() == rhs.typed_id() && lhs.model() == rhs.model(); + return lhs.typed_id() == rhs.typed_id() && lhs.storage() == rhs.storage(); } } // namespace internal @@ -1305,6 +1594,637 @@ BoundedLinearExpression operator==(const double lhs, const Variable rhs) { return lhs == LinearTerm(rhs, 1.0); } +//////////////////////////////////////////////////////////////////////////////// +// QuadraticTermKey +//////////////////////////////////////////////////////////////////////////////// + +QuadraticTermKey::QuadraticTermKey(const ModelStorage* storage, + const QuadraticProductId id) + : storage_(storage), variable_ids_(id) { + if (variable_ids_.first > variable_ids_.second) { + using std::swap; // go/using-std-swap + swap(variable_ids_.first, variable_ids_.second); + } +} + +QuadraticTermKey::QuadraticTermKey(const Variable first_variable, + const Variable second_variable) + : QuadraticTermKey(first_variable.storage(), {first_variable.typed_id(), + second_variable.typed_id()}) { + CHECK_EQ(first_variable.storage(), second_variable.storage()) + << internal::kObjectsFromOtherModelStorage; +} + +QuadraticProductId QuadraticTermKey::typed_id() const { return variable_ids_; } + +const ModelStorage* QuadraticTermKey::storage() const { return storage_; } + +template +H AbslHashValue(H h, const QuadraticTermKey& key) { + return H::combine(std::move(h), key.typed_id().first.value(), + key.typed_id().second.value(), key.storage()); +} + +bool operator==(const QuadraticTermKey lhs, const QuadraticTermKey rhs) { + return lhs.storage() == rhs.storage() && lhs.typed_id() == rhs.typed_id(); +} + +bool operator!=(const QuadraticTermKey lhs, const QuadraticTermKey rhs) { + return !(lhs == rhs); +} + +//////////////////////////////////////////////////////////////////////////////// +// QuadraticTerm (no arithmetic) +//////////////////////////////////////////////////////////////////////////////// + +QuadraticTerm::QuadraticTerm(Variable first_variable, Variable second_variable, + const double coefficient) + : first_variable_(std::move(first_variable)), + second_variable_(std::move(second_variable)), + coefficient_(coefficient) { + CHECK_EQ(first_variable_.storage(), second_variable_.storage()) + << internal::kObjectsFromOtherModelStorage; +} + +double QuadraticTerm::coefficient() const { return coefficient_; } +Variable QuadraticTerm::first_variable() const { return first_variable_; } +Variable QuadraticTerm::second_variable() const { return second_variable_; } + +QuadraticTermKey QuadraticTerm::GetKey() const { + return QuadraticTermKey( + first_variable_.storage(), + std::make_pair(first_variable_.typed_id(), second_variable_.typed_id())); +} + +//////////////////////////////////////////////////////////////////////////////// +// QuadraticExpression (no arithmetic) +//////////////////////////////////////////////////////////////////////////////// + +QuadraticExpression::QuadraticExpression( + const std::initializer_list quadratic_terms, + const std::initializer_list linear_terms, const double offset) + : offset_(offset) { +#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS + ++num_calls_initializer_list_constructor_; +#endif // MATH_OPT_USE_EXPRESSION_COUNTERS + for (const LinearTerm& term : linear_terms) { + linear_terms_[term.variable] += term.coefficient; + } + for (const QuadraticTerm& term : quadratic_terms) { + quadratic_terms_[term.GetKey()] += term.coefficient(); + } + CheckModelsAgree(); +} + +QuadraticExpression::QuadraticExpression(const double offset) + : QuadraticExpression({}, {}, offset) {} + +QuadraticExpression::QuadraticExpression(const Variable variable) + : QuadraticExpression({}, {LinearTerm(variable, 1.0)}, 0.0) {} + +QuadraticExpression::QuadraticExpression(const LinearTerm& term) + : QuadraticExpression({}, {term}, 0.0) {} + +QuadraticExpression::QuadraticExpression(LinearExpression expr) + : linear_terms_(std::move(expr.terms_)), + offset_(std::exchange(expr.offset_, 0.0)) { +#ifdef MATH_OPT_USE_EXPRESSION_COUNTERS + ++num_calls_linear_expression_constructor_; +#endif // MATH_OPT_USE_EXPRESSION_COUNTERS +} + +QuadraticExpression::QuadraticExpression(const QuadraticTerm& term) + : QuadraticExpression({term}, {}, 0.0) {} + +void QuadraticExpression::CheckModelsAgree() { + const ModelStorage* const quadratic_model = quadratic_terms_.storage(); + const ModelStorage* const linear_model = linear_terms_.storage(); + if ((linear_model != nullptr) && (quadratic_model != nullptr) && + (quadratic_model != linear_model)) { + LOG(FATAL) << internal::kObjectsFromOtherModelStorage; + } +} + +const ModelStorage* QuadraticExpression::storage() const { + if (quadratic_terms().storage()) { + return quadratic_terms().storage(); + } else { + return linear_terms().storage(); + } +} + +double QuadraticExpression::offset() const { return offset_; } + +const VariableMap& QuadraticExpression::linear_terms() const { + return linear_terms_; +} + +const QuadraticTermMap& QuadraticExpression::quadratic_terms() const { + return quadratic_terms_; +} + +const absl::flat_hash_map& +QuadraticExpression::raw_linear_terms() const { + return linear_terms_.raw_map(); +} + +const absl::flat_hash_map& +QuadraticExpression::raw_quadratic_terms() const { + return quadratic_terms_.raw_map(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Arithmetic operators (non-member). +// +// These are NOT required to explicitly CHECK that the underlying model storages +// agree between linear_terms_ and quadratic_terms_ unless they are a friend of +// QuadraticExpression. As much as possible, defer to the assignment operators +// and the initializer list constructor for QuadraticExpression. +//////////////////////////////////////////////////////////////////////////////// + +// ----------------------------- Addition (+) ---------------------------------- + +QuadraticExpression operator+(const double lhs, const QuadraticTerm& rhs) { + return QuadraticExpression({rhs}, {}, lhs); +} + +QuadraticExpression operator+(const double lhs, QuadraticExpression rhs) { + rhs += lhs; + return rhs; +} + +QuadraticExpression operator+(const Variable lhs, const QuadraticTerm& rhs) { + return QuadraticExpression({rhs}, {LinearTerm(lhs, 1.0)}, 0.0); +} + +QuadraticExpression operator+(const Variable lhs, QuadraticExpression rhs) { + rhs += LinearTerm(lhs, 1.0); + return rhs; +} + +QuadraticExpression operator+(const LinearTerm& lhs, const QuadraticTerm& rhs) { + return QuadraticExpression({rhs}, {lhs}, 0.0); +} + +QuadraticExpression operator+(const LinearTerm& lhs, QuadraticExpression rhs) { + rhs += lhs; + return rhs; +} + +QuadraticExpression operator+(LinearExpression lhs, const QuadraticTerm& rhs) { + QuadraticExpression expr(std::move(lhs)); + expr += rhs; + return expr; +} + +QuadraticExpression operator+(const LinearExpression& lhs, + QuadraticExpression rhs) { + rhs += lhs; + return rhs; +} + +QuadraticExpression operator+(const QuadraticTerm& lhs, const double rhs) { + return QuadraticExpression({lhs}, {}, rhs); +} + +QuadraticExpression operator+(const QuadraticTerm& lhs, const Variable rhs) { + return QuadraticExpression({lhs}, {LinearTerm(rhs, 1.0)}, 0.0); +} + +QuadraticExpression operator+(const QuadraticTerm& lhs, const LinearTerm& rhs) { + return QuadraticExpression({lhs}, {rhs}, 0.0); +} + +QuadraticExpression operator+(const QuadraticTerm& lhs, LinearExpression rhs) { + QuadraticExpression expr(std::move(rhs)); + expr += lhs; + return expr; +} + +QuadraticExpression operator+(const QuadraticTerm& lhs, + const QuadraticTerm& rhs) { + return QuadraticExpression({lhs, rhs}, {}, 0.0); +} + +QuadraticExpression operator+(const QuadraticTerm& lhs, + QuadraticExpression rhs) { + rhs += lhs; + return rhs; +} + +QuadraticExpression operator+(QuadraticExpression lhs, const double rhs) { + lhs += rhs; + return lhs; +} + +QuadraticExpression operator+(QuadraticExpression lhs, const Variable rhs) { + lhs += LinearTerm(rhs, 1.0); + return lhs; +} + +QuadraticExpression operator+(QuadraticExpression lhs, const LinearTerm& rhs) { + lhs += rhs; + return lhs; +} + +QuadraticExpression operator+(QuadraticExpression lhs, + const LinearExpression& rhs) { + lhs += rhs; + return lhs; +} + +QuadraticExpression operator+(QuadraticExpression lhs, + const QuadraticTerm& rhs) { + lhs += rhs; + return lhs; +} + +QuadraticExpression operator+(QuadraticExpression lhs, + const QuadraticExpression& rhs) { + lhs += rhs; + return lhs; +} + +// --------------------------- Subtraction (-) --------------------------------- + +// NOTE: A friend of QuadraticTerm, but does not touch variables +QuadraticTerm operator-(QuadraticTerm term) { + term.coefficient_ *= -1.0; + return term; +} + +// NOTE: A friend of QuadraticExpression, but does not touch variables +QuadraticExpression operator-(QuadraticExpression expr) { + expr.offset_ = -expr.offset_; + for (auto term : expr.linear_terms_) { + term.second = -term.second; + } + for (auto term : expr.quadratic_terms_) { + term.second = -term.second; + } + return expr; +} + +QuadraticExpression operator-(const double lhs, const QuadraticTerm& rhs) { + return QuadraticExpression({-rhs}, {}, lhs); +} + +QuadraticExpression operator-(const double lhs, QuadraticExpression rhs) { + auto expr = -std::move(rhs); + expr += lhs; + return expr; +} + +QuadraticExpression operator-(const Variable lhs, const QuadraticTerm& rhs) { + return QuadraticExpression({-rhs}, {LinearTerm(lhs, 1.0)}, 0.0); +} + +QuadraticExpression operator-(const Variable lhs, QuadraticExpression rhs) { + return LinearTerm(lhs, 1.0) - std::move(rhs); +} + +QuadraticExpression operator-(const LinearTerm& lhs, const QuadraticTerm& rhs) { + return QuadraticExpression({-rhs}, {lhs}, 0.0); +} + +QuadraticExpression operator-(const LinearTerm& lhs, QuadraticExpression rhs) { + auto expr = -std::move(rhs); + expr += lhs; + return expr; +} + +QuadraticExpression operator-(LinearExpression lhs, const QuadraticTerm& rhs) { + QuadraticExpression expr(std::move(lhs)); + expr -= rhs; + return expr; +} + +QuadraticExpression operator-(const LinearExpression& lhs, + QuadraticExpression rhs) { + auto expr = -std::move(rhs); + expr += lhs; + return expr; +} + +QuadraticExpression operator-(const QuadraticTerm& lhs, const double rhs) { + return QuadraticExpression({lhs}, {}, -rhs); +} + +QuadraticExpression operator-(const QuadraticTerm& lhs, const Variable rhs) { + return QuadraticExpression({lhs}, {LinearTerm(rhs, -1.0)}, 0.0); +} + +QuadraticExpression operator-(const QuadraticTerm& lhs, const LinearTerm& rhs) { + return QuadraticExpression({lhs}, {-rhs}, 0.0); +} + +QuadraticExpression operator-(const QuadraticTerm& lhs, LinearExpression rhs) { + QuadraticExpression expr(-std::move(rhs)); + expr += lhs; + return expr; +} + +QuadraticExpression operator-(const QuadraticTerm& lhs, + const QuadraticTerm& rhs) { + return QuadraticExpression({lhs, -rhs}, {}, 0.0); +} + +QuadraticExpression operator-(const QuadraticTerm& lhs, + QuadraticExpression rhs) { + rhs *= -1.0; + rhs += lhs; + return rhs; +} + +QuadraticExpression operator-(QuadraticExpression lhs, const double rhs) { + lhs -= rhs; + return lhs; +} + +// NOTE: Out-of-order for compilation purposes +QuadraticExpression operator-(QuadraticExpression lhs, const LinearTerm& rhs) { + lhs -= rhs; + return lhs; +} + +QuadraticExpression operator-(QuadraticExpression lhs, const Variable rhs) { + lhs -= LinearTerm(rhs, 1.0); + return lhs; +} + +// NOTE: operator-(QuadraticExpression, const LinearTerm) appears above + +QuadraticExpression operator-(QuadraticExpression lhs, + const LinearExpression& rhs) { + lhs -= rhs; + return lhs; +} + +QuadraticExpression operator-(QuadraticExpression lhs, + const QuadraticTerm& rhs) { + lhs -= rhs; + return lhs; +} + +QuadraticExpression operator-(QuadraticExpression lhs, + const QuadraticExpression& rhs) { + lhs -= rhs; + return lhs; +} + +// ---------------------------- Multiplication (*) ----------------------------- + +// NOTE: A friend of QuadraticTerm, but does not touch variables +QuadraticTerm operator*(const double lhs, QuadraticTerm rhs) { + rhs.coefficient_ *= lhs; + return rhs; +} + +QuadraticExpression operator*(const double lhs, QuadraticExpression rhs) { + rhs *= lhs; + return rhs; +} + +QuadraticTerm operator*(Variable lhs, Variable rhs) { + return QuadraticTerm(std::move(lhs), std::move(rhs), 1.0); +} + +QuadraticTerm operator*(Variable lhs, LinearTerm rhs) { + return QuadraticTerm(std::move(lhs), std::move(rhs.variable), + rhs.coefficient); +} + +QuadraticExpression operator*(Variable lhs, const LinearExpression& rhs) { + QuadraticExpression expr; + for (const auto& [var, coeff] : rhs.terms()) { + expr += QuadraticTerm(lhs, var, coeff); + } + if (rhs.offset() != 0) { + expr += LinearTerm(std::move(lhs), rhs.offset()); + } + return expr; +} + +QuadraticTerm operator*(LinearTerm lhs, Variable rhs) { + return QuadraticTerm(std::move(lhs.variable), std::move(rhs), + lhs.coefficient); +} + +QuadraticTerm operator*(LinearTerm lhs, LinearTerm rhs) { + return QuadraticTerm(std::move(lhs.variable), std::move(rhs.variable), + lhs.coefficient * rhs.coefficient); +} + +QuadraticExpression operator*(LinearTerm lhs, const LinearExpression& rhs) { + QuadraticExpression expr; + for (const auto& [var, coeff] : rhs.terms()) { + expr += QuadraticTerm(lhs.variable, var, lhs.coefficient * coeff); + } + if (rhs.offset() != 0) { + expr += LinearTerm(std::move(lhs.variable), lhs.coefficient * rhs.offset()); + } + return expr; +} + +QuadraticExpression operator*(const LinearExpression& lhs, Variable rhs) { + QuadraticExpression expr; + for (const auto& [var, coeff] : lhs.terms()) { + expr += QuadraticTerm(var, rhs, coeff); + } + if (lhs.offset() != 0) { + expr += LinearTerm(std::move(rhs), lhs.offset()); + } + return expr; +} + +QuadraticExpression operator*(const LinearExpression& lhs, LinearTerm rhs) { + QuadraticExpression expr; + for (const auto& [var, coeff] : lhs.terms()) { + expr += QuadraticTerm(var, rhs.variable, coeff * rhs.coefficient); + } + if (lhs.offset() != 0) { + expr += LinearTerm(std::move(rhs.variable), lhs.offset() * rhs.coefficient); + } + return expr; +} + +QuadraticExpression operator*(const LinearExpression& lhs, + const LinearExpression& rhs) { + QuadraticExpression expr = lhs.offset() * rhs.offset(); + if (rhs.offset() != 0) { + for (const auto& [var, coeff] : lhs.terms()) { + expr += LinearTerm(var, coeff * rhs.offset()); + } + } + if (lhs.offset() != 0) { + for (const auto& [var, coeff] : rhs.terms()) { + expr += LinearTerm(var, lhs.offset() * coeff); + } + } + for (const auto& [lhs_var, lhs_coeff] : lhs.terms()) { + for (const auto& [rhs_var, rhs_coeff] : rhs.terms()) { + expr += QuadraticTerm(lhs_var, rhs_var, lhs_coeff * rhs_coeff); + } + } + return expr; +} + +// NOTE: A friend of QuadraticTerm, but does not touch variables +QuadraticTerm operator*(QuadraticTerm lhs, const double rhs) { + lhs.coefficient_ *= rhs; + return lhs; +} + +QuadraticExpression operator*(QuadraticExpression lhs, const double rhs) { + lhs *= rhs; + return lhs; +} + +// ------------------------------- Division (/) -------------------------------- + +// NOTE: A friend of QuadraticTerm, but does not touch variables +QuadraticTerm operator/(QuadraticTerm lhs, const double rhs) { + lhs.coefficient_ /= rhs; + return lhs; +} + +QuadraticExpression operator/(QuadraticExpression lhs, const double rhs) { + lhs /= rhs; + return lhs; +} + +//////////////////////////////////////////////////////////////////////////////// +// In-place arithmetic operators. +// +// These must guarantee that the underlying model storages for linear_terms_ and +// quadratic_terms_ agree upon exit of the function, using CheckModelsAgree(), +// the list initializer constructor for QuadraticExpression, or similar logic. +//////////////////////////////////////////////////////////////////////////////// + +QuadraticExpression& QuadraticExpression::operator+=(const double value) { + offset_ += value; + // NOTE: Not touching terms, no need to check models + return *this; +} + +QuadraticExpression& QuadraticExpression::operator+=(const Variable variable) { + linear_terms_[variable] += 1; + CheckModelsAgree(); + return *this; +} + +QuadraticExpression& QuadraticExpression::operator+=(const LinearTerm& term) { + linear_terms_[term.variable] += term.coefficient; + CheckModelsAgree(); + return *this; +} + +QuadraticExpression& QuadraticExpression::operator+=( + const LinearExpression& expr) { + offset_ += expr.offset(); + linear_terms_.Add(expr.terms()); + CheckModelsAgree(); + return *this; +} + +QuadraticExpression& QuadraticExpression::operator+=( + const QuadraticTerm& term) { + quadratic_terms_[term.GetKey()] += term.coefficient(); + CheckModelsAgree(); + return *this; +} + +QuadraticExpression& QuadraticExpression::operator+=( + const QuadraticExpression& expr) { + offset_ += expr.offset(); + linear_terms_.Add(expr.linear_terms()); + quadratic_terms_.Add(expr.quadratic_terms()); + CheckModelsAgree(); + return *this; +} + +QuadraticExpression& QuadraticExpression::operator-=(const double value) { + offset_ -= value; + // NOTE: Not touching terms, no need to check models + return *this; +} + +QuadraticExpression& QuadraticExpression::operator-=(const Variable variable) { + linear_terms_[variable] -= 1; + CheckModelsAgree(); + return *this; +} + +QuadraticExpression& QuadraticExpression::operator-=(const LinearTerm& term) { + linear_terms_[term.variable] -= term.coefficient; + CheckModelsAgree(); + return *this; +} + +QuadraticExpression& QuadraticExpression::operator-=( + const LinearExpression& expr) { + offset_ -= expr.offset(); + linear_terms_.Subtract(expr.terms()); + CheckModelsAgree(); + return *this; +} + +QuadraticExpression& QuadraticExpression::operator-=( + const QuadraticTerm& term) { + quadratic_terms_[term.GetKey()] -= term.coefficient(); + CheckModelsAgree(); + return *this; +} + +QuadraticExpression& QuadraticExpression::operator-=( + const QuadraticExpression& expr) { + offset_ -= expr.offset(); + linear_terms_.Subtract(expr.linear_terms()); + quadratic_terms_.Subtract(expr.quadratic_terms()); + CheckModelsAgree(); + return *this; +} + +QuadraticTerm& QuadraticTerm::operator*=(const double value) { + coefficient_ *= value; + // NOTE: Not touching variables in term, just modifying coefficient, so no + // need to check that models agree. + return *this; +} + +QuadraticExpression& QuadraticExpression::operator*=(const double value) { + offset_ *= value; + for (auto term : linear_terms_) { + term.second *= value; + } + for (auto term : quadratic_terms_) { + term.second *= value; + } + // NOTE: Not adding/removing/altering variables in expression, just modifying + // coefficients, so no need to check that models agree. + return *this; +} + +QuadraticTerm& QuadraticTerm::operator/=(const double value) { + coefficient_ /= value; + // NOTE: Not touching variables in term, just modifying coefficient, so no + // need to check that models agree. + return *this; +} + +QuadraticExpression& QuadraticExpression::operator/=(const double value) { + offset_ /= value; + for (auto term : linear_terms_) { + term.second /= value; + } + for (auto term : quadratic_terms_) { + term.second /= value; + } + // NOTE: Not adding/removing/altering variables in expression, just modifying + // coefficients, so no need to check that models agree. + return *this; +} + } // namespace math_opt } // namespace operations_research diff --git a/ortools/math_opt/io/proto_converter.cc b/ortools/math_opt/io/proto_converter.cc index eaa8930fc2..f4ea8dd900 100644 --- a/ortools/math_opt/io/proto_converter.cc +++ b/ortools/math_opt/io/proto_converter.cc @@ -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, 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 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 = diff --git a/ortools/math_opt/io/proto_converter.h b/ortools/math_opt/io/proto_converter.h index ca1069b294..34af40fbf0 100644 --- a/ortools/math_opt/io/proto_converter.h +++ b/ortools/math_opt/io/proto_converter.h @@ -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 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 MathOptModelToMPModelProto( + const ModelProto& model); -} // namespace math_opt -} // namespace operations_research +} // namespace operations_research::math_opt #endif // OR_TOOLS_MATH_OPT_IO_PROTO_CONVERTER_H_ diff --git a/ortools/math_opt/model.proto b/ortools/math_opt/model.proto index 1cd9f9ac7e..2f3d484731 100644 --- a/ortools/math_opt/model.proto +++ b/ortools/math_opt/model.proto @@ -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" = diff --git a/ortools/math_opt/model_parameters.proto b/ortools/math_opt/model_parameters.proto index c42a8747fe..27f8ba7012 100644 --- a/ortools/math_opt/model_parameters.proto +++ b/ortools/math_opt/model_parameters.proto @@ -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; } diff --git a/ortools/math_opt/model_update.proto b/ortools/math_opt/model_update.proto index b2527b13b8..6ae4f41067 100644 --- a/ortools/math_opt/model_update.proto +++ b/ortools/math_opt/model_update.proto @@ -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. diff --git a/ortools/math_opt/parameters.proto b/ortools/math_opt/parameters.proto index 9c5157ac29..c3c5e54146 100644 --- a/ortools/math_opt/parameters.proto +++ b/ortools/math_opt/parameters.proto @@ -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; } diff --git a/ortools/math_opt/result.proto b/ortools/math_opt/result.proto index a6d9ae3e97..e4bf970e1a 100644 --- a/ortools/math_opt/result.proto +++ b/ortools/math_opt/result.proto @@ -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; } } diff --git a/ortools/math_opt/samples/BUILD.bazel b/ortools/math_opt/samples/BUILD.bazel index 8d9fb9bfd0..65a7f9d574 100644 --- a/ortools/math_opt/samples/BUILD.bazel +++ b/ortools/math_opt/samples/BUILD.bazel @@ -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"], diff --git a/ortools/math_opt/samples/basic_example.cc b/ortools/math_opt/samples/basic_example.cc index bb7f431472..c33569a787 100644 --- a/ortools/math_opt/samples/basic_example.cc +++ b/ortools/math_opt/samples/basic_example.cc @@ -33,31 +33,28 @@ namespace { void SolveVersion1() { using ::operations_research::math_opt::LinearConstraint; - using ::operations_research::math_opt::MathOpt; - using ::operations_research::math_opt::Objective; - using ::operations_research::math_opt::Result; - using ::operations_research::math_opt::SolveParametersProto; - using ::operations_research::math_opt::SolveResultProto; + using ::operations_research::math_opt::Model; + using ::operations_research::math_opt::SolveResult; + using ::operations_research::math_opt::SolverType; + using ::operations_research::math_opt::TerminationReason; using ::operations_research::math_opt::Variable; - MathOpt optimizer(operations_research::math_opt::SOLVER_TYPE_GSCIP, - "my_model"); - const Variable x = optimizer.AddBinaryVariable("x"); - const Variable y = optimizer.AddContinuousVariable(0.0, 2.5, "y"); - const LinearConstraint c = optimizer.AddLinearConstraint( + Model model("my_model"); + const Variable x = model.AddBinaryVariable("x"); + const Variable y = model.AddContinuousVariable(0.0, 2.5, "y"); + const LinearConstraint c = model.AddLinearConstraint( -std::numeric_limits::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 diff --git a/ortools/math_opt/samples/cocktail_hour.cc b/ortools/math_opt/samples/cocktail_hour.cc new file mode 100644 index 0000000000..458ed31b8d --- /dev/null +++ b/ortools/math_opt/samples/cocktail_hour.cc @@ -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 +#include + +#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, existing_ingredients, {}, + "Ingredients you already have (ignored in analysis mode)."); +ABSL_FLAG(std::vector, unavailable_ingredients, {}, + "Ingredients you cannot get (ignored in analysis mode)."); +ABSL_FLAG(std::vector, required_cocktails, {}, + "Cocktails you must be able to make (ignored in analysis mode)."); +ABSL_FLAG(std::vector, 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 ingredients; +}; + +std::vector 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 ingredients; + std::vector cocktails; +}; + +absl::StatusOr SolveForMenu( + const int max_new_ingredients, const bool enable_solver_output, + const absl::flat_hash_set& existing_ingredients, + const absl::flat_hash_set& unavailable_ingredients, + const absl::flat_hash_set& required_cocktails, + const absl::flat_hash_set& blocked_cocktails) { + const std::vector all_cocktails = AllCocktails(); + math_opt::Model model("Cocktail hour"); + absl::flat_hash_map 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 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 SetFromVec( + const std::vector& 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 = 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& cocktails, + const std::string& title = "Cocktail Hour") { + std::vector 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({"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 = + 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; +} diff --git a/ortools/math_opt/samples/cutting_stock.cc b/ortools/math_opt/samples/cutting_stock.cc new file mode 100644 index 0000000000..ba04975d07 --- /dev/null +++ b/ortools/math_opt/samples/cutting_stock.cc @@ -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 +#include +#include +#include + +#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::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 piece_sizes; + std::vector 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 pieces; + std::vector 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 configurations; + std::vector 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> BestConfiguration( + const std::vector& piece_prices, + const std::vector& 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 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( + 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 SolveCuttingStock( + const CuttingStockInstance& instance) { + math_opt::Model model("cutting_stock"); + model.set_minimize(); + const int n = instance.piece_sizes.size(); + std::vector 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> 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 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(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; +} diff --git a/ortools/math_opt/samples/facility_lp_benders.cc b/ortools/math_opt/samples/facility_lp_benders.cc index 958e3deb26..ceba15b3b0 100644 --- a/ortools/math_opt/samples/facility_lp_benders.cc +++ b/ortools/math_opt/samples/facility_lp_benders.cc @@ -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 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 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 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 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 first_stage_solver = + IncrementalSolver::New(first_stage_model, SolverType::kGurobi).value(); + const std::unique_ptr 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; diff --git a/ortools/math_opt/samples/integer_programming.cc b/ortools/math_opt/samples/integer_programming.cc index 06f5f8000b..f45212aa6b 100644 --- a/ortools/math_opt/samples/integer_programming.cc +++ b/ortools/math_opt/samples/integer_programming.cc @@ -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 #include @@ -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::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 diff --git a/ortools/math_opt/samples/lagrangian_relaxation.cc b/ortools/math_opt/samples/lagrangian_relaxation.cc index c4a7d0f200..ed005ec800 100644 --- a/ortools/math_opt/samples/lagrangian_relaxation.cc +++ b/ortools/math_opt/samples/lagrangian_relaxation.cc @@ -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(solver_type, "LagrangianProblem"); - } - std::unique_ptr model; + FlowModel() : model(std::make_unique("LagrangianProblem")) {} + std::unique_ptr 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& 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& 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 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().infinity(); - double lower_bound = -std::numeric_limits().infinity(); + double upper_bound = std::numeric_limits::infinity(); + double lower_bound = -std::numeric_limits::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& 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); } } diff --git a/ortools/math_opt/samples/linear_programming.cc b/ortools/math_opt/samples/linear_programming.cc index cb9787fc07..18ce9a991e 100644 --- a/ortools/math_opt/samples/linear_programming.cc +++ b/ortools/math_opt/samples/linear_programming.cc @@ -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::infinity(); @@ -51,40 +49,39 @@ constexpr double kInf = std::numeric_limits::infinity(); // x2 in [0, infinity) // void SolveSimpleLp() { - MathOpt optimizer(SOLVER_TYPE_GLOP, "Linear programming example"); + Model model("Linear programming example"); // Variables std::vector 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 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 diff --git a/ortools/math_opt/solution.proto b/ortools/math_opt/solution.proto index ddd00a6dec..9494f2604f 100644 --- a/ortools/math_opt/solution.proto +++ b/ortools/math_opt/solution.proto @@ -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; } diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index 0ab0549fff..5d93555500 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -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"], +) diff --git a/ortools/math_opt/solvers/cp_sat_solver.cc b/ortools/math_opt/solvers/cp_sat_solver.cc index e1d6566fa1..6e61dc37bd 100644 --- a/ortools/math_opt/solvers/cp_sat_solver.cc +++ b/ortools/math_opt/solvers/cp_sat_solver.cc @@ -13,7 +13,10 @@ #include "ortools/math_opt/solvers/cp_sat_solver.h" +#include +#include #include +#include #include #include #include @@ -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::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 SetSolveParameters( - const SolveParametersProto& parameters, MPModelRequest& request) { + const SolveParametersProto& parameters, const bool has_message_callback, + MPModelRequest& request) { std::vector 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 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 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 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> +GetTerminationAndStats(const bool is_interrupted, const bool maximize, + const MPSolutionResponse& response) { + SolveStatsProto solve_stats; + TerminationProto termination; -absl::StatusOr> 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 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 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 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> 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 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 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 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 logging_callback; + if (message_cb != nullptr) { + logging_callback = [&](const std::string& message) { + message_cb(absl::StrSplit(message, '\n')); + }; + } + + const absl::flat_hash_set events = + EventSet(callback_registration); + std::function 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 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 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); diff --git a/ortools/math_opt/solvers/cp_sat_solver.h b/ortools/math_opt/solvers/cp_sat_solver.h index 2ec59718c4..78aeb1d089 100644 --- a/ortools/math_opt/solvers/cp_sat_solver.h +++ b/ortools/math_opt/solvers/cp_sat_solver.h @@ -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> New( - const ModelProto& model, const SolverInitializerProto& initializer); + const ModelProto& model, const InitArgs& init_args); absl::StatusOr 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 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 cp_sat_variable_values, const ModelSolveParametersProto& model_parameters) const; const MPModelProto cp_sat_model_; diff --git a/ortools/math_opt/solvers/glop_solver.cc b/ortools/math_opt/solvers/glop_solver.cc index 1685f5e0fb..acf1822afd 100644 --- a/ortools/math_opt/solvers/glop_solver.cc +++ b/ortools/math_opt/solvers/glop_solver.cc @@ -14,7 +14,10 @@ #include "ortools/math_opt/solvers/glop_solver.h" #include +#include #include +#include +#include #include #include #include @@ -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 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> -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 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 @@ -336,6 +391,58 @@ SparseDoubleVectorProto FillSparseDoubleVector( return result; } +// ValueType should be glop's VariableStatus or ConstraintStatus. +template +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 +SparseBasisStatusVector FillSparseBasisStatusVector( + const std::vector& ids_in_order, + const absl::flat_hash_map& id_map, + const glop::StrictITIVector& 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 +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 std::vector GetSortedIs( const absl::flat_hash_map& id_map) { @@ -348,74 +455,250 @@ std::vector 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::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( + static_cast(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( + static_cast(value)); + } + lp_solver_.SetInitialBasis(variable_statuses, constraint_statuses); } absl::StatusOr 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 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 interrupt_solve = false; + const std::unique_ptr 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> 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()); diff --git a/ortools/math_opt/solvers/glop_solver.h b/ortools/math_opt/solvers/glop_solver.h index 31f3fb0a5b..1c32b9fc43 100644 --- a/ortools/math_opt/solvers/glop_solver.h +++ b/ortools/math_opt/solvers/glop_solver.h @@ -16,6 +16,7 @@ #include +#include #include #include #include @@ -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> New( - const ModelProto& model, const SolverInitializerProto& initializer); + const ModelProto& model, const InitArgs& init_args); absl::StatusOr 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> - 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_; diff --git a/ortools/math_opt/solvers/gscip_solver.cc b/ortools/math_opt/solvers/gscip_solver.cc index f03d08cdbc..2af49e82ea 100644 --- a/ortools/math_opt/solvers/gscip_solver.cc +++ b/ortools/math_opt/solvers/gscip_solver.cc @@ -17,12 +17,14 @@ #include #include #include +#include #include #include #include #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 initializer_; - absl::optional value_; + std::optional value_; }; template @@ -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> -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 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 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 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 GScipSolver::CreateSolveResultProto( return solve_result; } +GScipSolver::GScipSolver(std::unique_ptr gscip) + : gscip_(std::move(ABSL_DIE_IF_NULL(gscip))) { + interrupt_event_handler_.Register(gscip_.get()); +} + absl::StatusOr> 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::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. + 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 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 callback_handler = GScipSolverCallbackHandler::RegisterIfNeeded(callback_registration, cb, start, gscip_->scip()); - const GScipParameters gscip_parameters = MergeCommonParameters( - parameters.common_parameters(), parameters.gscip_parameters()); + std::unique_ptr message_cb_handler; + if (message_cb != nullptr) { + message_cb_handler = + std::make_unique(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 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 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 diff --git a/ortools/math_opt/solvers/gscip_solver.h b/ortools/math_opt/solvers/gscip_solver.h index 2b437c5bd4..7fe37e0ae7 100644 --- a/ortools/math_opt/solvers/gscip_solver.h +++ b/ortools/math_opt/solvers/gscip_solver.h @@ -17,9 +17,7 @@ #include #include #include -#include -#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> New( - const ModelProto& model, const SolverInitializerProto& initializer); + const ModelProto& model, const InitArgs& init_args); absl::StatusOr 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); absl::Status AddVariables(const VariablesProto& variables, const absl::flat_hash_map& @@ -73,16 +110,12 @@ class GScipSolver : public SolverInterface { const SparseDoubleMatrixProto& linear_constraint_matrix); absl::flat_hash_set LookupAllVariables( absl::Span variable_ids); - static absl::StatusOr< - std::pair> - ConvertTerminationReason(GScipOutput::Status gscip_status, - const std::string& gscip_status_detail, - bool has_feasible_solution); absl::StatusOr CreateSolveResultProto( GScipResult gscip_result, const ModelSolveParametersProto& model_parameters); - std::unique_ptr gscip_; + const std::unique_ptr gscip_; + InterruptEventHandler interrupt_event_handler_; absl::flat_hash_map variables_; absl::flat_hash_map linear_constraints_; int64_t next_unused_variable_id_ = 0; diff --git a/ortools/math_opt/solvers/gscip_solver_callback.cc b/ortools/math_opt/solvers/gscip_solver_callback.cc index f0ad185c8f..0e46396241 100644 --- a/ortools/math_opt/solvers/gscip_solver_callback.cc +++ b/ortools/math_opt/solvers/gscip_solver_callback.cc @@ -13,8 +13,8 @@ #include "ortools/math_opt/solvers/gscip_solver_callback.h" -#include #include +#include #include #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 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 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 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 -GScipSolverCallbackHandler::CallUserCallback( +std::optional 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 result_or = callback_(callback_data); @@ -176,7 +84,7 @@ GScipSolverCallbackHandler::CallUserCallback( << interrupt_status; } } - return absl::nullopt; + return std::nullopt; } return *std::move(result_or); diff --git a/ortools/math_opt/solvers/gscip_solver_callback.h b/ortools/math_opt/solvers/gscip_solver_callback.h index 305371ba9f..76b336da4c 100644 --- a/ortools/math_opt/solvers/gscip_solver_callback.h +++ b/ortools/math_opt/solvers/gscip_solver_callback.h @@ -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 CallUserCallback( + std::optional 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 diff --git a/ortools/math_opt/solvers/gscip_solver_message_callback_handler.cc b/ortools/math_opt/solvers/gscip_solver_message_callback_handler.cc new file mode 100644 index 0000000000..69fd5a0ca1 --- /dev/null +++ b/ortools/math_opt/solvers/gscip_solver_message_callback_handler.cc @@ -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 +#include +#include +#include + +#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 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 lines = message_callback_data_.Parse(message); + if (!lines.empty()) { + message_callback_(lines); + } +} + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/solvers/gscip_solver_message_callback_handler.h b/ortools/math_opt/solvers/gscip_solver_message_callback_handler.h new file mode 100644 index 0000000000..d4a37869f7 --- /dev/null +++ b/ortools/math_opt/solvers/gscip_solver_message_callback_handler.h @@ -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 message_callback_handler; +// if (message_callback != nullptr) { +// message_callback_handler = +// std::make_unique(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_ diff --git a/ortools/math_opt/solvers/gurobi.proto b/ortools/math_opt/solvers/gurobi.proto new file mode 100644 index 0000000000..a9b0368e43 --- /dev/null +++ b/ortools/math_opt/solvers/gurobi.proto @@ -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; +} diff --git a/ortools/math_opt/solvers/gurobi/BUILD.bazel b/ortools/math_opt/solvers/gurobi/BUILD.bazel new file mode 100644 index 0000000000..b3ebd57344 --- /dev/null +++ b/ortools/math_opt/solvers/gurobi/BUILD.bazel @@ -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", + ], +) diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.cc b/ortools/math_opt/solvers/gurobi/g_gurobi.cc new file mode 100644 index 0000000000..f64e737524 --- /dev/null +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.cc @@ -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 + +#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(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 GurobiNewMasterEnv( + const std::optional& 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> Gurobi::NewWithSharedMasterEnv( + GRBenv* const master_env) { + CHECK(master_env != nullptr); + return New(nullptr, master_env); +} + +absl::StatusOr> 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> 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 obj, + const absl::Span lb, + const absl::Span ub, + const absl::Span vtype, + const absl::Span names) { + return AddVars({}, {}, {}, obj, lb, ub, vtype, names); +} + +absl::Status Gurobi::AddVars(const absl::Span vbegin, + const absl::Span vind, + const absl::Span vval, + const absl::Span obj, + const absl::Span lb, + const absl::Span ub, + const absl::Span vtype, + const absl::Span 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(obj.data()); + } + if (!vbegin.empty()) { + CHECK_EQ(vbegin.size(), num_vars); + } + char** c_names = nullptr; + std::vector 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(name.c_str())); + } + c_names = c_names_data.data(); + } + return ToStatus(GRBaddvars(/*model=*/gurobi_model_, /*numvars=*/num_vars, + /*numnz=*/vind.size(), + /*vbeg=*/const_cast(vbegin.data()), + /*vind=*/const_cast(vind.data()), + /*vval=*/const_cast(vval.data()), + /*obj=*/c_obj, + /*lb=*/const_cast(lb.data()), + /*ub=*/const_cast(ub.data()), + /*vtype=*/const_cast(vtype.data()), + /*varnames=*/c_names)); +} + +absl::Status Gurobi::DelVars(const absl::Span ind) { + return ToStatus( + GRBdelvars(gurobi_model_, ind.size(), const_cast(ind.data()))); +} + +absl::Status Gurobi::AddConstrs(const absl::Span sense, + const absl::Span rhs, + const absl::Span names) { + const int num_cons = sense.size(); + CHECK_EQ(rhs.size(), num_cons); + char** c_names = nullptr; + std::vector 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(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(sense.data()), + /*rhs=*/const_cast(rhs.data()), /*constrnames=*/c_names)); +} + +absl::Status Gurobi::DelConstrs(const absl::Span ind) { + return ToStatus( + GRBdelconstrs(gurobi_model_, ind.size(), const_cast(ind.data()))); +} + +absl::Status Gurobi::AddQpTerms(const absl::Span qrow, + const absl::Span qcol, + const absl::Span qval) { + const int numqnz = qrow.size(); + CHECK_EQ(qcol.size(), numqnz); + CHECK_EQ(qval.size(), numqnz); + return ToStatus(GRBaddqpterms( + gurobi_model_, numqnz, const_cast(qcol.data()), + const_cast(qrow.data()), const_cast(qval.data()))); +} + +absl::Status Gurobi::DelQ() { return ToStatus(GRBdelq(gurobi_model_)); } + +absl::Status Gurobi::ChgCoeffs(const absl::Span cind, + const absl::Span vind, + const absl::Span 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(cind.data()), + const_cast(vind.data()), const_cast(val.data()))); +} + +absl::StatusOr 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 vbegin, + const absl::Span vind, + const absl::Span 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::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 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 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 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 new_values) { + return ToStatus(GRBsetintattrarray(gurobi_model_, name, 0, new_values.size(), + const_cast(new_values.data()))); +} + +absl::Status Gurobi::SetDoubleAttrArray( + const char* const name, const absl::Span new_values) { + return ToStatus(GRBsetdblattrarray(gurobi_model_, name, 0, new_values.size(), + const_cast(new_values.data()))); +} + +absl::Status Gurobi::SetCharAttrArray(const char* const name, + const absl::Span new_values) { + return ToStatus(GRBsetcharattrarray(gurobi_model_, name, 0, new_values.size(), + const_cast(new_values.data()))); +} + +absl::Status Gurobi::GetIntAttrArray(const char* const name, + const absl::Span 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> Gurobi::GetIntAttrArray(const char* const name, + const int len) const { + std::vector result(len); + RETURN_IF_ERROR(GetIntAttrArray(name, absl::MakeSpan(result))); + return result; +} + +absl::Status Gurobi::GetDoubleAttrArray( + const char* const name, const absl::Span 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> Gurobi::GetDoubleAttrArray( + const char* const name, const int len) const { + std::vector result(len); + RETURN_IF_ERROR(GetDoubleAttrArray(name, absl::MakeSpan(result))); + return result; +} + +absl::Status Gurobi::GetCharAttrArray(const char* const name, + const absl::Span 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> Gurobi::GetCharAttrArray( + const char* const name, const int len) const { + std::vector result(len); + RETURN_IF_ERROR(GetCharAttrArray(name, absl::MakeSpan(result))); + return result; +} + +absl::Status Gurobi::SetIntAttrList(const char* const name, + const absl::Span ind, + const absl::Span new_values) { + const int len = ind.size(); + CHECK_EQ(new_values.size(), len); + return ToStatus(GRBsetintattrlist(gurobi_model_, name, len, + const_cast(ind.data()), + const_cast(new_values.data()))); +} + +absl::Status Gurobi::SetDoubleAttrList( + const char* const name, const absl::Span ind, + const absl::Span new_values) { + const int len = ind.size(); + CHECK_EQ(new_values.size(), len); + return ToStatus(GRBsetdblattrlist(gurobi_model_, name, len, + const_cast(ind.data()), + const_cast(new_values.data()))); +} + +absl::Status Gurobi::SetCharAttrList(const char* const name, + const absl::Span ind, + const absl::Span new_values) { + const int len = ind.size(); + CHECK_EQ(new_values.size(), len); + return ToStatus(GRBsetcharattrlist(gurobi_model_, name, len, + const_cast(ind.data()), + const_cast(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 Gurobi::GetIntParam(const char* const name) { + int result; + RETURN_IF_ERROR(ToStatus(GRBgetintparam(model_env_, name, &result))); + return result; +} + +absl::StatusOr Gurobi::GetDoubleParam(const char* const name) { + double result; + RETURN_IF_ERROR(ToStatus(GRBgetdblparam(model_env_, name, &result))); + return result; +} + +absl::StatusOr Gurobi::GetStringParam(const char* const name) { + std::vector 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 Gurobi::CallbackContext::CbGetInt(const int what) const { + int result; + RETURN_IF_ERROR(gurobi_->ToStatus( + GRBcbget(cb_data_, where_, what, static_cast(&result)))); + return result; +} + +absl::StatusOr Gurobi::CallbackContext::CbGetDouble( + const int what) const { + double result; + RETURN_IF_ERROR(gurobi_->ToStatus( + GRBcbget(cb_data_, where_, what, static_cast(&result)))); + return result; +} + +absl::Status Gurobi::CallbackContext::CbGetDoubleArray( + const int what, const absl::Span result) const { + return gurobi_->ToStatus( + GRBcbget(cb_data_, where_, what, static_cast(result.data()))); +} + +absl::StatusOr Gurobi::CallbackContext::CbGetMessage() const { + char* result = nullptr; + RETURN_IF_ERROR(gurobi_->ToStatus(GRBcbget( + cb_data_, where_, GRB_CB_MSG_STRING, static_cast(&result)))); + if (result == nullptr) { + return std::string(); + } + return std::string(result); +} + +absl::Status Gurobi::CallbackContext::CbCut( + const absl::Span cutind, const absl::Span 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(cutind.data()), + const_cast(cutval.data()), cutsense, cutrhs)); +} + +absl::Status Gurobi::CallbackContext::CbLazy( + const absl::Span lazyind, const absl::Span 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(lazyind.data()), + const_cast(lazyval.data()), lazysense, lazyrhs)); +} + +absl::StatusOr Gurobi::CallbackContext::CbSolution( + const absl::Span solution) const { + double result; + RETURN_IF_ERROR(gurobi_->ToStatus( + GRBcbsolution(cb_data_, const_cast(solution.data()), &result))); + return result; +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.h b/ortools/math_opt/solvers/gurobi/g_gurobi.h new file mode 100644 index 0000000000..9e0fe3ed91 --- /dev/null +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.h @@ -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 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). +// * 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 +#include +#include +#include + +#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; + +// 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 GurobiNewMasterEnv( + const std::optional& 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::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::New().value(); +// bool is_avail = gurobi->IsAttrAvailable(GRB_DBL_ATTR_OBJVAL); +// To read an attribute: +// std::unique_ptr gurobi = Gurobi::New().value(); +// absl::StatusOr 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::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> 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::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::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 begins; + + // Has size equal to the number of nonzeros in the matrix, the row for this + // entry. + std::vector inds; + + // Has size equal to the number of nonzeros in the matrix, the value for + // this entry. + std::vector 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 CbGetInt(int what) const; + + // Calls GRBcbget() on "what" with result type double, see Callback Codes in + // Gurobi docs for values of "what". + absl::StatusOr 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 result) const; + + // Calls GRBcbget() where what=MSG_STRING (call only at where=MESSAGE). + absl::StatusOr CbGetMessage() const; + + // Calls GRBcbcut(). + absl::Status CbCut(absl::Span cutind, + absl::Span cutval, char cutsense, + double cutrhs) const; + + // Calls GRBcblazy(). + absl::Status CbLazy(absl::Span lazyind, + absl::Span lazyval, char lazysense, + double lazyrhs) const; + + // Calls GRBcbsolution(). + absl::StatusOr CbSolution(absl::Span 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; + + // 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> 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> 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 obj, + absl::Span lb, absl::Span ub, + absl::Span vtype, + absl::Span 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 vbegin, absl::Span vind, + absl::Span vval, + absl::Span obj, + absl::Span lb, absl::Span ub, + absl::Span vtype, + absl::Span names); + + // Calls GRBdelvars(). + absl::Status DelVars(absl::Span 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 sense, + absl::Span rhs, + absl::Span names); + + // Calls GRBdelconstrs(). + absl::Status DelConstrs(absl::Span ind); + + // Calls GRBchgcoeffs(). + // + // Requirements: + // * cind, vind, and val have size equal to the number of changed constraint + // matrix entries. + absl::Status ChgCoeffs(absl::Span cind, absl::Span vind, + absl::Span val); + + // Calls GRBaddqpterms(). + // + // Requirements: + // * qrow, qcol, and qval have size equal to the number of new quadratic + // objective terms. + absl::Status AddQpTerms(absl::Span qrow, + absl::Span qcol, + absl::Span 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 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 vbegin, absl::Span vind, + absl::Span 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 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 GetIntAttr(const char* name) const; + absl::Status SetIntAttr(const char* attr_name, int value); + + absl::StatusOr GetDoubleAttr(const char* name) const; + absl::Status SetDoubleAttr(const char* attr_name, double value); + + absl::StatusOr GetStringAttr(const char* name) const; + absl::Status SetStringAttr(const char* attr_name, const std::string& value); + + absl::Status GetIntAttrArray(const char* name, + absl::Span attr_out) const; + absl::StatusOr> GetIntAttrArray(const char* name, + int len) const; + absl::Status SetIntAttrArray(const char* name, + absl::Span new_values); + absl::Status SetIntAttrList(const char* name, absl::Span ind, + absl::Span new_values); + + absl::Status GetDoubleAttrArray(const char* name, + absl::Span attr_out) const; + absl::StatusOr> GetDoubleAttrArray(const char* name, + int len) const; + absl::Status SetDoubleAttrArray(const char* name, + absl::Span new_values); + absl::Status SetDoubleAttrList(const char* name, absl::Span ind, + absl::Span new_values); + + absl::Status GetCharAttrArray(const char* name, + absl::Span attr_out) const; + absl::StatusOr> GetCharAttrArray(const char* name, + int len) const; + absl::Status SetCharAttrArray(const char* name, + absl::Span new_values); + absl::Status SetCharAttrList(const char* name, absl::Span ind, + absl::Span 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 GetIntParam(const char* name); + + // Calls GRBgetdblparam(). + absl::StatusOr GetDoubleParam(const char* name); + + // Calls GRBgetstrparam(). + absl::StatusOr 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> 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_ diff --git a/ortools/math_opt/solvers/gurobi_callback.cc b/ortools/math_opt/solvers/gurobi_callback.cc index 3829ee06c8..34a3804f6d 100644 --- a/ortools/math_opt/solvers/gurobi_callback.cc +++ b/ortools/math_opt/solvers/gurobi_callback.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -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(); case CALLBACK_EVENT_PRESOLVE: return CheckedGuroibWhere(); case CALLBACK_EVENT_SIMPLEX: @@ -75,23 +74,12 @@ inline int GurobiEvent(CallbackEventProto event) { return CheckedGuroibWhere(); case CALLBACK_EVENT_BARRIER: return CheckedGuroibWhere(); - case CALLBACK_EVENT_MESSAGE: - return CheckedGuroibWhere(); 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& grb_solution, const gtl::linked_hash_map& 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 get_int(int what) const { - int result; - RETURN_IF_ERROR(AsStatus(GRBcbget(cbdata_, where_, what, &result))); - return result; +absl::StatusOr CbGetInt64(const Gurobi::CallbackContext& context, + int what) { + ASSIGN_OR_RETURN(const double result, context.CbGetDouble(what)); + int64_t result64 = static_cast(result); + if (result != static_cast(result64)) { + return absl::InternalError( + absl::StrCat("Error converting double attribute: ", what, + "with value: ", result, " to int64_t exactly.")); } + return result64; +} - absl::StatusOr get_double(int what) const { - double result; - RETURN_IF_ERROR(AsStatus(GRBcbget(cbdata_, where_, what, &result))); - return result; +absl::StatusOr CbGetBool(const Gurobi::CallbackContext& context, + int what) { + ASSIGN_OR_RETURN(const int result, context.CbGetInt(what)); + bool result_bool = static_cast(result); + if (result != static_cast(result_bool)) { + return absl::InternalError( + absl::StrCat("Error converting int attribute: ", what, + "with value: ", result, " to bool exactly.")); } - - absl::StatusOr get_int64(int what) const { - double result; - RETURN_IF_ERROR(AsStatus(GRBcbget(cbdata_, where_, what, &result))); - int64_t result64 = static_cast(result); - if (result != static_cast(result64)) { - return absl::InternalError( - absl::StrCat("Error converting double attribute: ", what, - "with value: ", result, " to int64_t exactly.")); - } - return result64; - } - - absl::StatusOr get_bool(int what) const { - int result; - RETURN_IF_ERROR(AsStatus(GRBcbget(cbdata_, where_, what, &result))); - bool result_bool = static_cast(result); - if (result != static_cast(result_bool)) { - return absl::InternalError( - absl::StrCat("Error converting int attribute: ", what, - "with value: ", result, " to bool exactly.")); - } - return result_bool; - } - - absl::StatusOr 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 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 vars, - absl::Span 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 SuggestSolution(absl::Span 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> CreateCallbackDataProto( - const GurobiCallbackContext& c, const GurobiCallbackInput& callback_input, +absl::StatusOr> 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 msg = c.get_string(GRB_CB_MSG_STRING); - RETURN_IF_ERROR(msg.status()) - << "Error getting message string in callback"; - absl::optional 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 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 grb_status = c.get_int(GRB_CB_MIPNODE_STATUS); + c.CbGetInt(GRB_CB_MIPNODE_SOLCNT)); + const absl::StatusOr 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 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> 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 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 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 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 callback_data, - CreateCallbackDataProto(cb_context, callback_input, - message_callback_data)); + if (context.where() == GRB_CB_MESSAGE) { + if (callback_input.message_cb) { + const absl::StatusOr msg = context.CbGetMessage(); + RETURN_IF_ERROR(msg.status()) + << "Error getting message string in callback"; + const std::vector 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 callback_data, + CreateCallbackDataProto(context, callback_input, message_callback_data)); if (!callback_data) { return absl::OkStatus(); } const absl::StatusOr 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 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 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 diff --git a/ortools/math_opt/solvers/gurobi_callback.h b/ortools/math_opt/solvers/gurobi_callback.h index fe20a6fddc..83b32a7b15 100644 --- a/ortools/math_opt/solvers/gurobi_callback.h +++ b/ortools/math_opt/solvers/gurobi_callback.h @@ -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& 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 EventToGurobiWhere( const absl::flat_hash_set& 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 diff --git a/ortools/math_opt/solvers/gurobi_init_arguments.cc b/ortools/math_opt/solvers/gurobi_init_arguments.cc new file mode 100644 index 0000000000..b102668d3b --- /dev/null +++ b/ortools/math_opt/solvers/gurobi_init_arguments.cc @@ -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 +#include "absl/status/statusor.h" +#include "ortools/math_opt/solvers/gurobi.pb.h" + +namespace operations_research { +namespace math_opt { + +absl::StatusOr NewMasterEnvironment( + std::optional proto_isv_key) { + std::optional 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 diff --git a/ortools/math_opt/solvers/gurobi_init_arguments.h b/ortools/math_opt/solvers/gurobi_init_arguments.h new file mode 100644 index 0000000000..6e49277c0a --- /dev/null +++ b/ortools/math_opt/solvers/gurobi_init_arguments.h @@ -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 +#include + +#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 incremental_solve_1, +// IncrementalSolver::New(model, SOLVER_TYPE_GUROBI, +// SolverInitArguments(gurobi_args))); +// ASSIGN_OR_RETURN( +// const std::unique_ptr 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 NewMasterEnvironment( + std::optional 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_ diff --git a/ortools/math_opt/solvers/gurobi_solver.cc b/ortools/math_opt/solvers/gurobi_solver.cc index e2b3ab9678..1534095b90 100644 --- a/ortools/math_opt/solvers/gurobi_solver.cc +++ b/ortools/math_opt/solvers/gurobi_solver.cc @@ -14,18 +14,23 @@ #include "ortools/math_opt/solvers/gurobi_solver.h" #include +#include #include #include #include #include #include +#include +#include #include +#include +#include #include #include -#include "ortools/base/integral_types.h" #include "ortools/base/logging.h" -#include "absl/cleanup/cleanup.h" +#include "ortools/base/cleanup.h" +#include "absl/container/flat_hash_set.h" #include "absl/memory/memory.h" #include "absl/status/status.h" #include "absl/status/statusor.h" @@ -39,6 +44,8 @@ #include "ortools/base/map_util.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/math_opt_proto_utils.h" +#include "ortools/math_opt/core/non_streamable_solver_init_arguments.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" @@ -47,99 +54,146 @@ #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.pb.h" #include "ortools/math_opt/solvers/gurobi_callback.h" +#include "ortools/math_opt/solvers/gurobi_init_arguments.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" #include "ortools/base/protoutil.h" -#include "ortools/gurobi/environment.h" - namespace operations_research { namespace math_opt { namespace { -inline BasisStatus ConvertVariableStatus(const int status) { - switch (status) { - case GRB_BASIC: - return BasisStatus::BASIC; - case GRB_NONBASIC_LOWER: - return BasisStatus::AT_LOWER_BOUND; - case GRB_NONBASIC_UPPER: - return BasisStatus::AT_UPPER_BOUND; - case GRB_SUPERBASIC: - return BasisStatus::FREE; - default: - return BasisStatus::INVALID; +constexpr int kGrbOk = 0; + +absl::StatusOr> GurobiFromInitArgs( + const SolverInterface::InitArgs& init_args) { + // We don't test or return an error for incorrect non streamable arguments + // type since it is already tested by the Solver class. + const NonStreamableGurobiInitArguments* const non_streamable_args = + init_args.non_streamable != nullptr + ? init_args.non_streamable->ToNonStreamableGurobiInitArguments() + : nullptr; + std::unique_ptr gurobi; + if (non_streamable_args != nullptr && + non_streamable_args->master_env != nullptr) { + return Gurobi::NewWithSharedMasterEnv(non_streamable_args->master_env); + } else if (init_args.streamable.has_gurobi() && + init_args.streamable.gurobi().has_isv_key()) { + ASSIGN_OR_RETURN( + GRBenvUniquePtr env, + NewMasterEnvironment(init_args.streamable.gurobi().isv_key())); + return Gurobi::New(std::move(env)); + } else { + return Gurobi::New(); } } -inline int GrbVariableStatus(const BasisStatus status) { +std::vector RepeatedPtrFieldToVec( + const google::protobuf::RepeatedPtrField& proto_string_vec) { + std::vector result; + result.reserve(proto_string_vec.size()); + for (const std::string& str : proto_string_vec) { + result.push_back(str); + } + return result; +} + +inline BasisStatusProto ConvertVariableStatus(const int status) { switch (status) { - case BasisStatus::BASIC: + case GRB_BASIC: + return BASIS_STATUS_BASIC; + case GRB_NONBASIC_LOWER: + return BASIS_STATUS_AT_LOWER_BOUND; + case GRB_NONBASIC_UPPER: + return BASIS_STATUS_AT_UPPER_BOUND; + case GRB_SUPERBASIC: + return BASIS_STATUS_FREE; + default: + return BASIS_STATUS_UNSPECIFIED; + } +} + +inline int GrbVariableStatus(const BasisStatusProto status) { + switch (status) { + case BASIS_STATUS_BASIC: return GRB_BASIC; - case BasisStatus::AT_LOWER_BOUND: - case BasisStatus::FIXED_VALUE: + case BASIS_STATUS_AT_LOWER_BOUND: + case BASIS_STATUS_FIXED_VALUE: return GRB_NONBASIC_LOWER; - case BasisStatus::AT_UPPER_BOUND: + case BASIS_STATUS_AT_UPPER_BOUND: return GRB_NONBASIC_UPPER; - case BasisStatus::FREE: + case BASIS_STATUS_FREE: return GRB_SUPERBASIC; - case BasisStatus::INVALID: + case BASIS_STATUS_UNSPECIFIED: default: LOG(FATAL) << "Unexpected invalid initial_basis."; return 0; } } -constexpr int kGrbOk = 0; - GurobiParametersProto MergeParameters( - const CommonSolveParametersProto& common_parameters, - const GurobiParametersProto& gurobi_parameters) { + const SolveParametersProto& solve_parameters) { GurobiParametersProto merged_parameters; - if (common_parameters.has_enable_output()) { - // TODO(user): Gurobi allows for custom display callbacks. This - // allows redirecting output to a file, or parsing it and retrieve key - // information, or redirect it to IDE outputs (such as in Jupiter - // Notebooks). We should install such a callback to manage output if this is - // enabled. - const int enable_output = common_parameters.enable_output() ? 1 : 0; + + { GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_INT_PAR_LOGTOCONSOLE); - parameter->set_value(absl::StrCat(enable_output)); + parameter->set_value(solve_parameters.enable_output() ? "1" : "0"); } - if (common_parameters.has_time_limit()) { + + if (solve_parameters.has_time_limit()) { const double time_limit = absl::ToDoubleSeconds( - util_time::DecodeGoogleApiProto(common_parameters.time_limit()) - .value()); + util_time::DecodeGoogleApiProto(solve_parameters.time_limit()).value()); GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_DBL_PAR_TIMELIMIT); parameter->set_value(absl::StrCat(time_limit)); } - if (common_parameters.has_threads()) { - const int threads = common_parameters.threads(); + + if (solve_parameters.has_threads()) { + const int threads = solve_parameters.threads(); GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_INT_PAR_THREADS); parameter->set_value(absl::StrCat(threads)); } - if (common_parameters.has_random_seed()) { + + if (solve_parameters.has_absolute_gap_limit()) { + const double absolute_gap_limit = solve_parameters.absolute_gap_limit(); + GurobiParametersProto::Parameter* const parameter = + merged_parameters.add_parameters(); + parameter->set_name(GRB_DBL_PAR_MIPGAPABS); + parameter->set_value(absl::StrCat(absolute_gap_limit)); + } + + if (solve_parameters.has_relative_gap_limit()) { + const double relative_gap_limit = solve_parameters.relative_gap_limit(); + GurobiParametersProto::Parameter* const parameter = + merged_parameters.add_parameters(); + parameter->set_name(GRB_DBL_PAR_MIPGAP); + parameter->set_value(absl::StrCat(relative_gap_limit)); + } + + if (solve_parameters.has_random_seed()) { const int random_seed = - std::min(GRB_MAXINT, std::max(common_parameters.random_seed(), 0)); + std::min(GRB_MAXINT, std::max(solve_parameters.random_seed(), 0)); GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_INT_PAR_SEED); parameter->set_value(absl::StrCat(random_seed)); } - if (common_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) { + + if (solve_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) { GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_INT_PAR_METHOD); - switch (common_parameters.lp_algorithm()) { + switch (solve_parameters.lp_algorithm()) { case LP_ALGORITHM_PRIMAL_SIMPLEX: parameter->set_value(absl::StrCat(GRB_METHOD_PRIMAL)); break; @@ -151,15 +205,16 @@ GurobiParametersProto MergeParameters( break; default: LOG(FATAL) << "LPAlgorithm: " - << ProtoEnumToString(common_parameters.lp_algorithm()) + << ProtoEnumToString(solve_parameters.lp_algorithm()) << " unknown, error setting Gurobi parameters"; } } - if (common_parameters.scaling() != EMPHASIS_UNSPECIFIED) { + + if (solve_parameters.scaling() != EMPHASIS_UNSPECIFIED) { GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_INT_PAR_SCALEFLAG); - switch (common_parameters.scaling()) { + switch (solve_parameters.scaling()) { case EMPHASIS_OFF: parameter->set_value(absl::StrCat(0)); break; @@ -175,15 +230,16 @@ GurobiParametersProto MergeParameters( break; default: LOG(FATAL) << "Scaling emphasis: " - << ProtoEnumToString(common_parameters.scaling()) + << ProtoEnumToString(solve_parameters.scaling()) << " unknown, error setting Gurobi parameters"; } } - if (common_parameters.cuts() != EMPHASIS_UNSPECIFIED) { + + if (solve_parameters.cuts() != EMPHASIS_UNSPECIFIED) { GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_INT_PAR_CUTS); - switch (common_parameters.cuts()) { + switch (solve_parameters.cuts()) { case EMPHASIS_OFF: parameter->set_value(absl::StrCat(0)); break; @@ -199,15 +255,16 @@ GurobiParametersProto MergeParameters( break; default: LOG(FATAL) << "Cuts emphasis: " - << ProtoEnumToString(common_parameters.cuts()) + << ProtoEnumToString(solve_parameters.cuts()) << " unknown, error setting Gurobi parameters"; } } - if (common_parameters.heuristics() != EMPHASIS_UNSPECIFIED) { + + if (solve_parameters.heuristics() != EMPHASIS_UNSPECIFIED) { GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_DBL_PAR_HEURISTICS); - switch (common_parameters.heuristics()) { + switch (solve_parameters.heuristics()) { case EMPHASIS_OFF: parameter->set_value(absl::StrCat(0.0)); break; @@ -227,16 +284,16 @@ GurobiParametersProto MergeParameters( break; default: LOG(FATAL) << "Heuristics emphasis: " - << ProtoEnumToString(common_parameters.heuristics()) + << ProtoEnumToString(solve_parameters.heuristics()) << " unknown, error setting Gurobi parameters"; } } - if (common_parameters.presolve() != EMPHASIS_UNSPECIFIED) { + if (solve_parameters.presolve() != EMPHASIS_UNSPECIFIED) { GurobiParametersProto::Parameter* const parameter = merged_parameters.add_parameters(); parameter->set_name(GRB_INT_PAR_PRESOLVE); - switch (common_parameters.presolve()) { + switch (solve_parameters.presolve()) { case EMPHASIS_OFF: parameter->set_value(absl::StrCat(0)); break; @@ -250,15 +307,29 @@ GurobiParametersProto MergeParameters( break; default: LOG(FATAL) << "Presolve emphasis: " - << ProtoEnumToString(common_parameters.presolve()) + << ProtoEnumToString(solve_parameters.presolve()) << " unknown, error setting Gurobi parameters"; } } - for (const GurobiParametersProto_Parameter& parameter : - gurobi_parameters.parameters()) { + if (solve_parameters.has_iteration_limit()) { + GurobiParametersProto::Parameter* const iterationlimit = + merged_parameters.add_parameters(); + iterationlimit->set_name(GRB_DBL_PAR_ITERATIONLIMIT); + iterationlimit->set_value(absl::StrCat(solve_parameters.iteration_limit())); + GurobiParametersProto::Parameter* const bariterlimit = + merged_parameters.add_parameters(); + bariterlimit->set_name(GRB_INT_PAR_BARITERLIMIT); + double val = std::min(std::numeric_limits::max(), + solve_parameters.iteration_limit()); + bariterlimit->set_value(absl::StrCat(val)); + } + + for (const GurobiParametersProto::Parameter& parameter : + solve_parameters.gurobi().parameters()) { *merged_parameters.add_parameters() = parameter; } + return merged_parameters; } @@ -280,152 +351,73 @@ absl::StatusOr SafeInt64FromDouble(const double d) { return result; } +const absl::flat_hash_set& SupportedMIPEvents() { + static const auto* const kEvents = + new absl::flat_hash_set({ + CALLBACK_EVENT_PRESOLVE, CALLBACK_EVENT_SIMPLEX, CALLBACK_EVENT_MIP, + CALLBACK_EVENT_MIP_SOLUTION, CALLBACK_EVENT_MIP_NODE, + // CALLBACK_EVENT_BARRIER is not supported when solving MIPs; it turns + // out that Gurobi uses a barrier algorithm to solve the root node + // relaxation (from the traces) but does not call the associated + // callback. + }); + return *kEvents; +} + +const absl::flat_hash_set& SupportedLPEvents() { + static const auto* const kEvents = + new absl::flat_hash_set({ + CALLBACK_EVENT_PRESOLVE, + CALLBACK_EVENT_SIMPLEX, + CALLBACK_EVENT_BARRIER, + }); + return *kEvents; +} + } // namespace -std::string GurobiSolver::GurobiErrorMessage(int error_code) const { - if (error_code == kGrbOk) { - return {}; - } - if (active_env_ == nullptr) { - if (error_code == GRB_ERROR_NO_LICENSE) { - return absl::StrCat("Gurobi error code:", error_code, - " (Failed to obtain a valid license)"); - } else { - return absl::StrCat("Gurboi error code:", error_code, - " (No environment is available)"); - } - } - return absl::StrCat("Gurobi error code: ", error_code, ", ", - GRBgeterrormsg(active_env_)); +GurobiSolver::GurobiSolver(std::unique_ptr g_gurobi) + : gurobi_(std::move(g_gurobi)) {} + +int GurobiSolver::num_gurobi_constraints() const { + return linear_constraints_map_.size(); } -std::string GurobiSolver::LogGurobiCode( - int error_code, const char* source_file, int source_line, - const char* statement, absl::string_view extra_message = "") const { - if (error_code == kGrbOk) { - return ""; - } - const std::string error_message = - absl::StrFormat("%s:%d, on '%s' : %s%s", source_file, source_line, - statement, GurobiErrorMessage(error_code), extra_message); - VLOG(1) << error_message; - return error_message; -} - -absl::Status GurobiSolver::GurobiCodeToUtilStatus(int error_code, - const char* source_file, - int source_line, - const char* statement) const { - if (error_code == kGrbOk) { - return absl::OkStatus(); - } - return absl::InvalidArgumentError( - LogGurobiCode(error_code, source_file, source_line, statement)); -} - -// This macro is intended to be used only in class functions of GurobiSolver. -#define RETURN_IF_GUROBI_ERROR(_x_) \ - RETURN_IF_ERROR(GurobiCodeToUtilStatus(_x_, __FILE__, __LINE__, #_x_)) - -// TODO(user): Use empty environments once we move to Gurobi 9. Here -// we would also setup a log callback. -absl::Status GurobiSolver::LoadEnvironment() { - CHECK(master_env_ == nullptr); - RETURN_IF_GUROBI_ERROR(GRBloadenv(&master_env_, - /*logfilename=*/nullptr)); - active_env_ = master_env_; - CHECK(active_env_ != nullptr); - RETURN_IF_GUROBI_ERROR(GRBnewmodel(master_env_, &gurobi_model_, - /*Pname=*/nullptr, - /*numvars=*/0, - /*obj=*/nullptr, /*lb=*/nullptr, - /*ub=*/nullptr, /*vtype=*/nullptr, - /*varnames=*/nullptr)); - CHECK(gurobi_model_ != nullptr); - active_env_ = GRBgetenv(gurobi_model_); - RETURN_IF_GUROBI_ERROR( - GRBsetintattr(gurobi_model_, GRB_INT_ATTR_MODELSENSE, GRB_MINIMIZE)); - - if (VLOG_IS_ON(3)) { - int gurobi_major, gurobi_minor, gurobi_technical; - GRBversion(&gurobi_major, &gurobi_minor, &gurobi_technical); - VLOG(3) << absl::StrFormat( - "Successfully opened environment for Gurobi v%d.%d.%d (%s)", - gurobi_major, gurobi_minor, gurobi_technical, GRBplatform()); - } - return absl::OkStatus(); -} - -absl::StatusOr GurobiSolver::GetIntAttr(const char* name) const { - int result; - RETURN_IF_GUROBI_ERROR(GRBgetintattr(gurobi_model_, name, &result)) - << "Error getting Gurobi int attribute: " << name; - return result; -} - -absl::StatusOr GurobiSolver::GetDoubleAttr(const char* name) const { - double result; - RETURN_IF_GUROBI_ERROR(GRBgetdblattr(gurobi_model_, name, &result)) - << "Error getting Gurobi double attribute: " << name; - return result; -} - -absl::Status GurobiSolver::GetIntAttrArray(const char* name, - absl::Span attr_out) const { - RETURN_IF_GUROBI_ERROR(GRBgetintattrarray(gurobi_model_, name, 0, - attr_out.size(), attr_out.data())) - << "Error getting Gurobi int array attribute: " << name; - return absl::OkStatus(); -} - -absl::Status GurobiSolver::GetDoubleAttrArray( - const char* name, absl::Span attr_out) const { - RETURN_IF_GUROBI_ERROR(GRBgetdblattrarray(gurobi_model_, name, 0, - attr_out.size(), attr_out.data())) - << "Error getting Gurobi double array attribute: " << name; - return absl::OkStatus(); -} - -absl::StatusOr> -GurobiSolver::ConvertTerminationReason(const int gurobi_status, - const bool has_feasible_solution) { +absl::StatusOr GurobiSolver::ConvertTerminationReason( + const int gurobi_status, const SolutionClaims solution_claims) { switch (gurobi_status) { case GRB_OPTIMAL: - return std::make_pair(SolveResultProto::OPTIMAL, ""); + return TerminateForReason(TERMINATION_REASON_OPTIMAL); case GRB_INFEASIBLE: - return std::make_pair(SolveResultProto::INFEASIBLE, ""); - case GRB_UNBOUNDED: { - if (has_feasible_solution) { - return std::make_pair(SolveResultProto::UNBOUNDED, ""); - } else { - return std::make_pair(SolveResultProto::DUAL_INFEASIBLE, - "Gurobi status GRB_UNBOUNDED, but no feasible " - "point found, only primal ray."); + return TerminateForReason(TERMINATION_REASON_INFEASIBLE); + case GRB_UNBOUNDED: + if (solution_claims.primal_feasible_solution_exists) { + return TerminateForReason(TERMINATION_REASON_UNBOUNDED); } - } + return TerminateForReason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, + "Gurobi status GRB_UNBOUNDED"); case GRB_INF_OR_UNBD: - return std::make_pair(SolveResultProto::DUAL_INFEASIBLE, - "Gurobi status GRB_INF_OR_UNBD."); + return TerminateForReason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, + "Gurobi status GRB_INF_OR_UNBD"); case GRB_CUTOFF: - return std::make_pair(SolveResultProto::OBJECTIVE_LIMIT, - "Gurobi status GRB_CUTOFF."); + return TerminateForLimit(LIMIT_OBJECTIVE, "Gurobi status GRB_CUTOFF"); case GRB_ITERATION_LIMIT: - return std::make_pair(SolveResultProto::ITERATION_LIMIT, ""); + return TerminateForLimit(LIMIT_ITERATION); case GRB_NODE_LIMIT: - return std::make_pair(SolveResultProto::NODE_LIMIT, ""); + return TerminateForLimit(LIMIT_NODE); case GRB_TIME_LIMIT: - return std::make_pair(SolveResultProto::TIME_LIMIT, ""); + return TerminateForLimit(LIMIT_TIME); case GRB_SOLUTION_LIMIT: - return std::make_pair(SolveResultProto::SOLUTION_LIMIT, ""); + return TerminateForLimit(LIMIT_SOLUTION); case GRB_INTERRUPTED: - return std::make_pair(SolveResultProto::INTERRUPTED, ""); + return TerminateForLimit(LIMIT_INTERRUPTED); case GRB_NUMERIC: - return std::make_pair(SolveResultProto::NUMERICAL_ERROR, ""); + return TerminateForReason(TERMINATION_REASON_NUMERICAL_ERROR); case GRB_SUBOPTIMAL: - return std::make_pair(SolveResultProto::IMPRECISE, ""); + return TerminateForReason(TERMINATION_REASON_IMPRECISE); case GRB_USER_OBJ_LIMIT: - return std::make_pair(SolveResultProto::OBJECTIVE_LIMIT, - "Gurobi status GRB_USR_OBJ_LIMIT."); + return TerminateForLimit(LIMIT_OBJECTIVE, + "Gurobi status GRB_USR_OBJ_LIMIT"); case GRB_LOADED: return absl::InternalError( "Error creating termination reason, unexpected gurobi status code " @@ -440,18 +432,29 @@ GurobiSolver::ConvertTerminationReason(const int gurobi_status, } } -int GurobiSolver::num_gurobi_constraints() const { - return linear_constraints_map_.size(); +absl::StatusOr GurobiSolver::IsMaximize() const { + ASSIGN_OR_RETURN(const int obj_sense, + gurobi_->GetIntAttr(GRB_INT_ATTR_MODELSENSE)); + return obj_sense == GRB_MAXIMIZE; } absl::StatusOr GurobiSolver::IsLP() const { - ASSIGN_OR_RETURN(const int is_mip, GetIntAttr(GRB_INT_ATTR_IS_MIP)); - ASSIGN_OR_RETURN(const int is_qp, GetIntAttr(GRB_INT_ATTR_IS_QP)); - ASSIGN_OR_RETURN(const int is_qcp, GetIntAttr(GRB_INT_ATTR_IS_QCP)); + ASSIGN_OR_RETURN(const int is_mip, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_MIP)); + ASSIGN_OR_RETURN(const int is_qp, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_QP)); + ASSIGN_OR_RETURN(const int is_qcp, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_QCP)); return !static_cast(is_mip) && !static_cast(is_qp) && !static_cast(is_qcp); } +// TODO(b/204595455): Revisit logic when nonconvex QP support is decided upon +absl::StatusOr GurobiSolver::IsQP() const { + ASSIGN_OR_RETURN(const int is_mip, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_MIP)); + ASSIGN_OR_RETURN(const int is_qp, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_QP)); + ASSIGN_OR_RETURN(const int is_qcp, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_QCP)); + return !static_cast(is_mip) && static_cast(is_qp) && + !static_cast(is_qcp); +} + // TODO(user): switch the use of this function to something closer to // GetGurobiDualRay() template @@ -473,7 +476,7 @@ absl::Status GurobiSolver::SetGurobiBasis(const BasisProto& basis) { std::vector gurobi_variable_basis_status(num_gurobi_variables_); for (const auto [id, value] : MakeView(basis.variable_status())) { gurobi_variable_basis_status[variables_map_.at(id)] = - GrbVariableStatus(static_cast(value)); + GrbVariableStatus(static_cast(value)); } std::vector gurobi_constraint_basis_status; @@ -482,13 +485,13 @@ absl::Status GurobiSolver::SetGurobiBasis(const BasisProto& basis) { const ConstraintData& constraint_data = linear_constraints_map_.at(id); // Non-ranged constraints if (constraint_data.slack_index == kUnspecifiedIndex) { - if (value == BasisStatus::BASIC) { + if (value == BASIS_STATUS_BASIC) { gurobi_constraint_basis_status.push_back(kGrbBasicConstraint); } else { gurobi_constraint_basis_status.push_back(kGrbNonBasicConstraint); } // Ranged constraints - } else if (value == BasisStatus::BASIC) { + } else if (value == BASIS_STATUS_BASIC) { // Either constraint or MathOpt slack is basic, but not both (because // columns for MathOpt slack and internal Gurobi slack are linearly // dependent). We choose the MathOpt slack to be basic. @@ -496,34 +499,28 @@ absl::Status GurobiSolver::SetGurobiBasis(const BasisProto& basis) { gurobi_constraint_basis_status.push_back(kGrbNonBasicConstraint); } else { gurobi_variable_basis_status[constraint_data.slack_index] = - GrbVariableStatus(static_cast(value)); + GrbVariableStatus(static_cast(value)); gurobi_constraint_basis_status.push_back(kGrbNonBasicConstraint); } } - RETURN_IF_GUROBI_ERROR(GRBsetintattrarray( - /*model=*/gurobi_model_, /*attrname=*/GRB_INT_ATTR_VBASIS, - /*first=*/0, /*len=*/gurobi_variable_basis_status.size(), - /*newvalues=*/gurobi_variable_basis_status.data())); - RETURN_IF_GUROBI_ERROR(GRBsetintattrarray( - /*model=*/gurobi_model_, /*attrname=*/GRB_INT_ATTR_CBASIS, - /*first=*/0, /*len=*/gurobi_constraint_basis_status.size(), - /*newvalues=*/gurobi_constraint_basis_status.data())); - + RETURN_IF_ERROR(gurobi_->SetIntAttrArray(GRB_INT_ATTR_VBASIS, + gurobi_variable_basis_status)); + RETURN_IF_ERROR(gurobi_->SetIntAttrArray(GRB_INT_ATTR_CBASIS, + gurobi_constraint_basis_status)); return absl::OkStatus(); } absl::StatusOr GurobiSolver::GetGurobiBasis() { BasisProto basis; - - std::vector gurobi_variable_basis_status(num_gurobi_variables_); - RETURN_IF_ERROR(GetIntAttrArray( - GRB_INT_ATTR_VBASIS, absl::MakeSpan(gurobi_variable_basis_status))); + ASSIGN_OR_RETURN( + const std::vector gurobi_variable_basis_status, + gurobi_->GetIntAttrArray(GRB_INT_ATTR_VBASIS, num_gurobi_variables_)); for (auto [variable_id, gurobi_variable_index] : variables_map_) { basis.mutable_variable_status()->add_ids(variable_id); - const BasisStatus variable_status = ConvertVariableStatus( + const BasisStatusProto variable_status = ConvertVariableStatus( gurobi_variable_basis_status[gurobi_variable_index]); - if (variable_status == BasisStatus::INVALID) { + if (variable_status == BASIS_STATUS_UNSPECIFIED) { return absl::InternalError( absl::StrCat("Invalid Gurobi variable basis status: ", gurobi_variable_basis_status[gurobi_variable_index])); @@ -531,9 +528,9 @@ absl::StatusOr GurobiSolver::GetGurobiBasis() { basis.mutable_variable_status()->add_values(variable_status); } - std::vector gurobi_constraint_basis_status(num_gurobi_constraints()); - RETURN_IF_ERROR(GetIntAttrArray( - GRB_INT_ATTR_CBASIS, absl::MakeSpan(gurobi_constraint_basis_status))); + ASSIGN_OR_RETURN( + const std::vector gurobi_constraint_basis_status, + gurobi_->GetIntAttrArray(GRB_INT_ATTR_CBASIS, num_gurobi_constraints())); for (auto [constraint_id, gurobi_data] : linear_constraints_map_) { basis.mutable_constraint_status()->add_ids(constraint_id); const int gurobi_constraint_status = @@ -548,40 +545,40 @@ absl::StatusOr GurobiSolver::GetGurobiBasis() { if (gurobi_data.lower_bound <= -GRB_INFINITY && gurobi_data.upper_bound < GRB_INFINITY) { if (gurobi_constraint_status == kGrbBasicConstraint) { - basis.mutable_constraint_status()->add_values(BasisStatus::BASIC); + basis.mutable_constraint_status()->add_values(BASIS_STATUS_BASIC); } else { basis.mutable_constraint_status()->add_values( - BasisStatus::AT_UPPER_BOUND); + BASIS_STATUS_AT_UPPER_BOUND); } // linear_terms >= lower_bound } else if (gurobi_data.lower_bound > -GRB_INFINITY && gurobi_data.upper_bound >= GRB_INFINITY) { if (gurobi_constraint_status == kGrbBasicConstraint) { - basis.mutable_constraint_status()->add_values(BasisStatus::BASIC); + basis.mutable_constraint_status()->add_values(BASIS_STATUS_BASIC); } else { basis.mutable_constraint_status()->add_values( - BasisStatus::AT_LOWER_BOUND); + BASIS_STATUS_AT_LOWER_BOUND); } // linear_terms == xxxxx_bound } else if (gurobi_data.lower_bound == gurobi_data.upper_bound) { if (gurobi_constraint_status == kGrbBasicConstraint) { - basis.mutable_constraint_status()->add_values(BasisStatus::BASIC); + basis.mutable_constraint_status()->add_values(BASIS_STATUS_BASIC); } else { // TODO(user): consider refining this to // AT_LOWER_BOUND/AT_UPPER_BOUND using the sign of the dual variable. - basis.mutable_constraint_status()->add_values(BasisStatus::FIXED_VALUE); + basis.mutable_constraint_status()->add_values(BASIS_STATUS_FIXED_VALUE); } // linear_term - slack == 0 (ranged constraint) } else { - const BasisStatus slack_status = ConvertVariableStatus( + const BasisStatusProto slack_status = ConvertVariableStatus( gurobi_variable_basis_status[gurobi_data.slack_index]); - if (slack_status == BasisStatus::INVALID) { + if (slack_status == BASIS_STATUS_UNSPECIFIED) { return absl::InternalError(absl::StrCat( "Invalid Gurobi slack variable basis status: ", slack_status)); } if ((gurobi_constraint_status == kGrbBasicConstraint) || - (slack_status == BasisStatus::BASIC)) { - basis.mutable_constraint_status()->add_values(BasisStatus::BASIC); + (slack_status == BASIS_STATUS_BASIC)) { + basis.mutable_constraint_status()->add_values(BASIS_STATUS_BASIC); } else { basis.mutable_constraint_status()->add_values(slack_status); } @@ -596,9 +593,10 @@ absl::StatusOr GurobiSolver::GetGurobiDualRay( const SparseVectorFilterProto& linear_constraints_filter, const SparseVectorFilterProto& variables_filter, const bool is_maximize) { // farkas_dual = lambda - std::vector farkas_dual(num_gurobi_constraints()); - RETURN_IF_ERROR( - GetDoubleAttrArray(GRB_DBL_ATTR_FARKASDUAL, absl::MakeSpan(farkas_dual))); + ASSIGN_OR_RETURN(const std::vector farkas_dual, + gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_FARKASDUAL, + num_gurobi_constraints())); + DualRayProto dual_ray; // Compute y = -lambda @@ -619,23 +617,16 @@ absl::StatusOr GurobiSolver::GetGurobiDualRay( } // Compute r = \bar{a} = A^T lambda - std::vector constraint_indices(num_gurobi_constraints()); - std::vector coefficients(num_gurobi_constraints()); { SparseVectorFilterPredicate predicate(variables_filter); for (auto [var_id, gurobi_variable_index] : variables_map_) { // reduced_cost_value = r[gurobi_variable_index] // = \bar{a}[gurobi_variable_index] double reduced_cost_value = 0.0; - int result_size; - int vbeg; - // coefficients = column gurobi_variable_index of A - RETURN_IF_GUROBI_ERROR(GRBgetvars( - gurobi_model_, &result_size, &vbeg, constraint_indices.data(), - coefficients.data(), gurobi_variable_index, 1)); - for (int i = 0; i < result_size; ++i) { - reduced_cost_value += - farkas_dual[constraint_indices[i]] * coefficients[i]; + ASSIGN_OR_RETURN(Gurobi::SparseMat column, + gurobi_->GetVars(gurobi_variable_index, 1)); + for (int i = 0; i < column.inds.size(); ++i) { + reduced_cost_value += farkas_dual[column.inds[i]] * column.vals[i]; } if (predicate.AcceptsAndUpdate(var_id, reduced_cost_value)) { dual_ray.mutable_reduced_costs()->add_ids(var_id); @@ -650,161 +641,530 @@ absl::StatusOr GurobiSolver::GetGurobiDualRay( return dual_ray; } -absl::Status GurobiSolver::ExtractSolveResultProto( - const bool is_maximize, SolveResultProto& result, +absl::StatusOr GurobiSolver::GetProblemStatus( + const int grb_termination, const SolutionClaims solution_claims) { + ProblemStatusProto problem_status; + + // Set default statuses + problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); + problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED); + + // Set feasibility statuses + if (solution_claims.primal_feasible_solution_exists) { + problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE); + } + if (solution_claims.dual_feasible_solution_exists) { + problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE); + } + + // Process infeasible conclusions from grb_termination. + switch (grb_termination) { + case GRB_INFEASIBLE: + problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE); + if (solution_claims.primal_feasible_solution_exists) { + return absl::InternalError( + "GRB_INT_ATTR_STATUS == GRB_INFEASIBLE, but a primal feasible " + "solution was returned."); + } + break; + case GRB_UNBOUNDED: + // GRB_UNBOUNDED does necessarily imply the primal is feasible + // https://www.gurobi.com/documentation/9.1/refman/optimization_status_codes.html + problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE); + if (solution_claims.dual_feasible_solution_exists) { + return absl::InternalError( + "GRB_INT_ATTR_STATUS == GRB_UNBOUNDED, but a dual feasible " + "solution was returned or exists."); + } + break; + case GRB_INF_OR_UNBD: + problem_status.set_primal_or_dual_infeasible(true); + if (solution_claims.primal_feasible_solution_exists) { + return absl::InternalError( + "GRB_INT_ATTR_STATUS == GRB_INF_OR_UNBD, but a primal feasible " + "solution was returned."); + } + if (solution_claims.dual_feasible_solution_exists) { + return absl::InternalError( + "GRB_INT_ATTR_STATUS == GRB_INF_OR_UNBD, but a dual feasible " + "solution was returned or exists."); + } + break; + } + return problem_status; +} + +absl::StatusOr GurobiSolver::ExtractSolveResultProto( + const absl::Time start, const ModelSolveParametersProto& model_parameters) { + SolveResultProto result; + + // TODO(b/195295177): Add tests for rays in unbounded MIPs + RETURN_IF_ERROR(FillRays(model_parameters, result)); + + ASSIGN_OR_RETURN((auto [solutions, solution_claims]), + GetSolutions(model_parameters)); + + for (auto& solution : solutions) { + *result.add_solutions() = std::move(solution); + } + + ASSIGN_OR_RETURN(*result.mutable_solve_stats(), + GetSolveStats(start, solution_claims)); + + ASSIGN_OR_RETURN(const int grb_termination, + gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); + ASSIGN_OR_RETURN(*result.mutable_termination(), + ConvertTerminationReason(grb_termination, solution_claims)); + return std::move(result); +} + +absl::StatusOr GurobiSolver::GetSolutions( const ModelSolveParametersProto& model_parameters) { - int num_solutions = 0; - if (GRBisattravailable(gurobi_model_, GRB_INT_ATTR_SOLCOUNT)) { - ASSIGN_OR_RETURN(num_solutions, GetIntAttr(GRB_INT_ATTR_SOLCOUNT)); - } - if (GRBisattravailable(gurobi_model_, GRB_INT_ATTR_STATUS)) { - ASSIGN_OR_RETURN(const int grb_termination, - GetIntAttr(GRB_INT_ATTR_STATUS)); + ASSIGN_OR_RETURN(const bool is_lp, IsLP()); + ASSIGN_OR_RETURN(const bool is_qp, IsQP()); - const bool has_feasible_solution = num_solutions > 0; + if (is_lp) { + return GetLpSolution(model_parameters); + } else if (is_qp) { + return GetQpSolution(model_parameters); + } else { + return GetMipSolutions(model_parameters); + } +} - ASSIGN_OR_RETURN( - const auto reason_and_detail, - ConvertTerminationReason(grb_termination, has_feasible_solution)); - result.set_termination_reason(reason_and_detail.first); - result.set_termination_detail(reason_and_detail.second); - } - if (GRBisattravailable(gurobi_model_, GRB_DBL_ATTR_OBJVAL)) { - ASSIGN_OR_RETURN(const double obj_val, GetDoubleAttr(GRB_DBL_ATTR_OBJVAL)); - result.mutable_solve_stats()->set_best_primal_bound(obj_val); - } else { - result.mutable_solve_stats()->set_best_primal_bound(is_maximize ? -kInf - : kInf); - } - if (GRBisattravailable(gurobi_model_, GRB_DBL_ATTR_OBJBOUND)) { - ASSIGN_OR_RETURN(const double obj_bound, - GetDoubleAttr(GRB_DBL_ATTR_OBJBOUND)); - result.mutable_solve_stats()->set_best_dual_bound(obj_bound); - } else { - result.mutable_solve_stats()->set_best_dual_bound(is_maximize ? kInf - : -kInf); - } - if (GRBisattravailable(gurobi_model_, GRB_DBL_ATTR_ITERCOUNT)) { +absl::StatusOr GurobiSolver::GetSolveStats( + const absl::Time start, const SolutionClaims solution_claims) { + SolveStatsProto solve_stats; + + CHECK_OK(util_time::EncodeGoogleApiProto(absl::Now() - start, + solve_stats.mutable_solve_time())); + + ASSIGN_OR_RETURN(const double best_primal_bound, + GetBestPrimalBound( + /*has_primal_feasible_solution=*/solution_claims + .primal_feasible_solution_exists)); + solve_stats.set_best_primal_bound(best_primal_bound); + + ASSIGN_OR_RETURN(double best_dual_bound, GetBestDualBound()); + solve_stats.set_best_dual_bound(best_dual_bound); + + ASSIGN_OR_RETURN(const int grb_termination, + gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); + ASSIGN_OR_RETURN((*solve_stats.mutable_problem_status()), + GetProblemStatus(grb_termination, solution_claims)); + + if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_ITERCOUNT)) { ASSIGN_OR_RETURN(const double simplex_iters_double, - GetDoubleAttr(GRB_DBL_ATTR_ITERCOUNT)); + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_ITERCOUNT)); ASSIGN_OR_RETURN(const int64_t simplex_iters, SafeInt64FromDouble(simplex_iters_double)); - result.mutable_solve_stats()->set_simplex_iterations(simplex_iters); + solve_stats.set_simplex_iterations(simplex_iters); } - if (GRBisattravailable(gurobi_model_, GRB_INT_ATTR_BARITERCOUNT)) { + + if (gurobi_->IsAttrAvailable(GRB_INT_ATTR_BARITERCOUNT)) { ASSIGN_OR_RETURN(const int barrier_iters, - GetIntAttr(GRB_INT_ATTR_BARITERCOUNT)); - result.mutable_solve_stats()->set_barrier_iterations(barrier_iters); + gurobi_->GetIntAttr(GRB_INT_ATTR_BARITERCOUNT)); + solve_stats.set_barrier_iterations(barrier_iters); } - if (GRBisattravailable(gurobi_model_, GRB_DBL_ATTR_NODECOUNT)) { + + if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_NODECOUNT)) { ASSIGN_OR_RETURN(const double nodes_double, - GetDoubleAttr(GRB_DBL_ATTR_NODECOUNT)); + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_NODECOUNT)); ASSIGN_OR_RETURN(const int64_t nodes, SafeInt64FromDouble(nodes_double)); - result.mutable_solve_stats()->set_node_count(nodes); + solve_stats.set_node_count(nodes); } + return solve_stats; +} +absl::StatusOr GurobiSolver::GetMipSolutions( + const ModelSolveParametersProto& model_parameters) { + int num_solutions = 0; + if (gurobi_->IsAttrAvailable(GRB_INT_ATTR_SOLCOUNT)) { + ASSIGN_OR_RETURN(num_solutions, gurobi_->GetIntAttr(GRB_INT_ATTR_SOLCOUNT)); + } + std::vector solutions; + solutions.reserve(num_solutions); for (int i = 0; i < num_solutions; ++i) { - PrimalSolutionProto* const primal_solution = result.add_primal_solutions(); - double sol_val; - std::vector grb_var_values(num_gurobi_variables_); - // TODO(user): there seems to be some kind of issue with Gurobi where - // GRB_DBL_ATTR_POOLOBJVAL is not always filled in when when there is - // one, solution, probably a bug. - if (i == 0) { - ASSIGN_OR_RETURN(sol_val, GetDoubleAttr(GRB_DBL_ATTR_OBJVAL)); - RETURN_IF_ERROR( - GetDoubleAttrArray(GRB_DBL_ATTR_X, absl::MakeSpan(grb_var_values))); - } else { - RETURN_IF_GUROBI_ERROR( - GRBsetintparam(active_env_, GRB_INT_PAR_SOLUTIONNUMBER, i)) - << "Error setting solution to " << i; - ASSIGN_OR_RETURN(sol_val, GetDoubleAttr(GRB_DBL_ATTR_POOLOBJVAL)); - RETURN_IF_ERROR( - GetDoubleAttrArray(GRB_DBL_ATTR_XN, absl::MakeSpan(grb_var_values))); - } - primal_solution->set_objective_value(sol_val); - GurobiVectorToSparseDoubleVector( - grb_var_values, variables_map_, - *primal_solution->mutable_variable_values(), - model_parameters.primal_variables_filter()); - } - // TODO(user): support getting infeasibility proofs, and basis. - // If model is LP, get dual solutions - // Note that we can ignore the reduced costs of the slack variables for ranged - // constraints because of - // go/mathopt-dev-transformations#slack-var-range-constraint - ASSIGN_OR_RETURN(const bool is_lp, IsLP()); - if (is_lp && GRBisattravailable(gurobi_model_, GRB_DBL_ATTR_PI) && - GRBisattravailable(gurobi_model_, GRB_DBL_ATTR_RC)) { - std::vector grb_constraint_duals(num_gurobi_constraints()); - RETURN_IF_ERROR(GetDoubleAttrArray(GRB_DBL_ATTR_PI, - absl::MakeSpan(grb_constraint_duals))); - DualSolutionProto* const dual_solution = result.add_dual_solutions(); - ASSIGN_OR_RETURN(const double obj_val, GetDoubleAttr(GRB_DBL_ATTR_OBJVAL)); - dual_solution->set_objective_value(obj_val); - GurobiVectorToSparseDoubleVector( - grb_constraint_duals, linear_constraints_map_, - *dual_solution->mutable_dual_values(), - model_parameters.dual_linear_constraints_filter()); + RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_SOLUTIONNUMBER, i)); - std::vector grb_reduced_cost_values(num_gurobi_variables_); - RETURN_IF_ERROR(GetDoubleAttrArray( - GRB_DBL_ATTR_RC, absl::MakeSpan(grb_reduced_cost_values))); - GurobiVectorToSparseDoubleVector(grb_reduced_cost_values, variables_map_, - *dual_solution->mutable_reduced_costs(), - model_parameters.dual_variables_filter()); + PrimalSolutionProto primal_solution; + ASSIGN_OR_RETURN(const double sol_val, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_POOLOBJVAL)); + primal_solution.set_objective_value(sol_val); + primal_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + ASSIGN_OR_RETURN( + const std::vector grb_var_values, + gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_XN, num_gurobi_variables_)); + GurobiVectorToSparseDoubleVector(grb_var_values, variables_map_, + *primal_solution.mutable_variable_values(), + model_parameters.variable_values_filter()); + *solutions.emplace_back(SolutionProto()).mutable_primal_solution() = + std::move(primal_solution); } - if (is_lp && GRBisattravailable(gurobi_model_, GRB_DBL_ATTR_UNBDRAY)) { - std::vector grb_ray_var_values(num_gurobi_variables_); - RETURN_IF_ERROR(GetDoubleAttrArray(GRB_DBL_ATTR_UNBDRAY, - absl::MakeSpan(grb_ray_var_values))); + + // Set solution claims + ASSIGN_OR_RETURN(const double best_dual_bound, GetBestDualBound()); + // Note: here the existence of a dual solution refers to a dual solution to + // some convex relaxation of the MIP. This convex relaxation can likely be + // interpreted as an LP between the LP relaxation of the MIP and the convex + // hull of feasible solutions of the MIP. However, here we only use the fact + // that best_dual_bound being finite implies the existence of the trivial + // convex relaxation given by (assuming a minimization problem with objective + // function c^T x): min{c^T x : c^T x >= best_dual_bound}. + const SolutionClaims solution_claims = { + .primal_feasible_solution_exists = num_solutions > 0, + .dual_feasible_solution_exists = std::isfinite(best_dual_bound)}; + + // Check consistency of solutions, bounds and statuses. + ASSIGN_OR_RETURN(const int grb_termination, + gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); + if (grb_termination == GRB_OPTIMAL && num_solutions == 0) { + return absl::InternalError( + "GRB_INT_ATTR_STATUS == GRB_OPTIMAL, but solution pool is empty."); + } + if (grb_termination == GRB_OPTIMAL && !std::isfinite(best_dual_bound)) { + return absl::InternalError( + "GRB_INT_ATTR_STATUS == GRB_OPTIMAL, but GRB_DBL_ATTR_OBJBOUND is " + "unavailable or infinite."); + } + + return SolutionsAndClaims{.solutions = std::move(solutions), + .solution_claims = solution_claims}; +} + +absl::StatusOr> +GurobiSolver::GetConvexPrimalSolutionIfAvailable( + const ModelSolveParametersProto& model_parameters) { + if (!gurobi_->IsAttrAvailable(GRB_DBL_ATTR_X)) { + return SolutionAndClaim{ + .solution = std::nullopt, .feasible_solution_exists = false}; + } + ASSIGN_OR_RETURN(const int grb_termination, + gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); + + // Get primal solutions if available. + ASSIGN_OR_RETURN( + const std::vector grb_var_values, + gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_X, num_gurobi_variables_)); + + PrimalSolutionProto primal_solution; + // As noted in go/gurobi-objval-bug the objective value may be missing for + // primal feasible solutions for unbounded problems. + // TODO(b/195295177): for GRB_ITERATION_LIMIT an objective value of 0.0 is + // returned which breaks LpIncompleteSolveTest.PrimalSimplexAlgorithm. Explore + // more and make simple example to file a bug. + if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_OBJVAL) && + grb_termination != GRB_ITERATION_LIMIT) { + ASSIGN_OR_RETURN(const double sol_val, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJVAL)); + primal_solution.set_objective_value(sol_val); + } else { + double objective_value = 0.0; + ASSIGN_OR_RETURN( + const std::vector linear_obj_coefs, + gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_OBJ, num_gurobi_variables_)); + for (int i = 0; i < num_gurobi_variables_; ++i) { + objective_value += linear_obj_coefs[i] * grb_var_values[i]; + } + primal_solution.set_objective_value(objective_value); + } + + primal_solution.set_feasibility_status(SOLUTION_STATUS_UNDETERMINED); + if (grb_termination == GRB_OPTIMAL) { + primal_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + } else if (grb_termination == GRB_INFEASIBLE) { + primal_solution.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE); + } else if (PrimalSolutionQualityAvailable()) { + ASSIGN_OR_RETURN(const double solution_quality, GetPrimalSolutionQuality()); + ASSIGN_OR_RETURN(const double tolerance, + gurobi_->GetDoubleParam(GRB_DBL_PAR_FEASIBILITYTOL)); + if (solution_quality <= tolerance) { + primal_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + } else { + primal_solution.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE); + } + } + + GurobiVectorToSparseDoubleVector(grb_var_values, variables_map_, + *primal_solution.mutable_variable_values(), + model_parameters.variable_values_filter()); + const bool primal_feasible_solution_exists = + (primal_solution.feasibility_status() == SOLUTION_STATUS_FEASIBLE); + return SolutionAndClaim{ + .solution = std::move(primal_solution), + .feasible_solution_exists = primal_feasible_solution_exists}; +} + +bool GurobiSolver::PrimalSolutionQualityAvailable() const { + return gurobi_->IsAttrAvailable(GRB_DBL_ATTR_CONSTR_RESIDUAL) && + gurobi_->IsAttrAvailable(GRB_DBL_ATTR_CONSTR_VIO) && + gurobi_->IsAttrAvailable(GRB_DBL_ATTR_BOUND_VIO) && + gurobi_->IsAttrAvailable(GRB_DBL_ATTR_CONSTR_SRESIDUAL) && + gurobi_->IsAttrAvailable(GRB_DBL_ATTR_CONSTR_SVIO) && + gurobi_->IsAttrAvailable(GRB_DBL_ATTR_BOUND_SVIO); +} + +absl::StatusOr GurobiSolver::GetPrimalSolutionQuality() const { + ASSIGN_OR_RETURN(const double constraint_residual, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_CONSTR_RESIDUAL)); + ASSIGN_OR_RETURN(const double constraint_violation, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_CONSTR_VIO)); + ASSIGN_OR_RETURN(const double bound_violation, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_BOUND_VIO)); + ASSIGN_OR_RETURN(const double constraint_scaled_residual, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_CONSTR_SRESIDUAL)); + ASSIGN_OR_RETURN(const double constraint_scaled_violation, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_CONSTR_SVIO)); + ASSIGN_OR_RETURN(const double bound_scaled_violation, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_BOUND_SVIO)); + return std::max({constraint_residual, constraint_violation, bound_violation, + constraint_scaled_residual, constraint_scaled_violation, + bound_scaled_violation}); +} + +absl::StatusOr GurobiSolver::GetBestPrimalBound( + const bool has_primal_feasible_solution) { + ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize()); + // We need has_primal_feasible_solution because, as noted in + // go/gurobi-objval-bug, GRB_DBL_ATTR_OBJVAL may be available and finite for + // primal infeasible solutions. + if (has_primal_feasible_solution && + gurobi_->IsAttrAvailable(GRB_DBL_ATTR_OBJVAL)) { + // TODO(b/195295177): Discuss if this should be removed. Unlike the dual + // case below, it appears infesible models do not return GRB_DBL_ATTR_OBJVAL + // equal to GRB_INFINITY (GRB_DBL_ATTR_OBJVAL is just unavailable). Hence, + // this may not be needed and may not be consistent (e.g. we should explore + // whether GRB_DBL_ATTR_OBJVAL = GRB_INFINITY may happen for a primal + // feasible solution, in which the conversion of +/-GRB_INFINITY to +/-kInf + // would not be consistent). Note that unlike the dual case removing this + // does not break any test. + ASSIGN_OR_RETURN(const double obj_val, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJVAL)); + if (std::abs(obj_val) < GRB_INFINITY) { + return obj_val; + } + } + return is_maximize ? -kInf : kInf; +} + +absl::StatusOr GurobiSolver::GetBestDualBound() { + if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_OBJBOUND)) { + ASSIGN_OR_RETURN(const double obj_bound, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJBOUND)); + // Note: Unbounded models return GRB_DBL_ATTR_OBJBOUND = GRB_INFINITY so + // the conversion of +/-GRB_INFINITY to +/-kInf is needed and consistent. + if (std::abs(obj_bound) < GRB_INFINITY) { + return obj_bound; + } + } + ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize()); + return is_maximize ? kInf : -kInf; +} + +absl::StatusOr> GurobiSolver::GetBasisIfAvailable() { + if (gurobi_->IsAttrAvailable(GRB_INT_ATTR_VBASIS) && + gurobi_->IsAttrAvailable(GRB_INT_ATTR_CBASIS)) { + ASSIGN_OR_RETURN(BasisProto basis, GetGurobiBasis()); + ASSIGN_OR_RETURN(const int grb_termination, + gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); + basis.set_basic_dual_feasibility(SOLUTION_STATUS_UNDETERMINED); + if (grb_termination == GRB_OPTIMAL) { + basis.set_basic_dual_feasibility(SOLUTION_STATUS_FEASIBLE); + } else if (grb_termination == GRB_UNBOUNDED) { + basis.set_basic_dual_feasibility(SOLUTION_STATUS_INFEASIBLE); + } + // TODO(b/195295177): double check if the move is needed + return std::move(basis); + } + return std::nullopt; +} + +absl::StatusOr GurobiSolver::GetLpSolution( + const ModelSolveParametersProto& model_parameters) { + ASSIGN_OR_RETURN(auto primal_solution_and_claim, + GetConvexPrimalSolutionIfAvailable(model_parameters)); + ASSIGN_OR_RETURN(auto dual_solution_and_claim, + GetLpDualSolutionIfAvailable(model_parameters)); + ASSIGN_OR_RETURN(auto basis, GetBasisIfAvailable()); + const SolutionClaims solution_claims = { + .primal_feasible_solution_exists = + primal_solution_and_claim.feasible_solution_exists, + .dual_feasible_solution_exists = + dual_solution_and_claim.feasible_solution_exists}; + + if (!primal_solution_and_claim.solution.has_value() && + !dual_solution_and_claim.solution.has_value() && !basis.has_value()) { + return SolutionsAndClaims{.solution_claims = solution_claims}; + } + SolutionsAndClaims solution_and_claims{.solution_claims = solution_claims}; + SolutionProto& solution = + solution_and_claims.solutions.emplace_back(SolutionProto()); + if (primal_solution_and_claim.solution.has_value()) { + *solution.mutable_primal_solution() = + std::move(*primal_solution_and_claim.solution); + } + if (dual_solution_and_claim.solution.has_value()) { + *solution.mutable_dual_solution() = + std::move(*dual_solution_and_claim.solution); + } + if (basis.has_value()) { + *solution.mutable_basis() = std::move(*basis); + } + return solution_and_claims; +} + +absl::StatusOr> +GurobiSolver::GetLpDualSolutionIfAvailable( + const ModelSolveParametersProto& model_parameters) { + if (!gurobi_->IsAttrAvailable(GRB_DBL_ATTR_PI) || + !gurobi_->IsAttrAvailable(GRB_DBL_ATTR_RC)) { + return SolutionAndClaim{ + .solution = std::nullopt, .feasible_solution_exists = false}; + } + + // Note that we can ignore the reduced costs of the slack variables for + // ranged constraints because of + // go/mathopt-dev-transformations#slack-var-range-constraint + DualSolutionProto dual_solution; + bool dual_feasible_solution_exists = false; + ASSIGN_OR_RETURN( + const std::vector grb_constraint_duals, + gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_PI, num_gurobi_constraints())); + GurobiVectorToSparseDoubleVector(grb_constraint_duals, + linear_constraints_map_, + *dual_solution.mutable_dual_values(), + model_parameters.dual_values_filter()); + + ASSIGN_OR_RETURN( + const std::vector grb_reduced_cost_values, + gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_RC, num_gurobi_variables_)); + GurobiVectorToSparseDoubleVector(grb_reduced_cost_values, variables_map_, + *dual_solution.mutable_reduced_costs(), + model_parameters.reduced_costs_filter()); + + ASSIGN_OR_RETURN(const int grb_termination, + gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); + if (grb_termination == GRB_OPTIMAL && + gurobi_->IsAttrAvailable(GRB_DBL_ATTR_OBJVAL)) { + ASSIGN_OR_RETURN(const double obj_val, + gurobi_->GetDoubleAttr(GRB_DBL_ATTR_OBJVAL)); + dual_solution.set_objective_value(obj_val); + } + // TODO(b/195295177): explore using GRB_DBL_ATTR_OBJBOUND to set the dual + // objective. As described in go/gurobi-objval-bug, this could provide the + // dual objective in some cases. + + dual_solution.set_feasibility_status(SOLUTION_STATUS_UNDETERMINED); + if (grb_termination == GRB_OPTIMAL) { + dual_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + dual_feasible_solution_exists = true; + } else if (grb_termination == GRB_UNBOUNDED) { + dual_solution.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE); + } + // TODO(b/195295177): We could use gurobi's dual solution quality measures + // for further upgrade the dual feasibility but it likely is only useful + // for phase II of dual simplex because: + // * the quality measures seem to evaluate if the basis is dual feasible + // so for primal simplex we would not improve over checking + // GRB_OPTIMAL. + // * for phase I dual simplex we cannot rely on the quality measures + // because of go/gurobi-solution-quality-bug. + // We could also use finiteness of GRB_DBL_ATTR_OBJBOUND to deduce dual + // feasibility as described in go/gurobi-objval-bug. + + // Note: as shown in go/gurobi-objval-bug, GRB_DBL_ATTR_OBJBOUND can + // sometimes provide the objective value of a sub-optimal dual feasible + // solution. Here we only use it to possibly update + // dual_feasible_solution_exists (Otherwise + // StatusTest.PrimalInfeasibleAndDualFeasible for pure dual simplex would + // fail because go/gurobi-solution-quality-bug prevents us from certifying + // feasibility of the dual solution found in this case). + ASSIGN_OR_RETURN(const double best_dual_bound, GetBestDualBound()); + if (dual_feasible_solution_exists || std::isfinite(best_dual_bound)) { + dual_feasible_solution_exists = true; + } else if (grb_termination == GRB_OPTIMAL) { + return absl::InternalError( + "GRB_INT_ATTR_STATUS == GRB_OPTIMAL, but GRB_DBL_ATTR_OBJBOUND is " + "unavailable or infinite, and no dual feasible solution is returned"); + } + return SolutionAndClaim{ + .solution = std::move(dual_solution), + .feasible_solution_exists = dual_feasible_solution_exists}; +} + +absl::Status GurobiSolver::FillRays( + const ModelSolveParametersProto& model_parameters, + SolveResultProto& result) { + ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize()); + if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_UNBDRAY) && + num_gurobi_variables_ > 0) { + ASSIGN_OR_RETURN(const std::vector grb_ray_var_values, + gurobi_->GetDoubleAttrArray(GRB_DBL_ATTR_UNBDRAY, + num_gurobi_variables_)); PrimalRayProto* const primal_ray = result.add_primal_rays(); - GurobiVectorToSparseDoubleVector( - grb_ray_var_values, variables_map_, - *primal_ray->mutable_variable_values(), - model_parameters.primal_variables_filter()); + GurobiVectorToSparseDoubleVector(grb_ray_var_values, variables_map_, + *primal_ray->mutable_variable_values(), + model_parameters.variable_values_filter()); } - if (is_lp && GRBisattravailable(gurobi_model_, GRB_DBL_ATTR_FARKASDUAL)) { + if (gurobi_->IsAttrAvailable(GRB_DBL_ATTR_FARKASDUAL) && + num_gurobi_constraints() + num_gurobi_variables_ > 0) { ASSIGN_OR_RETURN( DualRayProto dual_ray, - GetGurobiDualRay(model_parameters.dual_linear_constraints_filter(), - model_parameters.dual_variables_filter(), - is_maximize)); + GetGurobiDualRay(model_parameters.dual_values_filter(), + model_parameters.reduced_costs_filter(), is_maximize)); result.mutable_dual_rays()->Add(std::move(dual_ray)); } - if (is_lp && GRBisattravailable(gurobi_model_, GRB_INT_ATTR_VBASIS) && - GRBisattravailable(gurobi_model_, GRB_INT_ATTR_CBASIS)) { - ASSIGN_OR_RETURN(BasisProto basis, GetGurobiBasis()); - result.mutable_basis()->Add(std::move(basis)); + return absl::OkStatus(); +} + +absl::StatusOr GurobiSolver::GetQpSolution( + const ModelSolveParametersProto& model_parameters) { + ASSIGN_OR_RETURN((auto [primal_solution, found_primal_feasible_solution]), + GetConvexPrimalSolutionIfAvailable(model_parameters)); + + // TODO(b/195295177): Update, seems GRB_DBL_ATTR_OBJBOUND is unavailable + // even for GRB_OPTIMAL, so the code below will only give a finite bound + // and a dual feasible status for GRB_OPTIMAL. + ASSIGN_OR_RETURN(const int grb_termination, + gurobi_->GetIntAttr(GRB_INT_ATTR_STATUS)); + bool dual_feasible_solution_exists = false; + ASSIGN_OR_RETURN(double best_dual_bound, GetBestDualBound()); + if (grb_termination == GRB_OPTIMAL || std::isfinite(best_dual_bound)) { + dual_feasible_solution_exists = true; } - return absl::OkStatus(); -} + // Basis information is available when Gurobi uses QP simplex. As of v9.1 this + // is not the default [1], so a user will need to explicitly set the Method + // parameter in order for the following call to do anything interesting. + // [1] https://www.gurobi.com/documentation/9.1/refman/method.html + ASSIGN_OR_RETURN(auto basis, GetBasisIfAvailable()); -absl::Status GurobiSolver::SetParameter(const std::string& param_name, - const std::string& param_value) { - RETURN_IF_GUROBI_ERROR( - GRBsetparam(active_env_, param_name.c_str(), param_value.c_str())) - << "Error setting parameter: " << param_name << " to value " - << param_value; - return absl::OkStatus(); -} + const SolutionClaims solution_claims = { + .primal_feasible_solution_exists = found_primal_feasible_solution, + .dual_feasible_solution_exists = dual_feasible_solution_exists}; -absl::Status GurobiSolver::ResetParameters() { - RETURN_IF_GUROBI_ERROR(GRBresetparams(active_env_)) - << "Error reseting parameters"; - return absl::OkStatus(); + if (!primal_solution.has_value() && !basis.has_value()) { + return GurobiSolver::SolutionsAndClaims{.solution_claims = solution_claims}; + } + SolutionsAndClaims solution_and_claims{.solution_claims = solution_claims}; + SolutionProto& solution = + solution_and_claims.solutions.emplace_back(SolutionProto()); + if (primal_solution.has_value()) { + *solution.mutable_primal_solution() = std::move(*primal_solution); + } + if (basis.has_value()) { + *solution.mutable_basis() = std::move(*basis); + } + return solution_and_claims; } std::vector GurobiSolver::SetParameters( const SolveParametersProto& parameters) { - CHECK(active_env_ != nullptr); - const GurobiParametersProto gurobi_parameters = MergeParameters( - parameters.common_parameters(), parameters.gurobi_parameters()); + const GurobiParametersProto gurobi_parameters = MergeParameters(parameters); std::vector parameter_errors; for (const GurobiParametersProto::Parameter& parameter : gurobi_parameters.parameters()) { absl::Status param_status = - SetParameter(parameter.name(), parameter.value()); + gurobi_->SetParam(parameter.name().c_str(), parameter.value()); if (!param_status.ok()) { parameter_errors.emplace_back(std::move(param_status)); } @@ -814,40 +1174,29 @@ std::vector GurobiSolver::SetParameters( absl::Status GurobiSolver::AddNewVariables( const VariablesProto& new_variables) { - CHECK(gurobi_model_ != nullptr); const int num_new_variables = new_variables.lower_bounds().size(); - std::vector variable_type(num_new_variables); - std::vector variable_names; - const bool has_variable_names = !new_variables.names().empty(); - if (has_variable_names) { - variable_names.reserve(num_new_variables); - } for (int j = 0; j < num_new_variables; ++j) { const VariableId id = new_variables.ids(j); gtl::InsertOrDie(&variables_map_, id, j + num_gurobi_variables_); variable_type[j] = new_variables.integers(j) ? GRB_INTEGER : GRB_CONTINUOUS; - if (has_variable_names) { - variable_names.emplace_back( - const_cast(new_variables.names(j).c_str())); - } } - - RETURN_IF_GUROBI_ERROR(GRBaddvars( - gurobi_model_, /*numvars=*/num_new_variables, - /*numnz=*/0, /*vbeg=*/nullptr, /*vind=*/nullptr, /*vval=*/nullptr, - /*obj=*/nullptr, - /*lb=*/const_cast(new_variables.lower_bounds().data()), - /*ub=*/const_cast(new_variables.upper_bounds().data()), - /*vtype=*/variable_type.data(), - /*varnames=*/has_variable_names ? variable_names.data() : nullptr)); + // We need to copy the names, RepeatedPtrField cannot be converted to + // absl::Span. + const std::vector variable_names = + RepeatedPtrFieldToVec(new_variables.names()); + RETURN_IF_ERROR(gurobi_->AddVars( + /*obj=*/{}, + /*lb=*/new_variables.lower_bounds(), + /*ub=*/new_variables.upper_bounds(), + /*vtype=*/variable_type, variable_names)); num_gurobi_variables_ += num_new_variables; return absl::OkStatus(); } // Given a vector of pairs add a slack -// variable for each of the constraints in the underlying `gurobi_model_` +// variable for each of the constraints in the underlying `gurobi_` // using the referenced bounds. absl::Status GurobiSolver::AddNewSlacks( const std::vector& new_slacks) { @@ -862,9 +1211,10 @@ absl::Status GurobiSolver::AddNewSlacks( return absl::OkStatus(); } // Build the D matrix in CSC format. - std::vector column_non_zeros(num_slacks, -1.0); + const std::vector column_non_zeros(num_slacks, -1.0); std::vector lower_bounds; std::vector upper_bounds; + const std::vector vtypes(num_slacks, GRB_CONTINUOUS); std::vector row_indices; std::vector column_non_zero_begin; column_non_zero_begin.reserve(num_slacks); @@ -880,27 +1230,24 @@ absl::Status GurobiSolver::AddNewSlacks( column_non_zero_begin.emplace_back(k); } // Add variables to the underlying model. - RETURN_IF_GUROBI_ERROR(GRBaddvars( - /*model=*/gurobi_model_, /*numvars=*/num_slacks, /*numnz=*/num_slacks, - /*vbeg=*/column_non_zero_begin.data(), /*vind=*/row_indices.data(), - /*vval=*/column_non_zeros.data(), /*obj=*/nullptr, - /*lb=*/lower_bounds.data(), /*ub=*/upper_bounds.data(), - /*vtype=*/nullptr, /*varnames=*/nullptr)); + RETURN_IF_ERROR(gurobi_->AddVars(/*vbegin=*/column_non_zero_begin, + /*vind=*/row_indices, + /*vval=*/column_non_zeros, /*obj=*/{}, + /*lb=*/lower_bounds, /*ub=*/upper_bounds, + /*vtype=*/vtypes, /*names=*/{})); num_gurobi_variables_ += num_slacks; return absl::OkStatus(); } absl::Status GurobiSolver::AddNewConstraints( const LinearConstraintsProto& constraints) { - CHECK(gurobi_model_ != nullptr); const int num_model_constraints = num_gurobi_constraints(); const int num_new_constraints = constraints.lower_bounds().size(); - const bool has_constraint_names = !constraints.names().empty(); - std::vector constraint_names; - if (has_constraint_names) { - constraint_names.reserve(num_new_constraints); - } + // We need to copy the names, RepeatedPtrField cannot be converted to + // absl::Span. + const std::vector constraint_names = + RepeatedPtrFieldToVec(constraints.names()); // Constraints are translated into: // 1. ax <= upper_bound (if lower bound <= -GRB_INFINITY, and upper_bound // is finite and less than GRB_INFINITY) @@ -945,19 +1292,10 @@ absl::Status GurobiSolver::AddNewConstraints( } constraint_rhs.emplace_back(rhs); constraint_sense.emplace_back(sense); - if (has_constraint_names) { - constraint_names.emplace_back( - const_cast(constraints.names(i).c_str())); - } } // Add all constraints in one call. - RETURN_IF_GUROBI_ERROR(GRBaddconstrs( - gurobi_model_, /*numconstrs=*/num_new_constraints, - /*numnz=*/0, /*cbeg=*/nullptr, /*cind=*/nullptr, /*cval=*/nullptr, - /*sense=*/constraint_sense.data(), /*rhs=*/constraint_rhs.data(), - /*constrnames=*/ - has_constraint_names ? constraint_names.data() : nullptr)); - + RETURN_IF_ERROR( + gurobi_->AddConstrs(constraint_sense, constraint_rhs, constraint_names)); // Add slacks for true ranged constraints (if needed) if (!new_slacks.empty()) { RETURN_IF_ERROR(AddNewSlacks(new_slacks)); @@ -967,7 +1305,6 @@ absl::Status GurobiSolver::AddNewConstraints( absl::Status GurobiSolver::ChangeCoefficients( const SparseDoubleMatrixProto& matrix) { - CHECK(gurobi_model_ != nullptr); const int num_coefficients = matrix.row_ids().size(); std::vector row_index(num_coefficients); std::vector col_index(num_coefficients); @@ -976,56 +1313,41 @@ absl::Status GurobiSolver::ChangeCoefficients( linear_constraints_map_.at(matrix.row_ids(k)).constraint_index; col_index[k] = variables_map_.at(matrix.column_ids(k)); } - double* const new_values = const_cast(matrix.coefficients().data()); - RETURN_IF_GUROBI_ERROR( - GRBchgcoeffs(/*model=*/gurobi_model_, - /*cnt=*/num_coefficients, /*cind=*/row_index.data(), - /*vind=*/col_index.data(), /*val=*/new_values)); - return absl::OkStatus(); + return gurobi_->ChgCoeffs(row_index, col_index, matrix.coefficients()); } absl::Status GurobiSolver::UpdateDoubleListAttribute( const SparseDoubleVectorProto& update, const char* attribute_name, const IdHashMap& id_hash_map) { - CHECK(gurobi_model_ != nullptr); - const int sparse_length = update.ids().size(); - if (sparse_length == 0) { + if (update.ids_size() == 0) { return absl::OkStatus(); } - - std::vector index(sparse_length); - for (int k = 0; k < sparse_length; ++k) { - index[k] = id_hash_map.at(update.ids(k)); + std::vector index; + index.reserve(update.ids_size()); + for (const int id : update.ids()) { + index.push_back(id_hash_map.at(id)); } - - double* const new_values = const_cast(update.values().data()); - RETURN_IF_GUROBI_ERROR( - GRBsetdblattrlist(/*model=*/gurobi_model_, /*attrname=*/attribute_name, - /*len=*/sparse_length, /*ind=*/index.data(), - /*newvalues=*/new_values)); - return absl::OkStatus(); + return gurobi_->SetDoubleAttrList(attribute_name, index, update.values()); } -GurobiSolver::~GurobiSolver() { - if (gurobi_model_ != nullptr) { - std::string free_error = GurobiErrorMessage(GRBfreemodel(gurobi_model_)); - if (!free_error.empty()) { - LOG(ERROR) << free_error; - } +absl::Status GurobiSolver::UpdateInt32ListAttribute( + const SparseInt32VectorProto& update, const char* attribute_name, + const IdHashMap& id_hash_map) { + if (update.ids_size() == 0) { + return absl::OkStatus(); } - if (master_env_ != nullptr) { - GRBfreeenv(master_env_); + std::vector index; + index.reserve(update.ids_size()); + for (const int id : update.ids()) { + index.push_back(id_hash_map.at(id)); } - active_env_ = nullptr; - gurobi_model_ = nullptr; - master_env_ = nullptr; - VLOG(3) << "Freed unmanaged Gurobi pointers"; + return gurobi_->SetIntAttrList(attribute_name, index, update.values()); } absl::Status GurobiSolver::LoadModel(const ModelProto& input_model) { - CHECK(gurobi_model_ != nullptr); - RETURN_IF_GUROBI_ERROR(GRBsetstrattr(gurobi_model_, GRB_STR_ATTR_MODELNAME, - input_model.name().c_str())); + CHECK(gurobi_ != nullptr); + RETURN_IF_ERROR( + gurobi_->SetStringAttr(GRB_STR_ATTR_MODELNAME, input_model.name())); RETURN_IF_ERROR(AddNewVariables(input_model.variables())); RETURN_IF_ERROR(AddNewConstraints(input_model.linear_constraints())); @@ -1034,14 +1356,66 @@ absl::Status GurobiSolver::LoadModel(const ModelProto& input_model) { const int model_sense = input_model.objective().maximize() ? GRB_MAXIMIZE : GRB_MINIMIZE; - RETURN_IF_GUROBI_ERROR( - GRBsetintattr(gurobi_model_, GRB_INT_ATTR_MODELSENSE, model_sense)); - RETURN_IF_GUROBI_ERROR(GRBsetdblattr(gurobi_model_, GRB_DBL_ATTR_OBJCON, - input_model.objective().offset())); + RETURN_IF_ERROR(gurobi_->SetIntAttr(GRB_INT_ATTR_MODELSENSE, model_sense)); + RETURN_IF_ERROR(gurobi_->SetDoubleAttr(GRB_DBL_ATTR_OBJCON, + input_model.objective().offset())); + RETURN_IF_ERROR( UpdateDoubleListAttribute(input_model.objective().linear_coefficients(), GRB_DBL_ATTR_OBJ, variables_map_)); + RETURN_IF_ERROR(ResetQuadraticObjectiveTerms( + input_model.objective().quadratic_coefficients())); + return absl::OkStatus(); +} +absl::Status GurobiSolver::ResetQuadraticObjectiveTerms( + const SparseDoubleMatrixProto& terms) { + quadratic_objective_coefficients_.clear(); + RETURN_IF_ERROR(gurobi_->DelQ()); + const int num_terms = terms.row_ids().size(); + if (num_terms > 0) { + std::vector first_var_index(num_terms); + std::vector second_var_index(num_terms); + for (int k = 0; k < num_terms; ++k) { + const VariableId row_id = terms.row_ids(k); + const VariableId column_id = terms.column_ids(k); + first_var_index[k] = variables_map_.at(row_id); + second_var_index[k] = variables_map_.at(column_id); + quadratic_objective_coefficients_[{row_id, column_id}] = + terms.coefficients(k); + } + RETURN_IF_ERROR(gurobi_->AddQpTerms(first_var_index, second_var_index, + terms.coefficients())); + } + return absl::OkStatus(); +} + +absl::Status GurobiSolver::UpdateQuadraticObjectiveTerms( + const SparseDoubleMatrixProto& terms) { + CHECK(gurobi_ != nullptr); + const int num_terms = terms.row_ids().size(); + if (num_terms > 0) { + std::vector first_var_index(num_terms); + std::vector second_var_index(num_terms); + std::vector coefficient_updates(num_terms); + for (int k = 0; k < num_terms; ++k) { + const VariableId row_id = terms.row_ids(k); + const VariableId column_id = terms.column_ids(k); + first_var_index[k] = variables_map_.at(row_id); + second_var_index[k] = variables_map_.at(column_id); + const std::pair qp_term_key(row_id, column_id); + const double new_coefficient = terms.coefficients(k); + // Gurobi will maintain any existing quadratic coefficients unless we + // call GRBdelq (which we don't). So, since stored entries in terms + // specify the target coefficients, we need to compute the difference from + // the existing coefficient with Gurobi, if any. + coefficient_updates[k] = + new_coefficient - quadratic_objective_coefficients_[qp_term_key]; + quadratic_objective_coefficients_[qp_term_key] = new_coefficient; + } + RETURN_IF_ERROR(gurobi_->AddQpTerms(first_var_index, second_var_index, + coefficient_updates)); + } return absl::OkStatus(); } @@ -1186,24 +1560,16 @@ absl::Status GurobiSolver::UpdateLinearConstraints( // Pass down changes to Gurobi. if (!rhs_index.empty()) { - RETURN_IF_GUROBI_ERROR(GRBsetdblattrlist( - /*model=*/gurobi_model_, /*attrname=*/GRB_DBL_ATTR_RHS, - /*len=*/rhs_index.size(), /*ind=*/rhs_index.data(), - /*newvalues=*/rhs_data.data())); - RETURN_IF_GUROBI_ERROR(GRBsetcharattrlist( - /*model=*/gurobi_model_, /*attrname=*/GRB_CHAR_ATTR_SENSE, - /*len=*/rhs_index.size(), /*ind=*/rhs_index.data(), - /*newvalues=*/sense_data.data())); + RETURN_IF_ERROR( + gurobi_->SetDoubleAttrList(GRB_DBL_ATTR_RHS, rhs_index, rhs_data)); + RETURN_IF_ERROR( + gurobi_->SetCharAttrList(GRB_CHAR_ATTR_SENSE, rhs_index, sense_data)); } // rhs changes if (!bound_index.empty()) { - RETURN_IF_GUROBI_ERROR(GRBsetdblattrlist( - /*model=*/gurobi_model_, /*attrname=*/GRB_DBL_ATTR_LB, - /*len=*/bound_index.size(), /*ind=*/bound_index.data(), - /*newvalues=*/lower_bound_data.data())); - RETURN_IF_GUROBI_ERROR(GRBsetdblattrlist( - /*model=*/gurobi_model_, /*attrname=*/GRB_DBL_ATTR_UB, - /*len=*/bound_index.size(), /*ind=*/bound_index.data(), - /*newvalues=*/upper_bound_data.data())); + RETURN_IF_ERROR(gurobi_->SetDoubleAttrList(GRB_DBL_ATTR_LB, bound_index, + lower_bound_data)); + RETURN_IF_ERROR(gurobi_->SetDoubleAttrList(GRB_DBL_ATTR_UB, bound_index, + upper_bound_data)); } // Slack bound changes. if (!new_slacks.empty()) { @@ -1263,8 +1629,6 @@ absl::Status GurobiSolver::UpdateGurobiIndices() { } absl::Status GurobiSolver::Update(const ModelUpdateProto& model_update) { - CHECK(gurobi_model_ != nullptr); - RETURN_IF_ERROR(AddNewVariables(model_update.new_variables())); RETURN_IF_ERROR(AddNewConstraints(model_update.new_linear_constraints())); @@ -1276,20 +1640,21 @@ absl::Status GurobiSolver::Update(const ModelUpdateProto& model_update) { const int model_sense = model_update.objective_updates().direction_update() ? GRB_MAXIMIZE : GRB_MINIMIZE; - RETURN_IF_GUROBI_ERROR( - GRBsetintattr(gurobi_model_, GRB_INT_ATTR_MODELSENSE, model_sense)); + RETURN_IF_ERROR(gurobi_->SetIntAttr(GRB_INT_ATTR_MODELSENSE, model_sense)); } if (model_update.objective_updates().has_offset_update()) { - RETURN_IF_GUROBI_ERROR( - GRBsetdblattr(gurobi_model_, GRB_DBL_ATTR_OBJCON, - model_update.objective_updates().offset_update())); + RETURN_IF_ERROR(gurobi_->SetDoubleAttr( + GRB_DBL_ATTR_OBJCON, model_update.objective_updates().offset_update())); } RETURN_IF_ERROR(UpdateDoubleListAttribute( model_update.objective_updates().linear_coefficients(), GRB_DBL_ATTR_OBJ, variables_map_)); + RETURN_IF_ERROR(UpdateQuadraticObjectiveTerms( + model_update.objective_updates().quadratic_coefficients())); + RETURN_IF_ERROR( UpdateDoubleListAttribute(model_update.variable_updates().lower_bounds(), GRB_DBL_ATTR_LB, variables_map_)); @@ -1301,18 +1666,37 @@ absl::Status GurobiSolver::Update(const ModelUpdateProto& model_update) { if (model_update.variable_updates().has_integers()) { const SparseBoolVectorProto& update = model_update.variable_updates().integers(); - const int sparse_length = update.ids().size(); - std::vector index(sparse_length); - std::vector value(sparse_length); - for (int k = 0; k < sparse_length; ++k) { - index[k] = variables_map_.at(update.ids(k)); - value[k] = update.values(k) ? GRB_INTEGER : GRB_CONTINUOUS; + std::vector index; + index.reserve(update.ids_size()); + for (int id : update.ids()) { + index.push_back(variables_map_.at(id)); } - RETURN_IF_GUROBI_ERROR(GRBsetcharattrlist( - gurobi_model_, /*attrname=*/GRB_CHAR_ATTR_VTYPE, /*len=*/sparse_length, - /*ind=*/index.data(), /*newvalues=*/value.data())); + std::vector value; + value.reserve(update.values_size()); + for (const bool val : update.values()) { + value.push_back(val ? GRB_INTEGER : GRB_CONTINUOUS); + } + RETURN_IF_ERROR( + gurobi_->SetCharAttrList(GRB_CHAR_ATTR_VTYPE, index, value)); } + // Now we update quadratic_objective_coefficients_, removing any terms where + // either or both of the involved variables are about to be deleted. + const absl::flat_hash_set variable_ids_to_be_deleted( + model_update.deleted_variable_ids().begin(), + model_update.deleted_variable_ids().end()); + // NOTE: Introducing more state and complexity should speed this up, but we + // opt for the simpler approach for now. + for (auto it = quadratic_objective_coefficients_.cbegin(); + it != quadratic_objective_coefficients_.cend(); + /*incremented in loop*/) { + if (variable_ids_to_be_deleted.contains(it->first.first) || + variable_ids_to_be_deleted.contains(it->first.second)) { + quadratic_objective_coefficients_.erase(it++); + } else { + ++it; + } + } // We cache all Gurobi variables and constraint indices that must be deleted, // and perform deletions at the end of the update call. std::vector deleted_variables_index; @@ -1348,23 +1732,18 @@ absl::Status GurobiSolver::Update(const ModelUpdateProto& model_update) { // After that we must update the model so that sequence of updates don't // interfere with one-another. if (!deleted_constraints_index.empty()) { - RETURN_IF_GUROBI_ERROR( - GRBdelconstrs(/*model=*/gurobi_model_, - /*len=*/deleted_constraints_index.size(), - /*ind=*/deleted_constraints_index.data())); + RETURN_IF_ERROR(gurobi_->DelConstrs(deleted_constraints_index)); } if (!deleted_variables_index.empty()) { - RETURN_IF_GUROBI_ERROR(GRBdelvars(/*model=*/gurobi_model_, - /*len=*/deleted_variables_index.size(), - /*ind=*/deleted_variables_index.data())); + RETURN_IF_ERROR(gurobi_->DelVars(deleted_variables_index)); num_gurobi_variables_ -= deleted_variables_index.size(); } // If we removed variables or constraints we must flush all pending changes // to synchronize the number of variables and constraints with the Gurobi // model. - RETURN_IF_GUROBI_ERROR(GRBupdatemodel(gurobi_model_)); + RETURN_IF_ERROR(gurobi_->UpdateModel()); // Regenerate indices. RETURN_IF_ERROR(UpdateGurobiIndices()); @@ -1372,139 +1751,173 @@ absl::Status GurobiSolver::Update(const ModelUpdateProto& model_update) { } absl::StatusOr> GurobiSolver::New( - const ModelProto& input_model, const SolverInitializerProto& initializer) { + const ModelProto& input_model, const SolverInterface::InitArgs& init_args) { if(!GurobiIsCorrectlyInstalled()) { return absl::InvalidArgumentError("Gurobi is not correctly installed."); } - auto gurobi_data = absl::WrapUnique(new GurobiSolver); - RETURN_IF_ERROR(gurobi_data->LoadEnvironment()); - RETURN_IF_ERROR(gurobi_data->LoadModel(input_model)); - return gurobi_data; -} - -int GurobiSolver::GurobiCallback(GRBmodel* model, void* cbdata, int where, - void* usrdata) { - DCHECK_NE(usrdata, nullptr); - DCHECK_NE(model, nullptr); - auto gurobi_cb_data = static_cast(usrdata); - // NOTE: if a previous callback failed, we never run the callback again. It - // is the responsibility of the failing call to GurobiCallbackImpl to request - // early termination. - if (!gurobi_cb_data->status.ok()) { - return kGrbOk; - } - gurobi_cb_data->status = - GurobiCallbackImpl(model, cbdata, where, gurobi_cb_data->callback_input, - gurobi_cb_data->message_callback_data); - if (!gurobi_cb_data->status.ok()) { - return GRB_ERROR_CALLBACK; - } - return kGrbOk; + ASSIGN_OR_RETURN(std::unique_ptr gurobi, + GurobiFromInitArgs(init_args)); + auto gurobi_solver = absl::WrapUnique(new GurobiSolver(std::move(gurobi))); + RETURN_IF_ERROR(gurobi_solver->LoadModel(input_model)); + return gurobi_solver; } absl::StatusOr> GurobiSolver::RegisterCallback(const CallbackRegistrationProto& registration, - Callback cb, const absl::Time start) { + const Callback cb, + const MessageCallback message_cb, + const absl::Time start, + SolveInterrupter* const local_interrupter) { const absl::flat_hash_set events = EventSet(registration); + + // Note that IS_MIP does not necessarily mean the problem has integer + // variables. Please refer to Gurobi's doc for details: + // https://www.gurobi.com/documentation/9.1/refman/ismip.html. + // + // Here we assume that we get MIP related events and use a MIP solving + // stragegy when IS_MIP is true. + ASSIGN_OR_RETURN(const int is_mip, gurobi_->GetIntAttr(GRB_INT_ATTR_IS_MIP)); + + RETURN_IF_ERROR(CheckRegisteredCallbackEvents( + registration, is_mip ? SupportedMIPEvents() : SupportedLPEvents())) + << "for a " << (is_mip ? "MIP" : "LP") << " model"; + // Set Gurobi parameters. - if (events.contains(CALLBACK_EVENT_MESSAGE)) { + if (message_cb != nullptr) { // Disable logging messages to the console the user wants to handle // messages. - RETURN_IF_ERROR(SetParameter(GRB_INT_PAR_LOGTOCONSOLE, "0")); + RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_LOGTOCONSOLE, 0)); } if (registration.add_cuts() || registration.add_lazy_constraints()) { // This is to signal the solver presolve to limit primal transformations // that precludes crushing cuts to the presolved model. - RETURN_IF_ERROR(SetParameter(GRB_INT_PAR_PRECRUSH, "1")); + RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_PRECRUSH, 1)); } if (registration.add_lazy_constraints()) { // This is needed so that the solver knows that some presolve reductions // can not be performed safely. - RETURN_IF_ERROR(SetParameter(GRB_INT_PAR_LAZYCONSTRAINTS, "1")); + RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_LAZYCONSTRAINTS, 1)); } - return absl::make_unique(GurobiCallbackInput{ - .user_cb = cb, - .variable_ids = variables_map_, - .num_gurobi_vars = num_gurobi_variables_, - .events = EventToGurobiWhere(events), - .mip_solution_filter = registration.mip_solution_filter(), - .mip_node_filter = registration.mip_node_filter(), - .start = start}); + return absl::make_unique( + GurobiCallbackInput{ + .user_cb = cb, + .message_cb = message_cb, + .variable_ids = variables_map_, + .num_gurobi_vars = num_gurobi_variables_, + .events = EventToGurobiWhere(events), + .mip_solution_filter = registration.mip_solution_filter(), + .mip_node_filter = registration.mip_node_filter(), + .start = start}, + local_interrupter); } absl::StatusOr GurobiSolver::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(); - SolveResultProto solve_result; // We must set the parameters before calling RegisterCallback since it changes // some parameters depending on the callback registration. const std::vector param_errors = SetParameters(parameters); + std::vector warnings; if (!param_errors.empty()) { - if (parameters.common_parameters().strictness().bad_parameter()) { + if (parameters.strictness().bad_parameter()) { return absl::InvalidArgumentError(JoinErrors(param_errors)); } else { for (const absl::Status& param_error : param_errors) { - solve_result.add_warnings(std::string(param_error.message())); + warnings.push_back(std::string(param_error.message())); } } } + // We use a local interrupter that will triggers the calls to GRBterminate() + // when either the user interrupter is triggered or when a callback returns a + // true `terminate`. + std::unique_ptr local_interrupter; + if (cb != nullptr || interrupter != nullptr) { + local_interrupter = std::make_unique(); + } + const ScopedSolveInterrupterCallback scoped_terminate_callback( + local_interrupter.get(), [&]() { + // Make an immediate call to GRBterminate() as soon as this interrupter + // is triggered (which may immediately happen in the code below when it + // is chained with the optional user interrupter). + // + // This call may happen too early. This is not an issue since we will + // repeat this call at each call of the Gurobi callback. See the comment + // in GurobiCallbackImpl() for details. + gurobi_->Terminate(); + }); + + // Chain the user interrupter to the local interrupter. If/when the user + // interrupter is triggered, this triggers the local interrupter. This may + // happen immediately if the user interrupter is already triggered. + // + // The local interrupter can also be triggered by a callback returning a true + // `terminate`. + const ScopedSolveInterrupterCallback scoped_chaining_callback( + interrupter, [&]() { local_interrupter->Interrupt(); }); + // TODO(user): any warnings above will be lost if we get an error Status // below, reconsider the Solve API. - std::unique_ptr gurobi_cb_data; - if (cb != nullptr) { - ASSIGN_OR_RETURN(gurobi_cb_data, - RegisterCallback(callback_registration, cb, start)); - RETURN_IF_GUROBI_ERROR(GRBsetcallbackfunc(gurobi_model_, GurobiCallback, - gurobi_cb_data.get())); - } - auto callback_cleanup = absl::MakeCleanup([&]() { - if (cb != nullptr) { - GRBsetcallbackfunc(gurobi_model_, nullptr, nullptr); - } - }); - // Need to run GRBupdatemodel before setting basis and getting the obj sense. - CHECK(gurobi_model_ != nullptr); - RETURN_IF_GUROBI_ERROR(GRBupdatemodel(gurobi_model_)); - - ASSIGN_OR_RETURN(const int obj_sense, GetIntAttr(GRB_INT_ATTR_MODELSENSE)); - const bool is_maximize = obj_sense == GRB_MAXIMIZE; + // Need to run GRBupdatemodel before registering callbacks (to test if the + // problem is a MIP), setting basis and getting the obj sense. + RETURN_IF_ERROR(gurobi_->UpdateModel()); if (model_parameters.has_initial_basis()) { RETURN_IF_ERROR(SetGurobiBasis(model_parameters.initial_basis())); } - - const int gurobi_error = GRBoptimize(gurobi_model_); - // Check for callback errors when GurobiSolver::GurobiCallback signals their - // existence through GRB_ERROR_CALLBACK and when GRBterminate is called (in - // which case gurobi_error could also be kGrbOk) - if (gurobi_error == kGrbOk && gurobi_cb_data != nullptr && - gurobi_cb_data->status.ok()) { - gurobi_cb_data->status = GurobiCallbackImplFlush( - gurobi_cb_data->callback_input, gurobi_cb_data->message_callback_data); + RETURN_IF_ERROR(gurobi_->SetIntAttr(GRB_INT_ATTR_NUMSTART, + model_parameters.solution_hints_size())); + for (int i = 0; i < model_parameters.solution_hints_size(); ++i) { + RETURN_IF_ERROR(gurobi_->SetIntParam(GRB_INT_PAR_STARTNUMBER, i)); + RETURN_IF_ERROR(UpdateDoubleListAttribute( + model_parameters.solution_hints(i).variable_values(), + GRB_DBL_ATTR_START, variables_map_)); } - if ((gurobi_error == GRB_ERROR_CALLBACK || gurobi_error == kGrbOk) && - gurobi_cb_data != nullptr) { - RETURN_IF_ERROR(gurobi_cb_data->status) << "Error in callback"; - } - RETURN_IF_GUROBI_ERROR(gurobi_error); - if (cb != nullptr) { - std::move(callback_cleanup).Cancel(); - RETURN_IF_GUROBI_ERROR(GRBsetcallbackfunc(gurobi_model_, nullptr, nullptr)); - } - RETURN_IF_ERROR( - ExtractSolveResultProto(is_maximize, solve_result, model_parameters)); + UpdateInt32ListAttribute(model_parameters.branching_priorities(), + GRB_INT_ATTR_BRANCHPRIORITY, variables_map_)); + + // Here we register the callback when we either have a user callback or a + // local interrupter. The rationale for doing so when we have only an + // interrupter is explained in GurobiCallbackImpl(). + Gurobi::Callback grb_cb = nullptr; + std::unique_ptr gurobi_cb_data; + if (cb != nullptr || local_interrupter != nullptr || message_cb != nullptr) { + ASSIGN_OR_RETURN(gurobi_cb_data, + RegisterCallback(callback_registration, cb, message_cb, + start, local_interrupter.get())); + grb_cb = [&gurobi_cb_data]( + const Gurobi::CallbackContext& cb_context) -> absl::Status { + return GurobiCallbackImpl(cb_context, gurobi_cb_data->callback_input, + gurobi_cb_data->message_callback_data, + gurobi_cb_data->local_interrupter); + }; + } + RETURN_IF_ERROR(gurobi_->Optimize(grb_cb)); + + // We flush message callbacks before testing for Gurobi error in case where + // the unfinished line of message would help with the error. + if (gurobi_cb_data != nullptr) { + GurobiCallbackImplFlush(gurobi_cb_data->callback_input, + gurobi_cb_data->message_callback_data); + } + + ASSIGN_OR_RETURN(SolveResultProto solve_result, + ExtractSolveResultProto(start, model_parameters)); + for (const auto& warning : warnings) { + solve_result.add_warnings(warning); + } + // Reset Gurobi parameters. // TODO(user): ensure that resetting parameters does not degrade // incrementalism performance. - RETURN_IF_ERROR(ResetParameters()); - CHECK_OK(util_time::EncodeGoogleApiProto( - absl::Now() - start, - solve_result.mutable_solve_stats()->mutable_solve_time())); + RETURN_IF_ERROR(gurobi_->ResetParameters()); + return solve_result; } @@ -1512,8 +1925,6 @@ bool GurobiSolver::CanUpdate(const ModelUpdateProto& model_update) { return true; } -#undef RETURN_IF_GUROBI_ERROR - MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_GUROBI, GurobiSolver::New) } // namespace math_opt diff --git a/ortools/math_opt/solvers/gurobi_solver.h b/ortools/math_opt/solvers/gurobi_solver.h index 3c1d035d55..273db12c75 100644 --- a/ortools/math_opt/solvers/gurobi_solver.h +++ b/ortools/math_opt/solvers/gurobi_solver.h @@ -17,16 +17,20 @@ #include #include #include +#include #include #include #include +#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> New( - const ModelProto& input_model, const SolverInitializerProto& initializer); - - ~GurobiSolver() override; + const ModelProto& input_model, + const SolverInterface::InitArgs& init_args); absl::StatusOr 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 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; - using ConstraintMap = gtl::linked_hash_map; + 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> - ConvertTerminationReason(int gurobi_status, bool has_feasible_solution); - absl::Status ExtractSolveResultProto( - bool is_maximize, SolveResultProto& result, + struct SolutionsAndClaims { + std::vector solutions; + SolutionClaims solution_claims; + }; + + template + struct SolutionAndClaim { + std::optional solution; + bool feasible_solution_exists = false; + }; + + using IdHashMap = gtl::linked_hash_map; + + absl::StatusOr GetProblemStatus( + const int grb_termination, const SolutionClaims solution_claims); + absl::StatusOr ExtractSolveResultProto( + absl::Time start, const ModelSolveParametersProto& model_parameters); + absl::Status FillRays(const ModelSolveParametersProto& model_parameters, + SolveResultProto& result); + absl::StatusOr GetSolutions( const ModelSolveParametersProto& model_parameters); - absl::Status ResetParameters(); - absl::Status SetParameter(const std::string& param_name, - const std::string& param_value); + absl::StatusOr GetSolveStats(absl::Time start, + SolutionClaims solution_claims); + + absl::StatusOr GetBestDualBound(); + absl::StatusOr GetBestPrimalBound(bool has_primal_feasible_solution); + bool PrimalSolutionQualityAvailable() const; + absl::StatusOr GetPrimalSolutionQuality() const; + + // Warning: is read from gurobi, take care with gurobi update. + absl::StatusOr IsMaximize() const; + + static absl::StatusOr ConvertTerminationReason( + int gurobi_status, SolutionClaims solution_claims); + + absl::StatusOr GetQpSolution( + const ModelSolveParametersProto& model_parameters); + absl::StatusOr GetLpSolution( + const ModelSolveParametersProto& model_parameters); + absl::StatusOr GetMipSolutions( + const ModelSolveParametersProto& model_parameters); + + // return bool field should be true if a primal solution exists. + absl::StatusOr> + GetConvexPrimalSolutionIfAvailable( + const ModelSolveParametersProto& model_parameters); + absl::StatusOr> + GetLpDualSolutionIfAvailable( + const ModelSolveParametersProto& model_parameters); + absl::StatusOr> 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& 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& deleted_variables_index); - absl::StatusOr GetIntAttr(const char* name) const; - absl::StatusOr GetDoubleAttr(const char* name) const; - absl::Status GetIntAttrArray(const char* name, - absl::Span attr_out) const; - absl::Status GetDoubleAttrArray(const char* name, - absl::Span 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 IsLP() const; + absl::StatusOr IsQP() const; absl::StatusOr> 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_; - // 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, double> + quadratic_objective_coefficients_; static constexpr int kGrbBasicConstraint = 0; static constexpr int kGrbNonBasicConstraint = -1; diff --git a/ortools/math_opt/solvers/message_callback_data.cc b/ortools/math_opt/solvers/message_callback_data.cc index 5d4f4ce9d0..265e9f1778 100644 --- a/ortools/math_opt/solvers/message_callback_data.cc +++ b/ortools/math_opt/solvers/message_callback_data.cc @@ -14,32 +14,31 @@ #include "ortools/math_opt/solvers/message_callback_data.h" #include +#include #include +#include #include - -#include "absl/strings/string_view.h" -#include "absl/types/optional.h" -#include "ortools/math_opt/callback.pb.h" +#include namespace operations_research { namespace math_opt { -absl::optional MessageCallbackData::Parse( - const absl::string_view message) { - CallbackDataProto data; +std::vector MessageCallbackData::Parse( + const std::string_view message) { + std::vector 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 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 MessageCallbackData::Flush() { +std::vector 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 strings = {std::move(unfinished_line_)}; unfinished_line_.clear(); - return data; + return strings; } } // namespace math_opt diff --git a/ortools/math_opt/solvers/message_callback_data.h b/ortools/math_opt/solvers/message_callback_data.h index 8f46ac6674..6e39f1590b 100644 --- a/ortools/math_opt/solvers/message_callback_data.h +++ b/ortools/math_opt/solvers/message_callback_data.h @@ -14,22 +14,20 @@ #ifndef OR_TOOLS_MATH_OPT_SOLVERS_MESSAGE_CALLBACK_DATA_H_ #define OR_TOOLS_MATH_OPT_SOLVERS_MESSAGE_CALLBACK_DATA_H_ +#include #include - -#include "absl/strings/string_view.h" -#include "absl/types/optional.h" -#include "ortools/math_opt/callback.pb.h" +#include +#include namespace operations_research { namespace math_opt { -// Buffer for solvers messages that enforces the contract of -// CallbackDataProto.messages for CALLBACK_EVENT_MESSAGE events. +// Buffer for solvers messages that enforces the contract of MessageCallback. // // This contract mandates that each message is a full finished line. As a // consequence, if the solver calls the callback with a partial last line, this -// one must not be passed immediately in CallbackDataProto.messages but kept -// until the end of the line is reached (or the solve is done). +// one must not be passed immediately to MessageCallback but kept until the end +// of the line is reached (or the solve is done). // // To implement that this class has two methods: // @@ -45,23 +43,17 @@ class MessageCallbackData { MessageCallbackData(const MessageCallbackData&) = delete; MessageCallbackData& operator=(const MessageCallbackData&) = delete; - // Parses the input message, returning a message callback data proto with all - // finished lines. Returns nullopt if the input message did not contained any + // Parses the input message, returning a vector with all finished + // lines. Returns an empty vector if the input message did not contained any // '\n'. // // It updates this object with the last unfinished line to use it to complete // the next message. - // - // Note that CallbackDataProto.runtime field is not set. It must be set by the - // caller appropriately. - absl::optional Parse(absl::string_view message); + std::vector Parse(std::string_view message); - // Returns a message callback data proto with the last unfinished line if it - // exists. - // - // Note that CallbackDataProto.runtime field is not set. It must be set by the - // caller appropriately. - absl::optional Flush(); + // Returns a vector with the last unfinished line if it exists, else an empty + // vector. + std::vector Flush(); private: // The last message line not ending with '\n'. diff --git a/ortools/math_opt/solvers/pdlp_bridge.cc b/ortools/math_opt/solvers/pdlp_bridge.cc new file mode 100644 index 0000000000..a9399872da --- /dev/null +++ b/ortools/math_opt/solvers/pdlp_bridge.cc @@ -0,0 +1,169 @@ +// 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/pdlp_bridge.h" + +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "Eigen/Core" +#include "Eigen/SparseCore" +#include "ortools/pdlp/quadratic_program.h" +#include "ortools/math_opt/core/math_opt_proto_utils.h" +#include "ortools/math_opt/core/sparse_vector_view.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" + +namespace operations_research { +namespace math_opt { +namespace { + +absl::StatusOr ExtractSolution( + const Eigen::VectorXd& values, const std::vector& pdlp_index_to_id, + const SparseVectorFilterProto& filter, const double scale) { + if (values.size() != pdlp_index_to_id.size()) { + return absl::InternalError( + absl::StrCat("Expected solution vector with ", pdlp_index_to_id.size(), + " elements, found: ", values.size())); + } + SparseVectorFilterPredicate predicate(filter); + SparseDoubleVectorProto result; + for (int i = 0; i < pdlp_index_to_id.size(); ++i) { + const double value = scale * values[i]; + const int64_t id = pdlp_index_to_id[i]; + if (predicate.AcceptsAndUpdate(id, value)) { + result.add_ids(id); + result.add_values(value); + } + } + return result; +} + +} // namespace + +absl::StatusOr PdlpBridge::FromProto( + const ModelProto& model_proto) { + PdlpBridge result; + pdlp::QuadraticProgram& pdlp_lp = result.pdlp_lp_; + const VariablesProto& variables = model_proto.variables(); + const LinearConstraintsProto& linear_constraints = + model_proto.linear_constraints(); + pdlp_lp.ResizeAndInitialize(variables.ids_size(), + linear_constraints.ids_size()); + if (!model_proto.name().empty()) { + pdlp_lp.problem_name = model_proto.name(); + } + if (variables.names_size() > 0) { + pdlp_lp.variable_names = {variables.names().begin(), + variables.names().end()}; + } + if (linear_constraints.names_size() > 0) { + pdlp_lp.constraint_names = {linear_constraints.names().begin(), + linear_constraints.names().end()}; + } + for (int i = 0; i < variables.ids_size(); ++i) { + result.var_id_to_pdlp_index_[variables.ids(i)] = i; + result.pdlp_index_to_var_id_.push_back(variables.ids(i)); + pdlp_lp.variable_lower_bounds[i] = variables.lower_bounds(i); + pdlp_lp.variable_upper_bounds[i] = variables.upper_bounds(i); + if (variables.integers(i)) { + return absl::InvalidArgumentError( + "PDLP cannot solve problems with integer variables"); + } + } + for (int i = 0; i < linear_constraints.ids_size(); ++i) { + result.lin_con_id_to_pdlp_index_[linear_constraints.ids(i)] = i; + result.pdlp_index_to_lin_con_id_.push_back(linear_constraints.ids(i)); + pdlp_lp.constraint_lower_bounds[i] = linear_constraints.lower_bounds(i); + pdlp_lp.constraint_upper_bounds[i] = linear_constraints.upper_bounds(i); + } + const bool is_maximize = model_proto.objective().maximize(); + const double obj_scale = is_maximize ? -1.0 : 1.0; + pdlp_lp.objective_offset = obj_scale * model_proto.objective().offset(); + for (const auto [var_id, coef] : + MakeView(model_proto.objective().linear_coefficients())) { + pdlp_lp.objective_vector[result.var_id_to_pdlp_index_.at(var_id)] = + obj_scale * coef; + } + const SparseDoubleMatrixProto& quadratic_objective = + model_proto.objective().quadratic_coefficients(); + std::vector> obj_triplets; + const int obj_nnz = quadratic_objective.row_ids().size(); + for (int i = 0; i < obj_nnz; ++i) { + const int64_t row_index = + result.var_id_to_pdlp_index_.at(quadratic_objective.row_ids(i)); + const int64_t column_index = + result.var_id_to_pdlp_index_.at(quadratic_objective.column_ids(i)); + const double value = obj_scale * quadratic_objective.coefficients(i); + // MathOpt represents quadratic objectives in "terms" form, i.e. as a sum + // of double * Variable * Variable terms. They are stored in upper + // triangular form with row_index <= column_index. In contrast, PDLP + // represents quadratic objectives in "matrix" form as 1/2 x'Qx; it wants + // the symmetric matrix Q. To get to the right format, we simply add each + // term and its transposed entry, and defer to Eigen to sum duplicate + // entries along the diagonal. + obj_triplets.push_back({row_index, column_index, value}); + obj_triplets.push_back({column_index, row_index, value}); + } + pdlp_lp.objective_matrix.setFromTriplets(obj_triplets.begin(), + obj_triplets.end()); + pdlp_lp.objective_scaling_factor = obj_scale; + // Note: MathOpt stores the constraint data in row major order, but PDLP + // wants the data in column major order. There is probably a more efficient + // method to do this transformation. + std::vector> mat_triplets; + const int nnz = model_proto.linear_constraint_matrix().row_ids_size(); + mat_triplets.reserve(nnz); + const SparseDoubleMatrixProto& proto_mat = + model_proto.linear_constraint_matrix(); + for (int i = 0; i < nnz; ++i) { + const int64_t row_index = + result.lin_con_id_to_pdlp_index_.at(proto_mat.row_ids(i)); + const int64_t column_index = + result.var_id_to_pdlp_index_.at(proto_mat.column_ids(i)); + const double value = proto_mat.coefficients(i); + mat_triplets.emplace_back(row_index, column_index, value); + } + pdlp_lp.constraint_matrix.setFromTriplets(mat_triplets.begin(), + mat_triplets.end()); + return result; +} + +absl::StatusOr PdlpBridge::PrimalVariablesToProto( + const Eigen::VectorXd& primal_values, + const SparseVectorFilterProto& variable_filter) const { + return ExtractSolution(primal_values, pdlp_index_to_var_id_, variable_filter, + /*scale=*/1.0); +} +absl::StatusOr PdlpBridge::DualVariablesToProto( + const Eigen::VectorXd& dual_values, + const SparseVectorFilterProto& linear_constraint_filter) const { + return ExtractSolution(dual_values, pdlp_index_to_lin_con_id_, + linear_constraint_filter, + /*scale=*/pdlp_lp_.objective_scaling_factor); +} +absl::StatusOr PdlpBridge::ReducedCostsToProto( + const Eigen::VectorXd& reduced_costs, + const SparseVectorFilterProto& variable_filter) const { + return ExtractSolution(reduced_costs, pdlp_index_to_var_id_, variable_filter, + /*scale=*/pdlp_lp_.objective_scaling_factor); +} + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/solvers/pdlp_bridge.h b/ortools/math_opt/solvers/pdlp_bridge.h new file mode 100644 index 0000000000..3f7f8a81bd --- /dev/null +++ b/ortools/math_opt/solvers/pdlp_bridge.h @@ -0,0 +1,76 @@ +// 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_PDLP_BRIDGE_H_ +#define OR_TOOLS_MATH_OPT_SOLVERS_PDLP_BRIDGE_H_ + +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/status/statusor.h" +#include "Eigen/Core" +#include "ortools/pdlp/quadratic_program.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/sparse_containers.pb.h" + +namespace operations_research { +namespace math_opt { + +// Builds a PDLP model (QuadraticProgram) from ModelProto, and provides methods +// to translate solutions back and forth. +// +// The primary difference in the models are: +// 1. PDLP maps the variable/constraint ids to consecutive indices +// [0, 1, ..., n). +// 2. PDLP does not support maximization. If the ModelProto is a maximization +// problem, the objective is negated (coefficients and offset) before +// passing to PDLP. On the way back, the objective value, and all dual +// variables/reduced costs (also for rays) must be negated. +// +// Throughout, it is assumed that the MathOpt protos have been validated, but +// no assumption is made on the PDLP output. Any Status errors resulting from +// invalid PDLP output use the status code kInternal. +class PdlpBridge { + public: + PdlpBridge() = default; + static absl::StatusOr FromProto(const ModelProto& model_proto); + + const pdlp::QuadraticProgram& pdlp_lp() const { return pdlp_lp_; } + + // TODO(b/183616124): we need to support the inverse of these methods for + // warm start. + absl::StatusOr PrimalVariablesToProto( + const Eigen::VectorXd& primal_values, + const SparseVectorFilterProto& variable_filter) const; + absl::StatusOr DualVariablesToProto( + const Eigen::VectorXd& dual_values, + const SparseVectorFilterProto& linear_constraint_filter) const; + absl::StatusOr ReducedCostsToProto( + const Eigen::VectorXd& reduced_costs, + const SparseVectorFilterProto& variable_filter) const; + + private: + pdlp::QuadraticProgram pdlp_lp_; + absl::flat_hash_map var_id_to_pdlp_index_; + // NOTE: this vector is strictly increasing + std::vector pdlp_index_to_var_id_; + absl::flat_hash_map lin_con_id_to_pdlp_index_; + // NOTE: this vector is strictly increasing + std::vector pdlp_index_to_lin_con_id_; +}; + +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_SOLVERS_PDLP_BRIDGE_H_ diff --git a/ortools/math_opt/solvers/pdlp_solver.cc b/ortools/math_opt/solvers/pdlp_solver.cc new file mode 100644 index 0000000000..67c4b14f08 --- /dev/null +++ b/ortools/math_opt/solvers/pdlp_solver.cc @@ -0,0 +1,357 @@ +// 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/pdlp_solver.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ortools/base/logging.h" +#include "google/protobuf/duration.pb.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/time/time.h" +#include "ortools/pdlp/iteration_stats.h" +#include "ortools/pdlp/primal_dual_hybrid_gradient.h" +#include "ortools/pdlp/quadratic_program.h" +#include "ortools/pdlp/solve_log.pb.h" +#include "ortools/pdlp/solvers.pb.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/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/solvers/pdlp_bridge.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/base/status_macros.h" +#include "ortools/base/protoutil.h" + +namespace operations_research { +namespace math_opt { + +using pdlp::PrimalDualHybridGradientParams; +using pdlp::SolverResult; + +absl::StatusOr> PdlpSolver::New( + const ModelProto& model, const InitArgs& init_args) { + auto result = absl::WrapUnique(new PdlpSolver); + ASSIGN_OR_RETURN(result->pdlp_bridge_, PdlpBridge::FromProto(model)); + return result; +} + +std::pair> +PdlpSolver::MergeParameters(const SolveParametersProto& parameters) { + PrimalDualHybridGradientParams result; + std::vector warnings; + if (parameters.enable_output()) { + // TODO(b/183502493): this is not a robust solution. It is not thread safe + // and will interfere with the global vlog state. + SetVLOGLevel("primal_dual_hybrid_gradient", 2); + } + if (parameters.has_threads()) { + result.set_num_threads(parameters.threads()); + } + if (parameters.has_time_limit()) { + result.mutable_termination_criteria()->set_time_sec_limit( + absl::ToDoubleSeconds( + util_time::DecodeGoogleApiProto(parameters.time_limit()).value())); + } + if (parameters.has_random_seed()) { + warnings.push_back("parameter random_seed not supported for PDLP"); + } + if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) { + warnings.push_back("parameter lp_algorithm not supported for PDLP"); + } + if (parameters.presolve() != EMPHASIS_UNSPECIFIED) { + warnings.push_back("parameter presolve not supported for PDLP"); + } + if (parameters.cuts() != EMPHASIS_UNSPECIFIED) { + warnings.push_back("parameter cuts not supported for PDLP"); + } + if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) { + warnings.push_back("parameter heuristics not supported for PDLP"); + } + if (parameters.scaling() != EMPHASIS_UNSPECIFIED) { + warnings.push_back("parameter scaling not supported for PDLP"); + } + if (parameters.has_iteration_limit()) { + const int64_t limit = std::min(std::numeric_limits::max(), + parameters.iteration_limit()); + result.mutable_termination_criteria()->set_iteration_limit( + static_cast(limit)); + } + result.MergeFrom(parameters.pdlp()); + return {std::move(result), std::move(warnings)}; +} + +namespace { + +absl::StatusOr ConvertReason( + const pdlp::TerminationReason pdlp_reason, const std::string& pdlp_detail) { + switch (pdlp_reason) { + case pdlp::TERMINATION_REASON_UNSPECIFIED: + return TerminateForReason(TERMINATION_REASON_UNSPECIFIED, pdlp_detail); + case pdlp::TERMINATION_REASON_OPTIMAL: + return TerminateForReason(TERMINATION_REASON_OPTIMAL, pdlp_detail); + case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE: + return TerminateForReason(TERMINATION_REASON_INFEASIBLE, pdlp_detail); + case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE: + return TerminateForReason(TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED, + pdlp_detail); + case pdlp::TERMINATION_REASON_TIME_LIMIT: + return TerminateForLimit(LIMIT_TIME, pdlp_detail); + case pdlp::TERMINATION_REASON_ITERATION_LIMIT: + return TerminateForLimit(LIMIT_ITERATION, pdlp_detail); + case pdlp::TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT: + return TerminateForLimit(LIMIT_OTHER, pdlp_detail); + case pdlp::TERMINATION_REASON_NUMERICAL_ERROR: + return TerminateForReason(TERMINATION_REASON_NUMERICAL_ERROR, + pdlp_detail); + case pdlp::TERMINATION_REASON_INVALID_PROBLEM: + // Indicates that the solver detected invalid problem data, e.g., + // inconsistent bounds. + return absl::InternalError( + absl::StrCat("Invalid problem sent to PDLP solver " + "(TERMINATION_REASON_INVALID_PROBLEM): ", + pdlp_detail)); + // Indicates that an invalid value for the parameters was detected. + case pdlp::TERMINATION_REASON_INVALID_PARAMETER: + return absl::InvalidArgumentError(absl::StrCat( + "PDLP parameters invalid (TERMINATION_REASON_INVALID_PARAMETER): ", + pdlp_detail)); + case pdlp::TERMINATION_REASON_OTHER: + return TerminateForReason(TERMINATION_REASON_OTHER_ERROR, pdlp_detail); + default: + LOG(FATAL) << "PDLP status: " << ProtoEnumToString(pdlp_reason) + << " not implemented."; + } +} + +ProblemStatusProto GetProblemStatus(const pdlp::TerminationReason pdlp_reason, + const bool has_finite_dual_bound) { + ProblemStatusProto problem_status; + + switch (pdlp_reason) { + case pdlp::TERMINATION_REASON_OPTIMAL: + problem_status.set_primal_status(FEASIBILITY_STATUS_FEASIBLE); + problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE); + break; + case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE: + problem_status.set_primal_status(FEASIBILITY_STATUS_INFEASIBLE); + problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED); + break; + case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE: + problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); + problem_status.set_dual_status(FEASIBILITY_STATUS_INFEASIBLE); + break; + case pdlp::TERMINATION_REASON_PRIMAL_OR_DUAL_INFEASIBLE: + problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); + problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED); + problem_status.set_primal_or_dual_infeasible(true); + break; + default: + problem_status.set_primal_status(FEASIBILITY_STATUS_UNDETERMINED); + problem_status.set_dual_status(FEASIBILITY_STATUS_UNDETERMINED); + break; + } + if (has_finite_dual_bound) { + problem_status.set_dual_status(FEASIBILITY_STATUS_FEASIBLE); + } + return problem_status; +} + +} // namespace + +absl::Status PdlpSolver::FillSolveResult( + const pdlp::SolverResult& pdlp_result, + const ModelSolveParametersProto& model_params, SolveResultProto& result) { + ASSIGN_OR_RETURN(*result.mutable_termination(), + ConvertReason(pdlp_result.solve_log.termination_reason(), + pdlp_result.solve_log.termination_string())); + ASSIGN_OR_RETURN(*result.mutable_solve_stats()->mutable_solve_time(), + util_time::EncodeGoogleApiProto( + absl::Seconds(pdlp_result.solve_log.solve_time_sec()))); + const std::optional convergence_information = + pdlp::GetConvergenceInformation(pdlp_result.solve_log.solution_stats(), + pdlp_result.solve_log.solution_type()); + + // TODO(b/195295177): update description after changes to bounds below. + // Set default infinite primal/dual bounds. PDLP's default is a minimization + // problem for which the default primal and dual bounds are infinity and + // -infinity respectively. PDLP provides a scaling factor to flip the signs + // for maximization problems. Note that PDLP does not consider solutions that + // are feasible up to the solver's tolerances to update these bounds. PDLP + // provides a correction function for dual solutions that yields a true dual + // bound, but does not provide this function for primal solutions. + const double objective_scaling_factor = + pdlp_bridge_.pdlp_lp().objective_scaling_factor; + result.mutable_solve_stats()->set_best_primal_bound( + objective_scaling_factor * std::numeric_limits::infinity()); + result.mutable_solve_stats()->set_best_dual_bound( + -objective_scaling_factor * std::numeric_limits::infinity()); + + switch (pdlp_result.solve_log.termination_reason()) { + case pdlp::TERMINATION_REASON_OPTIMAL: + case pdlp::TERMINATION_REASON_TIME_LIMIT: + case pdlp::TERMINATION_REASON_ITERATION_LIMIT: + case pdlp::TERMINATION_REASON_KKT_MATRIX_PASS_LIMIT: + case pdlp::TERMINATION_REASON_NUMERICAL_ERROR: { + SolutionProto* solution_proto = result.add_solutions(); + { + auto maybe_primal = pdlp_bridge_.PrimalVariablesToProto( + pdlp_result.primal_solution, model_params.variable_values_filter()); + RETURN_IF_ERROR(maybe_primal.status()); + PrimalSolutionProto* primal_proto = + solution_proto->mutable_primal_solution(); + primal_proto->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED); + *primal_proto->mutable_variable_values() = *std::move(maybe_primal); + // Note: the solution could be primal feasible for termination reasons + // other than TERMINATION_REASON_OPTIMAL, but in theory, PDLP could + // also be modified to return the best feasible solution encounered in + // an early termination run (if any). + if (pdlp_result.solve_log.termination_reason() == + pdlp::TERMINATION_REASON_OPTIMAL) { + primal_proto->set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + } + if (convergence_information.has_value()) { + primal_proto->set_objective_value( + convergence_information->primal_objective()); + // TODO(b/195295177): update to return bounds. + // PDLP does not have a primal objective correction function so we + // skip the primal bound update. + } + } + { + auto maybe_dual = pdlp_bridge_.DualVariablesToProto( + pdlp_result.dual_solution, model_params.dual_values_filter()); + RETURN_IF_ERROR(maybe_dual.status()); + auto maybe_reduced = pdlp_bridge_.ReducedCostsToProto( + pdlp_result.reduced_costs, model_params.reduced_costs_filter()); + RETURN_IF_ERROR(maybe_reduced.status()); + DualSolutionProto* dual_proto = solution_proto->mutable_dual_solution(); + dual_proto->set_feasibility_status(SOLUTION_STATUS_UNDETERMINED); + *dual_proto->mutable_dual_values() = *std::move(maybe_dual); + *dual_proto->mutable_reduced_costs() = *std::move(maybe_reduced); + // Note: same comment on primal solution status holds here. + if (pdlp_result.solve_log.termination_reason() == + pdlp::TERMINATION_REASON_OPTIMAL) { + dual_proto->set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + } + if (convergence_information.has_value()) { + const double dual_obj = convergence_information->dual_objective(); + dual_proto->set_objective_value(dual_obj); + // TODO(b/195295177): update to use dual_obj or corrected_dual_bound. + // Using PDLP's corrected dual objective to update dual bounds. + const double corrected_dual_bound = + convergence_information->corrected_dual_objective(); + result.mutable_solve_stats()->set_best_dual_bound( + corrected_dual_bound); + } + } + break; + } + case pdlp::TERMINATION_REASON_PRIMAL_INFEASIBLE: { + // NOTE: for primal infeasible problems, PDLP stores the infeasibility + // certificate (dual ray) in the dual variables and reduced costs. + auto maybe_dual = pdlp_bridge_.DualVariablesToProto( + pdlp_result.dual_solution, model_params.dual_values_filter()); + RETURN_IF_ERROR(maybe_dual.status()); + auto maybe_reduced = pdlp_bridge_.ReducedCostsToProto( + pdlp_result.reduced_costs, model_params.reduced_costs_filter()); + RETURN_IF_ERROR(maybe_reduced.status()); + DualRayProto* dual_ray_proto = result.add_dual_rays(); + *dual_ray_proto->mutable_dual_values() = *std::move(maybe_dual); + *dual_ray_proto->mutable_reduced_costs() = *std::move(maybe_reduced); + break; + } + case pdlp::TERMINATION_REASON_DUAL_INFEASIBLE: { + // NOTE: for dual infeasible problems, PDLP stores the infeasibility + // certificate (primal ray) in the primal variables. + auto maybe_primal = pdlp_bridge_.PrimalVariablesToProto( + pdlp_result.primal_solution, model_params.variable_values_filter()); + RETURN_IF_ERROR(maybe_primal.status()); + PrimalRayProto* primal_ray_proto = result.add_primal_rays(); + *primal_ray_proto->mutable_variable_values() = *std::move(maybe_primal); + break; + } + default: + break; + } + *result.mutable_solve_stats()->mutable_problem_status() = + GetProblemStatus(pdlp_result.solve_log.termination_reason(), + std::isfinite(result.solve_stats().best_dual_bound())); + return absl::OkStatus(); +} + +absl::StatusOr PdlpSolver::Solve( + const SolveParametersProto& parameters, + const ModelSolveParametersProto& model_parameters, + const MessageCallback message_cb, + const CallbackRegistrationProto& callback_registration, const Callback cb, + SolveInterrupter* const interrupter) { + // TODO(b/192274409): Use interrupter if PDLP supports interruption. + + // TODO(b/183502493): Implement message callback when PDLP supports that. + if (message_cb != nullptr) { + return absl::InvalidArgumentError(internal::kMessageCallbackNotSupported); + } + + RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration, + /*supported_events=*/{})); + + SolveResultProto result; + auto [pdlp_params, param_warnings] = MergeParameters(parameters); + 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)); + } + } + } + const SolverResult pdlp_result = + PrimalDualHybridGradient(pdlp_bridge_.pdlp_lp(), pdlp_params); + RETURN_IF_ERROR(FillSolveResult(pdlp_result, model_parameters, result)); + return result; +} +absl::Status PdlpSolver::Update(const ModelUpdateProto& model_update) { + // This function should never be called since CanUpdate() always returns + // false. + return absl::InternalError("PDLP solver does not support incrementalism"); +} +bool PdlpSolver::CanUpdate(const ModelUpdateProto& model_update) { + return false; +} + +MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_PDLP, PdlpSolver::New); + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/solvers/pdlp_solver.h b/ortools/math_opt/solvers/pdlp_solver.h new file mode 100644 index 0000000000..efe6b54c64 --- /dev/null +++ b/ortools/math_opt/solvers/pdlp_solver.h @@ -0,0 +1,71 @@ +// 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_PDLP_SOLVER_H_ +#define OR_TOOLS_MATH_OPT_SOLVERS_PDLP_SOLVER_H_ + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "ortools/pdlp/primal_dual_hybrid_gradient.h" +#include "ortools/pdlp/quadratic_program.h" +#include "ortools/pdlp/solvers.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" +#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/solvers/pdlp_bridge.h" + +namespace operations_research { +namespace math_opt { + +class PdlpSolver : public SolverInterface { + public: + static absl::StatusOr> New( + const ModelProto& model, const InitArgs& init_args); + + absl::StatusOr Solve( + const SolveParametersProto& parameters, + const ModelSolveParametersProto& model_parameters, + 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. + static std::pair> + MergeParameters(const SolveParametersProto& parameters); + + private: + PdlpSolver() = default; + + absl::Status FillSolveResult(const pdlp::SolverResult& pdlp_result, + const ModelSolveParametersProto& model_params, + SolveResultProto& result); + + PdlpBridge pdlp_bridge_; +}; + +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_SOLVERS_PDLP_SOLVER_H_ diff --git a/ortools/math_opt/sparse_containers.proto b/ortools/math_opt/sparse_containers.proto index f49ca9ff3f..4ed1e4f1d4 100644 --- a/ortools/math_opt/sparse_containers.proto +++ b/ortools/math_opt/sparse_containers.proto @@ -16,6 +16,9 @@ syntax = "proto3"; package operations_research.math_opt; +option java_package = "com.google.ortools.mathopt"; +option java_multiple_files = true; + // A sparse representation of a vector of doubles. message SparseDoubleVectorProto { // Must be sorted (in increasing ordering) with all elements distinct. @@ -32,6 +35,14 @@ message SparseBoolVectorProto { repeated bool values = 2; } +// A sparse representation of a vector of ints. +message SparseInt32VectorProto { + // Should be sorted (in increasing ordering) with all elements distinct. + repeated int64 ids = 1; + // Must have equal length to ids. + repeated int32 values = 2; +} + // This message allows to query/set specific parts of a SparseXxxxVector. // The default behavior is not to filter out anything. // A common usage is to query only parts of solutions (only non-zero values, diff --git a/ortools/math_opt/validators/BUILD.bazel b/ortools/math_opt/validators/BUILD.bazel index f7ecb4fbc6..532bb482d6 100644 --- a/ortools/math_opt/validators/BUILD.bazel +++ b/ortools/math_opt/validators/BUILD.bazel @@ -25,6 +25,21 @@ cc_library( ], ) +cc_library( + name = "sparse_matrix_validator", + srcs = ["sparse_matrix_validator.cc"], + hdrs = ["sparse_matrix_validator.h"], + deps = [ + ":ids_validator", + "//ortools/base:status_macros", + "//ortools/math_opt:model_cc_proto", + "//ortools/math_opt:sparse_containers_cc_proto", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "sparse_vector_validator", hdrs = ["sparse_vector_validator.h"], @@ -64,13 +79,14 @@ cc_library( ":ids_validator", ":name_validator", ":scalar_validator", + ":sparse_matrix_validator", ":sparse_vector_validator", "//ortools/base", "//ortools/base:status_macros", "//ortools/math_opt:model_cc_proto", - "//ortools/math_opt/core:model_summary", "//ortools/math_opt:model_update_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/core:model_summary", "//ortools/math_opt/core:sparse_vector_view", "@com_google_absl//absl/status", "@com_google_absl//absl/strings", @@ -78,6 +94,41 @@ cc_library( ], ) +cc_library( + name = "solve_stats_validator", + srcs = ["solve_stats_validator.cc"], + hdrs = ["solve_stats_validator.h"], + deps = [ + ":scalar_validator", + "//ortools/base", + "//ortools/base:status_macros", + "//ortools/base:protoutil", + "//ortools/math_opt:result_cc_proto", + "//ortools/port:proto_utils", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + ], +) + +cc_library( + name = "result_validator", + srcs = ["result_validator.cc"], + hdrs = ["result_validator.h"], + deps = [ + ":solution_validator", + ":solve_stats_validator", + "//ortools/math_opt:model_parameters_cc_proto", + "//ortools/math_opt:result_cc_proto", + "//ortools/math_opt:solution_cc_proto", + "//ortools/math_opt/core:model_summary", + "//ortools/port:proto_utils", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + ], +) + cc_library( name = "solution_validator", srcs = ["solution_validator.cc"], @@ -88,13 +139,13 @@ cc_library( ":sparse_vector_validator", "//ortools/base", "//ortools/base:status_macros", - "//ortools/math_opt/core:math_opt_proto_utils", "//ortools/math_opt:model_cc_proto", "//ortools/math_opt:model_parameters_cc_proto", - "//ortools/math_opt/core:model_summary", "//ortools/math_opt:result_cc_proto", "//ortools/math_opt:solution_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/core:math_opt_proto_utils", + "//ortools/math_opt/core:model_summary", "//ortools/math_opt/core:sparse_vector_view", "@com_google_absl//absl/status", "@com_google_absl//absl/strings", diff --git a/ortools/math_opt/validators/callback_validator.cc b/ortools/math_opt/validators/callback_validator.cc index d8202ccc20..0344ef0661 100644 --- a/ortools/math_opt/validators/callback_validator.cc +++ b/ortools/math_opt/validators/callback_validator.cc @@ -13,23 +13,24 @@ #include "ortools/math_opt/validators/callback_validator.h" -#include -#include +#include #include #include +#include -#include "ortools/base/integral_types.h" #include "ortools/base/logging.h" #include "google/protobuf/duration.pb.h" +#include "absl/container/flat_hash_set.h" #include "absl/status/status.h" -#include "absl/strings/match.h" #include "absl/strings/str_cat.h" -#include "absl/strings/string_view.h" +#include "absl/strings/str_join.h" #include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/math_opt_proto_utils.h" #include "ortools/math_opt/core/model_summary.h" #include "ortools/math_opt/core/sparse_vector_view.h" #include "ortools/math_opt/solution.pb.h" #include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/validators/ids_validator.h" #include "ortools/math_opt/validators/model_parameters_validator.h" #include "ortools/math_opt/validators/scalar_validator.h" #include "ortools/math_opt/validators/solution_validator.h" @@ -55,7 +56,7 @@ absl::Status IsEventRegistered( } } return absl::InvalidArgumentError(absl::StrCat( - "Event ", ProtoEnumToString(event), + "event ", ProtoEnumToString(event), " not part of the registered_events in callback_registration")); } @@ -67,7 +68,7 @@ absl::Status ValidateGeneratedLinearConstraint( RETURN_IF_ERROR(CheckIdsAndValues( coefficients, {.allow_positive_infinity = false, .allow_negative_infinity = false})) - << "Invalid linear_constraint coefficients"; + << "invalid linear_constraint coefficients"; RETURN_IF_ERROR(CheckIdsSubset(coefficients.ids(), model_summary.variables, "cut variables", "model IDs")); RETURN_IF_ERROR(CheckScalar(linear_constraint.lower_bound(), @@ -79,43 +80,73 @@ absl::Status ValidateGeneratedLinearConstraint( if (linear_constraint.lower_bound() == -kInf && linear_constraint.upper_bound() == kInf) { return absl::InvalidArgumentError( - "Invalid GeneratedLinearConstraint, bounds [-inf,inf]"); + "invalid GeneratedLinearConstraint, bounds [-inf,inf]"); } if (linear_constraint.is_lazy() && !add_lazy_constraints) { return absl::InvalidArgumentError( - "Invalid GeneratedLinearConstraint with lazy attribute set to true, " + "invalid GeneratedLinearConstraint with lazy attribute set to true, " "adding lazy constraints requires " - "CallbackRegistrationProto.add_lazy_constraints=true."); + "CallbackRegistrationProto.add_lazy_constraints=true"); } if (!linear_constraint.is_lazy() && !add_cuts) { return absl::InvalidArgumentError( - "Invalid GeneratedLinearConstraint with lazy attribute set to false, " - "adding cuts requires CallbackRegistrationProto.add_cuts=true."); + "invalid GeneratedLinearConstraint with lazy attribute set to false, " + "adding cuts requires CallbackRegistrationProto.add_cuts=true"); } return absl::OkStatus(); } +template +struct ProtoEnumFormatter { + void operator()(std::string* const out, const T value) { + out->append(ProtoEnumToString(value)); + } +}; + } // namespace + absl::Status ValidateCallbackRegistration( const CallbackRegistrationProto& callback_registration, const ModelSummary& model_summary) { RETURN_IF_ERROR(ValidateSparseVectorFilter( callback_registration.mip_solution_filter(), model_summary.variables)) - << "Invalid CallbackRegistrationProto.mip_solution_filter"; + << "invalid CallbackRegistrationProto.mip_solution_filter"; RETURN_IF_ERROR(ValidateSparseVectorFilter( callback_registration.mip_node_filter(), model_summary.variables)) - << "Invalid CallbackRegistrationProto.mip_node_filter"; + << "invalid CallbackRegistrationProto.mip_node_filter"; // Unfortunatelly the range iterator return ints and not CallbackEventProtos. const int num_events = callback_registration.request_registration_size(); + bool can_add_lazy_constraints = false; + bool can_add_cuts = false; for (int k = 0; k < num_events; ++k) { const CallbackEventProto requested_event = callback_registration.request_registration(k); if (requested_event == CALLBACK_EVENT_UNSPECIFIED || !CallbackEventProto_IsValid(requested_event)) { return absl::InvalidArgumentError(absl::StrCat( - "Invalid event ", requested_event, " can not be registered")); + "invalid event ", requested_event, " can not be registered")); + } + if (requested_event == CALLBACK_EVENT_MIP_NODE) { + can_add_lazy_constraints = true; + can_add_cuts = true; + } + if (requested_event == CALLBACK_EVENT_MIP_SOLUTION) { + can_add_lazy_constraints = true; } } + if (callback_registration.add_cuts() && !can_add_cuts) { + return absl::InvalidArgumentError( + "can only add cuts at event CALLBACK_EVENT_MIP_NODE but this event was " + "not requested"); + } + if (callback_registration.add_lazy_constraints() && + !can_add_lazy_constraints) { + return absl::InvalidArgumentError( + "can only add lazy constraints at events CALLBACK_EVENT_MIP_NODE and " + "CALLBACK_EVENT_MIP_SOLUTION but neither of these events were " + "requested"); + } + return absl::OkStatus(); } @@ -125,20 +156,14 @@ absl::Status ValidateCallbackDataProto( const ModelSummary& model_summary) { const CallbackEventProto event = cb_data.event(); RETURN_IF_ERROR(IsEventRegistered(event, callback_registration)) - << "Invalid CallbackDataProto.event for given CallbackRegistrationProto"; + << "invalid CallbackDataProto.event for given CallbackRegistrationProto"; - if (!cb_data.messages().empty() && event != CALLBACK_EVENT_MESSAGE) { - return absl::InvalidArgumentError( - absl::StrCat("Can't provide message(s) for event ", event, " (", - ProtoEnumToString(event), ")")); - } - - const bool has_primal_solution = cb_data.has_primal_solution(); + const bool has_primal_solution = cb_data.has_primal_solution_vector(); if (has_primal_solution && event != CALLBACK_EVENT_MIP_SOLUTION && event != CALLBACK_EVENT_MIP_NODE) { return absl::InvalidArgumentError( - absl::StrCat("Can't provide primal_solution for event ", event, " (", - ProtoEnumToString(event), ")")); + absl::StrCat("can't provide primal_solution_vector for event ", event, + " (", ProtoEnumToString(event), ")")); } #ifdef RETURN_IF_SCALAR @@ -203,32 +228,14 @@ absl::Status ValidateCallbackDataProto( event == CALLBACK_EVENT_MIP_NODE ? callback_registration.mip_node_filter() : callback_registration.mip_solution_filter(); - RETURN_IF_ERROR(ValidatePrimalSolution(cb_data.primal_solution(), - filter, model_summary)) - << "Invalid CallbackDataProto.primal_solution"; + RETURN_IF_ERROR(ValidatePrimalSolutionVector( + cb_data.primal_solution_vector(), filter, model_summary)) + << "invalid CallbackDataProto.primal_solution_vector"; } else if (event == CALLBACK_EVENT_MIP_SOLUTION) { return absl::InvalidArgumentError( - absl::StrCat("Must provide primal_solution for event ", event, " (", - ProtoEnumToString(event), ")")); - } - break; - } - - case CALLBACK_EVENT_MESSAGE: { - if (!!cb_data.messages().empty()) { - return absl::InvalidArgumentError( - absl::StrCat("Invalid CallbackDataProto.messages, must provide " - "message(s) for event ", + absl::StrCat("must provide primal_solution_vector for event ", event, " (", ProtoEnumToString(event), ")")); } - for (absl::string_view message : cb_data.messages()) { - // TODO(b/184047243): prefer StrContains on absl version bump - if (message.find('\n') != message.npos) { - return absl::InvalidArgumentError( - absl::StrCat("Invalid CallbackDataProto.messages[], message '", - message, "' contains a new line character '\\n'")); - } - } break; } @@ -262,7 +269,7 @@ absl::Status ValidateCallbackResultProto( if (callback_event != CALLBACK_EVENT_MIP_NODE && callback_event != CALLBACK_EVENT_MIP_SOLUTION) { return absl::InvalidArgumentError(absl::StrCat( - "Invalid CallbackResultProto, can't return cuts for callback_event ", + "invalid CallbackResultProto, can't return cuts for callback_event ", callback_event, "(", ProtoEnumToString(callback_event), ")")); } for (const CallbackResultProto::GeneratedLinearConstraint& cut : @@ -272,23 +279,47 @@ absl::Status ValidateCallbackResultProto( callback_registration.add_lazy_constraints(), model_summary)); } } - if (!callback_result.suggested_solution().empty()) { + if (!callback_result.suggested_solutions().empty()) { if (callback_event != CALLBACK_EVENT_MIP_NODE) { return absl::InvalidArgumentError(absl::StrCat( - "Invalid CallbackResultProto, can't return suggested solutions for " + "invalid CallbackResultProto, can't return suggested solutions for " "callback_event ", callback_event, "(", ProtoEnumToString(callback_event), ")")); } - for (const PrimalSolutionProto& primal_solution : - callback_result.suggested_solution()) { - RETURN_IF_ERROR(ValidatePrimalSolution( - primal_solution, SparseVectorFilterProto(), model_summary)) - << "Invalid CallbackResultProto.suggested_solution"; + for (const SparseDoubleVectorProto& primal_solution_vector : + callback_result.suggested_solutions()) { + RETURN_IF_ERROR(ValidatePrimalSolutionVector( + primal_solution_vector, SparseVectorFilterProto(), model_summary)) + << "invalid CallbackResultProto.suggested_solutions"; } } return absl::OkStatus(); } +absl::Status CheckRegisteredCallbackEvents( + const CallbackRegistrationProto& registration, + const absl::flat_hash_set& supported_events) { + std::vector unsupported_events; + for (const CallbackEventProto event : EventSet(registration)) { + if (!supported_events.contains(event)) { + unsupported_events.push_back(event); + } + } + + if (unsupported_events.empty()) { + return absl::OkStatus(); + } + + std::sort(unsupported_events.begin(), unsupported_events.end()); + + const bool plural = unsupported_events.size() >= 2; + return absl::InvalidArgumentError( + absl::StrCat("event", (plural ? "s { " : " "), + absl::StrJoin(unsupported_events, ", ", + ProtoEnumFormatter()), + (plural ? " } are" : " is"), " not supported")); +} + } // namespace math_opt } // namespace operations_research diff --git a/ortools/math_opt/validators/callback_validator.h b/ortools/math_opt/validators/callback_validator.h index 52ae0dd10d..ba82a375b5 100644 --- a/ortools/math_opt/validators/callback_validator.h +++ b/ortools/math_opt/validators/callback_validator.h @@ -14,6 +14,7 @@ #ifndef OR_TOOLS_MATH_OPT_VALIDATORS_CALLBACK_VALIDATOR_H_ #define OR_TOOLS_MATH_OPT_VALIDATORS_CALLBACK_VALIDATOR_H_ +#include "absl/container/flat_hash_set.h" #include "absl/status/status.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/model_summary.h" @@ -41,6 +42,12 @@ absl::Status ValidateCallbackResultProto( const CallbackRegistrationProto& callback_registration, const ModelSummary& model_summary); +// Returns an InvalidArgumentError if some of the registered events are not +// supported. +absl::Status CheckRegisteredCallbackEvents( + const CallbackRegistrationProto& registration, + const absl::flat_hash_set& supported_events); + } // namespace math_opt } // namespace operations_research diff --git a/ortools/math_opt/validators/enum_sets.cc b/ortools/math_opt/validators/enum_sets.cc new file mode 100644 index 0000000000..185b74c49e --- /dev/null +++ b/ortools/math_opt/validators/enum_sets.cc @@ -0,0 +1,81 @@ +// 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/validators/enum_sets.h" + +#include +#include +#include + +#include "gtest/gtest.h" +#include "absl/strings/str_cat.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/port/proto_utils.h" + +namespace operations_research { +namespace math_opt { + +// portable. +std::vector AllFeasibilityStatuses() { + std::vector values; + for (int f = FeasibilityStatusProto_MIN; f <= FeasibilityStatusProto_MAX; + ++f) { + if (FeasibilityStatusProto_IsValid(f) && + f != FEASIBILITY_STATUS_UNSPECIFIED) { + values.push_back(static_cast(f)); + } + } + return values; +} + +std::vector AllSolutionStatuses() { + std::vector values; + for (int f = SolutionStatusProto_MIN; f <= SolutionStatusProto_MAX; ++f) { + if (SolutionStatusProto_IsValid(f) && f != SOLUTION_STATUS_UNSPECIFIED) { + values.push_back(static_cast(f)); + } + } + return values; +} + +void PrintTo(const SolutionStatusProto& proto, std::ostream* os) { + *os << ProtoEnumToString(proto); +} + +void PrintTo(const FeasibilityStatusProto& proto, std::ostream* os) { + *os << ProtoEnumToString(proto); +} + +void PrintTo(const std::tuple& proto, + std::ostream* os) { + *os << ProtoEnumToString(std::get<0>(proto)) << "_" + << ProtoEnumToString(std::get<1>(proto)); +} + +void PrintTo( + const std::tuple& proto, + std::ostream* os) { + *os << ProtoEnumToString(std::get<0>(proto)) << "_" + << ProtoEnumToString(std::get<1>(proto)); +} + +void PrintTo( + const std::tuple& proto, + std::ostream* os) { + *os << ProtoEnumToString(std::get<0>(proto)) << "_" + << ProtoEnumToString(std::get<1>(proto)); +} + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/validators/enum_sets.h b/ortools/math_opt/validators/enum_sets.h new file mode 100644 index 0000000000..8a1d8ed091 --- /dev/null +++ b/ortools/math_opt/validators/enum_sets.h @@ -0,0 +1,50 @@ +// 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 header defines sets of enum values that can be used for parametrized +// tests. +#ifndef OR_TOOLS_MATH_OPT_VALIDATORS_ENUM_SETS_H_ +#define OR_TOOLS_MATH_OPT_VALIDATORS_ENUM_SETS_H_ + +#include +#include +#include + +#include "gtest/gtest.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/solution.pb.h" + +namespace operations_research { +namespace math_opt { + +// Returns all valid feasibility statuses (i.e does not include UNSPECIFIED). +std::vector AllFeasibilityStatuses(); + +// Returns all valid solution statuses (i.e does not include UNSPECIFIED). +std::vector AllSolutionStatuses(); + +// Printing utilities for parametrized tests. +void PrintTo(const SolutionStatusProto& proto, std::ostream* os); +void PrintTo(const FeasibilityStatusProto& proto, std::ostream* os); +void PrintTo(const std::tuple& proto, + std::ostream* os); +void PrintTo( + const std::tuple& proto, + std::ostream* os); +void PrintTo( + const std::tuple& proto, + std::ostream* os); + +} // namespace math_opt +} // namespace operations_research +#endif // OR_TOOLS_MATH_OPT_VALIDATORS_ENUM_SETS_H_ diff --git a/ortools/math_opt/validators/ids_validator.cc b/ortools/math_opt/validators/ids_validator.cc index 4fe4b9da86..516e78e44d 100644 --- a/ortools/math_opt/validators/ids_validator.cc +++ b/ortools/math_opt/validators/ids_validator.cc @@ -27,6 +27,7 @@ #include "absl/strings/string_view.h" #include "absl/types/span.h" #include "ortools/math_opt/core/model_summary.h" +#include "ortools/base/status_builder.h" #include "ortools/base/status_macros.h" namespace operations_research { @@ -52,9 +53,9 @@ absl::Status CheckSortedIdsSubsetWithIndexOffset( } } if (id_index < ids.size()) { - return absl::InvalidArgumentError( - absl::StrCat("Bad id: ", ids[id_index], - " (at index: ", id_index + offset, ") found")); + return util::InvalidArgumentErrorBuilder() + << "Bad id: " << ids[id_index] << " (at index: " << id_index + offset + << ") found"; } return absl::OkStatus(); } @@ -106,8 +107,9 @@ absl::Status CheckSortedIdsNotBad(const absl::Span ids, } else if (bad_list[bad_index] > ids[id_index]) { ++id_index; } else { - return absl::InvalidArgumentError(absl::StrCat( - "Bad id: ", ids[id_index], " (at index: ", id_index, ") found.")); + return util::InvalidArgumentErrorBuilder() + << "Bad id: " << ids[id_index] << " (at index: " << id_index + << ") found"; } } return absl::OkStatus(); @@ -124,11 +126,12 @@ absl::Status CheckIdsNonnegativeAndStrictlyIncreasing( "index ", i, ", found id: ", ids[i]); if (i == 0) { - return absl::InvalidArgumentError( - absl::StrCat(error_base, " (a negative id)")); + return util::InvalidArgumentErrorBuilder() + << error_base << " (a negative id)"; } else { - return absl::InvalidArgumentError(absl::StrCat( - error_base, " and at index ", i - 1, " found id: ", ids[i - 1])); + return util::InvalidArgumentErrorBuilder() + << error_base << " and at index " << i - 1 + << " found id: " << ids[i - 1]; } } previous = ids[i]; @@ -150,8 +153,8 @@ absl::Status CheckUnsortedIdsSubset(const absl::Span ids, const FastIdCheck id_check(universe); for (int i = 0; i < ids.size(); ++i) { if (!id_check.contains(ids[i])) { - return absl::InvalidArgumentError( - absl::StrCat("Bad id: ", ids[i], " (at index: ", i, ") not found.")); + return util::InvalidArgumentErrorBuilder() + << "Bad id: " << ids[i] << " (at index: " << i << ") not found"; } } return absl::OkStatus(); @@ -161,18 +164,16 @@ absl::Status IdUpdateValidator::IsValid() const { for (int i = 0; i < deleted_ids_.size(); ++i) { const int64_t deleted_id = deleted_ids_[i]; if (!old_ids_.HasId(deleted_id)) { - return absl::InvalidArgumentError( - absl::StrCat("Tried to delete id: ", deleted_id, " (at index: ", i, - ") but it was not present.")); + return util::InvalidArgumentErrorBuilder() + << "Tried to delete id: " << deleted_id << " (at index: " << i + << ") but it was not present"; } } - if (old_ids_.Empty() || new_ids_.empty()) { - return absl::OkStatus(); - } - if (old_ids_.LargestId() >= new_ids_.front()) { - return absl::InvalidArgumentError(absl::StrCat( - "All old ids should be less than all new ids, but final old id was: ", - old_ids_.LargestId(), " and first new id was: ", new_ids_.front())); + if (!new_ids_.empty() && new_ids_.front() < old_ids_.next_free_id()) { + return util::InvalidArgumentErrorBuilder() + << "All new ids should be greater or equal to the first unused id: " + << old_ids_.next_free_id() + << " but the first new id was: " << new_ids_.front(); } return absl::OkStatus(); } @@ -182,8 +183,8 @@ absl::Status IdUpdateValidator::CheckSortedIdsSubsetOfNotDeleted( RETURN_IF_ERROR(CheckSortedIdsNotBad(ids, deleted_ids_)) << " was deleted"; for (int i = 0; i < ids.size(); ++i) { if (!old_ids_.HasId(ids[i])) { - return absl::InvalidArgumentError( - absl::StrCat("Bad id: ", ids[i], " (at index: ", i, ") not found.")); + return util::InvalidArgumentErrorBuilder() + << "Bad id: " << ids[i] << " (at index: " << i << ") not found"; } } return absl::OkStatus(); @@ -207,9 +208,6 @@ absl::Status IdUpdateValidator::CheckSortedIdsSubsetOfFinal( return absl::OkStatus(); } -// Checks that ids is a subset of FINAL = old_ids_ - deleted_ids_ + new_ids_. -// -// If ids is sorted, prefer CheckSortedIdsSubsetOfFinal. absl::Status IdUpdateValidator::CheckIdsSubsetOfFinal( const absl::Span ids) const { if (ids.empty()) { @@ -219,17 +217,17 @@ absl::Status IdUpdateValidator::CheckIdsSubsetOfFinal( const FastIdCheck new_fast(new_ids_); for (int i = 0; i < ids.size(); ++i) { const int64_t id = ids[i]; - if (id <= old_ids_.LargestId()) { - if (!old_ids_.HasId(id)) { - return absl::InvalidArgumentError( - absl::StrCat("Bad id: ", id, " (at index: ", i, ") not found.")); - } else if (deleted_fast.contains(id)) { - return absl::InvalidArgumentError( - absl::StrCat("Bad id: ", id, " (at index: ", i, ") was deleted.")); + if (!new_ids_.empty() && id >= new_ids_[0]) { + if (!new_fast.contains(id)) { + return util::InvalidArgumentErrorBuilder() + << "Bad id: " << id << " (at index: " << i << ") not found"; } - } else if (!new_fast.contains(id)) { - return absl::InvalidArgumentError( - absl::StrCat("Bad id: ", id, " (at index: ", i, ") not found.")); + } else if (!old_ids_.HasId(id)) { + return util::InvalidArgumentErrorBuilder() + << "Bad id: " << id << " (at index: " << i << ") not found"; + } else if (deleted_fast.contains(id)) { + return util::InvalidArgumentErrorBuilder() + << "Bad id: " << id << " (at index: " << i << ") was deleted"; } } return absl::OkStatus(); @@ -242,9 +240,9 @@ absl::Status CheckIdsSubset(absl::Span ids, for (int i = 0; i < ids.size(); ++i) { const int64_t id = ids[i]; if (!universe.HasId(id)) { - return absl::InvalidArgumentError( - absl::StrCat("Id: ", id, " (at index: ", i, ") in ", ids_description, - " is missing from ", universe_description, ".")); + return util::InvalidArgumentErrorBuilder() + << "Id: " << id << " (at index: " << i << ") in " + << ids_description << " is missing from " << universe_description; } } return absl::OkStatus(); @@ -255,9 +253,9 @@ absl::Status CheckIdsIdentical(absl::Span first_ids, absl::string_view first_description, absl::string_view second_description) { if (first_ids.size() != second_ids.Size()) { - return absl::InvalidArgumentError(absl::StrCat( - first_description, " has size ", first_ids.size(), ", but ", - second_description, " has size ", second_ids.Size(), ".")); + return util::InvalidArgumentErrorBuilder() + << first_description << " has size " << first_ids.size() << ", but " + << second_description << " has size " << second_ids.Size(); } RETURN_IF_ERROR(CheckIdsSubset(first_ids, second_ids, first_description, second_description)); diff --git a/ortools/math_opt/validators/ids_validator.h b/ortools/math_opt/validators/ids_validator.h index 4d647b31a8..06d2748821 100644 --- a/ortools/math_opt/validators/ids_validator.h +++ b/ortools/math_opt/validators/ids_validator.h @@ -70,6 +70,7 @@ class IdUpdateValidator { const absl::Span new_ids) : old_ids_(old_ids), deleted_ids_(deleted_ids), new_ids_(new_ids) {} + // Returns true if the sets of ids passed to the constructor are valid. absl::Status IsValid() const; // Checks that ids is a subset of NOT_DELETED = old_ids_ - deleted_ids_. @@ -92,8 +93,8 @@ class IdUpdateValidator { private: // NOT OWNED const IdNameBiMap& old_ids_; - absl::Span deleted_ids_; - absl::Span new_ids_; + const absl::Span deleted_ids_; + const absl::Span new_ids_; }; } // namespace math_opt diff --git a/ortools/math_opt/validators/model_parameters_validator.cc b/ortools/math_opt/validators/model_parameters_validator.cc index bec1173c99..db00f710b5 100644 --- a/ortools/math_opt/validators/model_parameters_validator.cc +++ b/ortools/math_opt/validators/model_parameters_validator.cc @@ -13,22 +13,48 @@ #include "ortools/math_opt/validators/model_parameters_validator.h" -#include - -#include - #include "ortools/base/integral_types.h" #include "absl/status/status.h" -#include "absl/strings/str_cat.h" #include "ortools/math_opt/core/model_summary.h" +#include "ortools/math_opt/core/sparse_vector_view.h" #include "ortools/math_opt/model_parameters.pb.h" #include "ortools/math_opt/sparse_containers.pb.h" #include "ortools/math_opt/validators/ids_validator.h" #include "ortools/math_opt/validators/solution_validator.h" +#include "ortools/math_opt/validators/sparse_vector_validator.h" #include "ortools/base/status_macros.h" namespace operations_research { namespace math_opt { +namespace { +absl::Status ValidateSolutionHint(const SolutionHintProto& solution_hint, + const ModelSummary& model_summary) { + const auto vector_view = MakeView(solution_hint.variable_values()); + RETURN_IF_ERROR(CheckIdsAndValues( + vector_view, + {.allow_positive_infinity = false, .allow_negative_infinity = false})) + << "Invalid solution_hint"; + RETURN_IF_ERROR(CheckIdsSubset(solution_hint.variable_values().ids(), + model_summary.variables, "solution_hint ids", + "model IDs")); + + return absl::OkStatus(); +} + +absl::Status ValidateBranchingPriorities( + const SparseInt32VectorProto& branching_priorities, + const ModelSummary& model_summary) { + const auto vector_view = MakeView(branching_priorities); + RETURN_IF_ERROR(CheckIdsAndValues(vector_view)) + << "Invalid branching_priorities"; + RETURN_IF_ERROR(CheckIdsSubset(branching_priorities.ids(), + model_summary.variables, + "branching_priorities ids", "model IDs")); + + return absl::OkStatus(); +} + +} // namespace absl::Status ValidateSparseVectorFilter(const SparseVectorFilterProto& v, const IdNameBiMap& valid_ids) { @@ -49,19 +75,23 @@ absl::Status ValidateModelSolveParameters( const ModelSolveParametersProto& parameters, const ModelSummary& model_summary) { RETURN_IF_ERROR(ValidateSparseVectorFilter( - parameters.primal_variables_filter(), model_summary.variables)) - << "invalid primal_variables_filter"; - RETURN_IF_ERROR(ValidateSparseVectorFilter(parameters.dual_variables_filter(), + parameters.variable_values_filter(), model_summary.variables)) + << "invalid variable_values_filter"; + RETURN_IF_ERROR(ValidateSparseVectorFilter(parameters.reduced_costs_filter(), model_summary.variables)) - << "invalid dual_variables_filter"; - RETURN_IF_ERROR( - ValidateSparseVectorFilter(parameters.dual_linear_constraints_filter(), - model_summary.linear_constraints)) - << "invalid dual_linear_constraints_filter"; + << "invalid reduced_costs_filter"; + RETURN_IF_ERROR(ValidateSparseVectorFilter(parameters.dual_values_filter(), + model_summary.linear_constraints)) + << "invalid dual_values_filter"; if (parameters.has_initial_basis()) { - RETURN_IF_ERROR(ValidateBasis(parameters.initial_basis(), model_summary)); + RETURN_IF_ERROR(ValidateBasis(parameters.initial_basis(), model_summary, + /*check_dual_feasibility=*/false)); } - + for (const SolutionHintProto& solution_hint : parameters.solution_hints()) { + RETURN_IF_ERROR(ValidateSolutionHint(solution_hint, model_summary)); + } + RETURN_IF_ERROR(ValidateBranchingPriorities(parameters.branching_priorities(), + model_summary)); return absl::OkStatus(); } diff --git a/ortools/math_opt/validators/model_validator.cc b/ortools/math_opt/validators/model_validator.cc index 0563a4ac77..0d16c66444 100644 --- a/ortools/math_opt/validators/model_validator.cc +++ b/ortools/math_opt/validators/model_validator.cc @@ -30,6 +30,7 @@ #include "ortools/math_opt/validators/ids_validator.h" #include "ortools/math_opt/validators/name_validator.h" #include "ortools/math_opt/validators/scalar_validator.h" +#include "ortools/math_opt/validators/sparse_matrix_validator.h" #include "ortools/math_opt/validators/sparse_vector_validator.h" #include "ortools/base/status_macros.h" @@ -90,26 +91,42 @@ absl::Status VariableUpdatesValidForState( absl::Status ObjectiveValid(const ObjectiveProto& objective, absl::Span variable_ids) { + // 1. Validate offset RETURN_IF_ERROR(CheckScalarNoNanNoInf(objective.offset())) << "Objective offset invalid"; - auto coefficients = MakeView(objective.linear_coefficients()); + // 2. Validate linear terms + const auto linear_coefficients = MakeView(objective.linear_coefficients()); RETURN_IF_ERROR(CheckIdsAndValues( - coefficients, + linear_coefficients, {.allow_positive_infinity = false, .allow_negative_infinity = false})) << "Linear objective coefficients bad"; - RETURN_IF_ERROR(CheckSortedIdsSubset(coefficients.ids(), variable_ids)) + RETURN_IF_ERROR(CheckSortedIdsSubset(linear_coefficients.ids(), variable_ids)) << "Objective.linear_coefficients.ids not found in Variables.ids"; + // 3. Validate quadratic terms + RETURN_IF_ERROR(SparseMatrixValid(objective.quadratic_coefficients(), + /*enforce_upper_triangular=*/true)) + << "Objective.quadratic_coefficients invalid"; + RETURN_IF_ERROR(SparseMatrixIdsAreKnown(objective.quadratic_coefficients(), + variable_ids, variable_ids)) + << "Objective.quadratic_coefficients invalid"; return absl::OkStatus(); } +// NOTE: This method does not check requirements on the IDs absl::Status ObjectiveUpdatesValid( const ObjectiveUpdatesProto& objective_updates) { + // 1. Validate offset RETURN_IF_ERROR(CheckScalarNoNanNoInf(objective_updates.offset_update())) << "Offset update invalid"; + // 2. Validate linear terms RETURN_IF_ERROR(CheckIdsAndValues( MakeView(objective_updates.linear_coefficients()), {.allow_positive_infinity = false, .allow_negative_infinity = false})) << "Linear objective coefficients bad"; + // 3. Validate quadratic terms + RETURN_IF_ERROR(SparseMatrixValid(objective_updates.quadratic_coefficients(), + /*enforce_upper_triangular=*/true)) + << "Objective.quadratic_coefficients invalid"; return absl::OkStatus(); } @@ -119,6 +136,12 @@ absl::Status ObjectiveUpdatesValidForModel( RETURN_IF_ERROR(id_validator.CheckSortedIdsSubsetOfFinal( objective_updates.linear_coefficients().ids())) << "Linear coefficients ids not found in variable ids"; + RETURN_IF_ERROR(id_validator.CheckSortedIdsSubsetOfFinal( + objective_updates.quadratic_coefficients().row_ids())) + << "Quadratic coefficient ids bad"; + RETURN_IF_ERROR(id_validator.CheckIdsSubsetOfFinal( + objective_updates.quadratic_coefficients().column_ids())) + << "Quadratic coefficient ids bad"; return absl::OkStatus(); } @@ -164,71 +187,6 @@ absl::Status LinearConstraintUpdatesValidForState( return absl::OkStatus(); } -absl::Status LinearConstraintMatrixValid( - const SparseDoubleMatrixProto& matrix) { - const int nnz = matrix.row_ids_size(); - if (nnz != matrix.column_ids_size()) { - return absl::InvalidArgumentError( - absl::StrCat("Expected row_id.size=", nnz, - " equal to column_ids.size=", matrix.column_ids_size())); - } - if (nnz != matrix.coefficients_size()) { - return absl::InvalidArgumentError(absl::StrCat( - "Expected row_id.size=", nnz, - " equal to coefficients.size=", matrix.coefficients_size())); - } - int64_t previous_row = -1; - int64_t previous_col = -1; - for (int i = 0; i < nnz; ++i) { - const int64_t row = matrix.row_ids(i); - if (row < 0) { - return absl::InvalidArgumentError( - absl::StrCat("row_ids should be nonnegative, but found id: ", row, - " (at index: ", i, ")")); - } - const int64_t col = matrix.column_ids(i); - if (col < 0) { - return absl::InvalidArgumentError( - absl::StrCat("column_ids should be nonnegative, but found id: ", col, - " (at index: ", i, ")")); - } - if (row < previous_row) { - return absl::InvalidArgumentError( - absl::StrCat("row_ids should nondecreasing, but found ids [", - previous_row, row, "] at indices [", i - 1, i, "]")); - } else if (row == previous_row) { - if (previous_col >= col) { - return absl::InvalidArgumentError( - absl::StrCat("column_ids should be strictly increasing within a " - "row, but for row_id: ", - row, " found [", previous_col, ", ", col, - "] at indices, [", i - 1, ", ", i, "]")); - } - } - // When row > previous_row, we have a new row, nothing to check. - if (!std::isfinite(matrix.coefficients(i))) { - return absl::InvalidArgumentError(absl::StrCat( - "Expected finite coefficients without NaN, but at row_id: ", row, - ", column_id: ", col, " found coefficient: ", matrix.coefficients(i), - " (at index: ", i, ")")); - } - previous_row = row; - previous_col = col; - } - return absl::OkStatus(); -} - -absl::Status LinearConstraintMatrixIdsAreKnown( - const SparseDoubleMatrixProto& matrix, - absl::Span linear_constraint_ids, - absl::Span variable_ids) { - RETURN_IF_ERROR(CheckSortedIdsSubset(matrix.row_ids(), linear_constraint_ids)) - << "Unknown linear_constraint_id"; - RETURN_IF_ERROR(CheckUnsortedIdsSubset(matrix.column_ids(), variable_ids)) - << "Unknown variable_id"; - return absl::OkStatus(); -} - absl::Status LinearConstraintMatrixIdsValidForUpdate( const SparseDoubleMatrixProto& matrix, const IdUpdateValidator& linear_constraint_id_validator, @@ -256,11 +214,11 @@ absl::Status ValidateModel(const ModelProto& model, const bool check_names) { RETURN_IF_ERROR( LinearConstraintsValid(model.linear_constraints(), check_names)) << "Model.linear_constraints are invalid"; - RETURN_IF_ERROR(LinearConstraintMatrixValid(model.linear_constraint_matrix())) + RETURN_IF_ERROR(SparseMatrixValid(model.linear_constraint_matrix())) << "Model.linear_constraint_matrix invalid"; - RETURN_IF_ERROR(LinearConstraintMatrixIdsAreKnown( - model.linear_constraint_matrix(), model.linear_constraints().ids(), - model.variables().ids())) + RETURN_IF_ERROR(SparseMatrixIdsAreKnown(model.linear_constraint_matrix(), + model.linear_constraints().ids(), + model.variables().ids())) << "Model.linear_constraint_matrix ids are inconsistent"; return absl::OkStatus(); } @@ -289,8 +247,8 @@ absl::Status ValidateModelUpdate(const ModelUpdateProto& model_update, << "ModelUpdateProto.new_linear_constraints invalid"; RETURN_IF_ERROR(ObjectiveUpdatesValid(model_update.objective_updates())) << "ModelUpdateProto.objective_update invalid"; - RETURN_IF_ERROR(LinearConstraintMatrixValid( - model_update.linear_constraint_matrix_updates())) + RETURN_IF_ERROR( + SparseMatrixValid(model_update.linear_constraint_matrix_updates())) << "Model.linear_constraint_matrix_updates invalid"; return absl::OkStatus(); } diff --git a/ortools/math_opt/validators/model_validator.h b/ortools/math_opt/validators/model_validator.h index e04d6108d1..e233e574dc 100644 --- a/ortools/math_opt/validators/model_validator.h +++ b/ortools/math_opt/validators/model_validator.h @@ -22,14 +22,32 @@ namespace operations_research { namespace math_opt { +// Runs in O(size of model) and allocates O(#variables + #linear constraints) +// memory. absl::Status ValidateModel(const ModelProto& model, bool check_names = true); +// Validates the update as-is; without any knowledge of the model or previous +// updates. Some tests of the validity of ids are also not done. +// // Performance: runs in O(size of update). +// +// See ValidateModelUpdateAndSummary() for a version that does a full +// validation taking into account the model and previous updates. absl::Status ValidateModelUpdate(const ModelUpdateProto& model_update, bool check_names = true); +// Validates the update taking into account the model and previous updates (via +// the provided summary). +// +// Note that this function uses model_summary.(variables|linear_constraints)'s +// next_free_id() to test that new variables/constraints ids are valid. +// // Performance: runs in O(size of update), allocates at most // O(#new or deleted variables + #new or deleted linear constraints). +// +// It internally calls ValidateModelUpdate() which validates all predicates that +// can be validated without knowledge of the initial model and the previous +// updates. absl::Status ValidateModelUpdateAndSummary(const ModelUpdateProto& model_update, const ModelSummary& model_summary, bool check_names = true); diff --git a/ortools/math_opt/validators/result_validator.cc b/ortools/math_opt/validators/result_validator.cc new file mode 100644 index 0000000000..ca5dfd1a90 --- /dev/null +++ b/ortools/math_opt/validators/result_validator.cc @@ -0,0 +1,300 @@ +// 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/validators/result_validator.h" + +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "ortools/math_opt/core/model_summary.h" +#include "ortools/math_opt/model_parameters.pb.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/validators/solution_validator.h" +#include "ortools/math_opt/validators/solve_stats_validator.h" +#include "ortools/port/proto_utils.h" +#include "ortools/base/status_macros.h" + +namespace operations_research { +namespace math_opt { +namespace { + +constexpr double kInf = std::numeric_limits::infinity(); + +absl::Status ValidateSolutionStatus(const SolutionStatusProto& status) { + if (!SolutionStatusProto_IsValid(status)) { + return absl::InvalidArgumentError(absl::StrCat("status = ", status)); + } + if (status == SOLUTION_STATUS_UNSPECIFIED) { + return absl::InvalidArgumentError("status = SOLUTION_STATUS_UNSPECIFIED"); + } + return absl::OkStatus(); +} + +absl::Status ValidateTermination(const TerminationProto& termination) { + if (termination.reason() == TERMINATION_REASON_UNSPECIFIED) { + return absl::InvalidArgumentError("termination reason must be specified"); + } + if (termination.reason() == TERMINATION_REASON_LIMIT_REACHED) { + if (termination.limit() == LIMIT_UNSPECIFIED) { + return absl::InvalidArgumentError( + "for reason TERMINATION_REASON_LIMIT_REACHED, limit must be " + "specified"); + } + } else { + if (termination.limit() != LIMIT_UNSPECIFIED) { + return absl::InvalidArgumentError( + absl::StrCat("for reason:", ProtoEnumToString(termination.reason()), + ", limit should be unspecified, but was set to: ", + ProtoEnumToString(termination.limit()))); + } + } + return absl::OkStatus(); +} + +bool HasPrimalFeasibleSolution(const SolutionProto& solution) { + return solution.has_primal_solution() && + solution.primal_solution().feasibility_status() == + SOLUTION_STATUS_FEASIBLE; +} + +bool HasPrimalFeasibleSolution(const SolveResultProto& result) { + // Assumes first solution is primal feasible if there is any primal solution. + return !result.solutions().empty() && + HasPrimalFeasibleSolution(result.solutions(0)); +} + +bool HasDualFeasibleSolution(const SolutionProto& solution) { + return solution.has_dual_solution() && + solution.dual_solution().feasibility_status() == + SOLUTION_STATUS_FEASIBLE; +} + +bool HasDualFeasibleSolution(const SolveResultProto& result) { + for (const auto& solution : result.solutions()) { + if (HasDualFeasibleSolution(solution)) { + return true; + } + } + return false; +} + +absl::Status ValidateSolutions( + const google::protobuf::RepeatedPtrField& solutions, + const ModelSolveParametersProto& parameters, + const ModelSummary& model_summary) { + // Validate individual solutions + for (int i = 0; i < solutions.size(); ++i) { + RETURN_IF_ERROR(ValidateSolution(solutions[i], parameters, model_summary)) + << "invalid solutions[" << i << "]"; + } + + if (solutions.empty()) return absl::OkStatus(); + + // Validate solution order. + // TODO(b/204457524): check objective ordering when possible. + bool previous_primal_feasible = HasPrimalFeasibleSolution(solutions[0]); + bool previous_dual_feasible = HasDualFeasibleSolution(solutions[0]); + for (int i = 1; i < solutions.size(); ++i) { + const bool current_primal_feasible = + HasPrimalFeasibleSolution(solutions[i]); + const bool current_dual_feasible = HasDualFeasibleSolution(solutions[i]); + // Primal-feasible solutions must appear first. + if (current_primal_feasible && !previous_primal_feasible) { + return absl::InvalidArgumentError( + "primal solution ordering not satisfied"); + } + // Dual-feasible solutions must appear first within the groups of + // primal-feasible and other solutions. Equivalently, a dual-feasible + // solution must be preceded by a dual-feasible solution, except when we + // switch from the group of primal-feasible solutions to the group of other + // solutions. + if (current_dual_feasible && !previous_dual_feasible) { + if (!(previous_primal_feasible && !current_primal_feasible)) { + return absl::InvalidArgumentError( + "dual solution ordering not satisfied"); + } + } + previous_primal_feasible = current_primal_feasible; + previous_dual_feasible = current_dual_feasible; + } + return absl::OkStatus(); +} + +absl::Status RequireNoPrimalFeasibleSolution(const SolveResultProto& result) { + if (HasPrimalFeasibleSolution(result)) { + return absl::InvalidArgumentError( + "expected no primal feasible solution, but one was returned"); + } + + return absl::OkStatus(); +} + +absl::Status RequireNoDualFeasibleSolution(const SolveResultProto& result) { + if (HasDualFeasibleSolution(result)) { + return absl::InvalidArgumentError( + "expected no dual feasible solution, but one was returned"); + } + + return absl::OkStatus(); +} +} // namespace + +absl::Status CheckHasPrimalSolution(const SolveResultProto& result) { + if (!HasPrimalFeasibleSolution(result)) { + return absl::InvalidArgumentError( + "primal feasible solution expected, but not found"); + } + + return absl::OkStatus(); +} + +absl::Status CheckPrimalSolutionAndStatusConsistency( + const SolveResultProto& result) { + if (result.solve_stats().problem_status().primal_status() != + FEASIBILITY_STATUS_FEASIBLE && + HasPrimalFeasibleSolution(result)) { + return absl::InvalidArgumentError( + "primal feasibility status is not FEASIBILITY_STATUS_FEASIBLE, but " + "primal feasible solution is returned."); + } + return absl::OkStatus(); +} + +absl::Status CheckDualSolutionAndStatusConsistency( + const SolveResultProto& result) { + if (result.solve_stats().problem_status().dual_status() != + FEASIBILITY_STATUS_FEASIBLE && + HasDualFeasibleSolution(result)) { + return absl::InvalidArgumentError( + "dual feasibility status is not FEASIBILITY_STATUS_FEASIBLE, but " + "dual feasible solution is returned."); + } + return absl::OkStatus(); +} + +// Assumes ValidateTermination has been called and ValidateProblemStatusProto +// has been called on result.solve_stats.problem_status. +// TODO(b/212685946): add ray checks +absl::Status ValidateTerminationConsistency(const SolveResultProto& result) { + const ProblemStatusProto status = result.solve_stats().problem_status(); + switch (result.termination().reason()) { + case TERMINATION_REASON_OPTIMAL: + RETURN_IF_ERROR(CheckPrimalStatusIs(status, FEASIBILITY_STATUS_FEASIBLE)); + RETURN_IF_ERROR(CheckDualStatusIs(status, FEASIBILITY_STATUS_FEASIBLE)); + RETURN_IF_ERROR(CheckHasPrimalSolution(result)); + // Dual feasible solution is not required. + // Primal/dual requirements imply primal/dual solution-status consistency. + return absl::OkStatus(); + // TODO(b/211677729): update when TERMINATION_REASON_FEASIBLE is added. + case TERMINATION_REASON_INFEASIBLE: + RETURN_IF_ERROR( + CheckPrimalStatusIs(status, FEASIBILITY_STATUS_INFEASIBLE)); + RETURN_IF_ERROR(RequireNoPrimalFeasibleSolution(result)); + // Primal requirements imply primal solution-status consistency. + // No dual requirements so we check consistency. + RETURN_IF_ERROR(CheckDualSolutionAndStatusConsistency(result)); + return absl::OkStatus(); + case TERMINATION_REASON_UNBOUNDED: + RETURN_IF_ERROR(CheckPrimalStatusIs(status, FEASIBILITY_STATUS_FEASIBLE)); + RETURN_IF_ERROR(CheckDualStatusIs(status, FEASIBILITY_STATUS_INFEASIBLE)); + // No primal feasible solution is required. + RETURN_IF_ERROR(RequireNoDualFeasibleSolution(result)); + // Primal/dual requirements imply primal/dual solution-status consistency. + return absl::OkStatus(); + case TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED: + RETURN_IF_ERROR( + CheckPrimalStatusIs(status, FEASIBILITY_STATUS_UNDETERMINED)); + RETURN_IF_ERROR( + CheckDualStatusIs(status, FEASIBILITY_STATUS_INFEASIBLE, + /*primal_or_dual_infeasible_also_ok=*/true)); + RETURN_IF_ERROR(RequireNoPrimalFeasibleSolution(result)); + RETURN_IF_ERROR(RequireNoDualFeasibleSolution(result)); + // Primal/dual requirements imply primal/dual solution-status consistency. + // Note if primal status was not FEASIBILITY_STATUS_UNDETERMINED, then + // primal_or_dual_infeasible must be false and dual status would be + // FEASIBILITY_STATUS_INFEASIBLE. Then if primal status was + // FEASIBILITY_STATUS_INFEASIBLE we would have + // TERMINATION_REASON_INFEASIBLE and if it was FEASIBILITY_STATUS_FEASIBLE + // we would have TERMINATION_REASON_UNBOUNDED. + return absl::OkStatus(); + case TERMINATION_REASON_IMPRECISE: + // TODO(b/211679884): update when imprecise solutions are added. + return absl::OkStatus(); + case TERMINATION_REASON_LIMIT_REACHED: + // TODO(b/211677729): update when TERMINATION_REASON_FEASIBLE is added. + // No primal or dual requirements so we check consistency. + RETURN_IF_ERROR(CheckPrimalSolutionAndStatusConsistency(result)); + RETURN_IF_ERROR(CheckDualSolutionAndStatusConsistency(result)); + return absl::OkStatus(); + case TERMINATION_REASON_NUMERICAL_ERROR: + case TERMINATION_REASON_OTHER_ERROR: { + RETURN_IF_ERROR( + CheckPrimalStatusIs(status, FEASIBILITY_STATUS_UNDETERMINED)); + RETURN_IF_ERROR( + CheckDualStatusIs(status, FEASIBILITY_STATUS_UNDETERMINED)); + if (!result.solutions().empty()) { + return absl::InvalidArgumentError( + absl::StrCat("termination reason is ", + ProtoEnumToString(result.termination().reason()), + ", but solutions are available")); + } + if (result.solve_stats().problem_status().primal_or_dual_infeasible()) { + return absl::InvalidArgumentError(absl::StrCat( + "termination reason is ", + ProtoEnumToString(result.termination().reason()), + ", but solve_stats.problem_status.primal_or_dual_infeasible = " + "true")); + } + // Primal/dual requirements imply primal/dual solution-status consistency. + } + return absl::OkStatus(); + default: + LOG(FATAL) << ProtoEnumToString(result.termination().reason()) + << " not implemented"; + } + + return absl::OkStatus(); +} + +absl::Status ValidateResult(const SolveResultProto& result, + const ModelSolveParametersProto& parameters, + const ModelSummary& model_summary) { + RETURN_IF_ERROR(ValidateTermination(result.termination())); + RETURN_IF_ERROR(ValidateSolveStats(result.solve_stats())); + RETURN_IF_ERROR( + ValidateSolutions(result.solutions(), parameters, model_summary)); + + for (int i = 0; i < result.primal_rays_size(); ++i) { + RETURN_IF_ERROR(ValidatePrimalRay(result.primal_rays(i), + parameters.variable_values_filter(), + model_summary)) + << "Invalid primal_rays[" << i << "]"; + } + for (int i = 0; i < result.dual_rays_size(); ++i) { + RETURN_IF_ERROR( + ValidateDualRay(result.dual_rays(i), parameters, model_summary)) + << "Invalid dual_rays[" << i << "]"; + } + + RETURN_IF_ERROR(ValidateTerminationConsistency(result)) + << "inconsistent termination reason " + << ProtoEnumToString(result.termination().reason()); + + return absl::OkStatus(); +} + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/validators/result_validator.h b/ortools/math_opt/validators/result_validator.h new file mode 100644 index 0000000000..ff4b3ce667 --- /dev/null +++ b/ortools/math_opt/validators/result_validator.h @@ -0,0 +1,40 @@ +// 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_VALIDATORS_RESULT_VALIDATOR_H_ +#define OR_TOOLS_MATH_OPT_VALIDATORS_RESULT_VALIDATOR_H_ + +#include "absl/status/status.h" +#include "ortools/math_opt/core/model_summary.h" +#include "ortools/math_opt/model_parameters.pb.h" +#include "ortools/math_opt/result.pb.h" + +namespace operations_research { +namespace math_opt { + +// Validates the input result. +absl::Status ValidateResult(const SolveResultProto& result, + const ModelSolveParametersProto& parameters, + const ModelSummary& model_summary); + +// Returns absl::Ok only if a primal feasible solution is available. +absl::Status CheckHasPrimalSolution(const SolveResultProto& result); +absl::Status CheckPrimalSolutionAndStatusConsistency( + const SolveResultProto& result); +absl::Status CheckDualSolutionAndStatusConsistency( + const SolveResultProto& result); + +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_VALIDATORS_RESULT_VALIDATOR_H_ diff --git a/ortools/math_opt/validators/solution_validator.cc b/ortools/math_opt/validators/solution_validator.cc index 6a4b5456c5..aa3ce98ee5 100644 --- a/ortools/math_opt/validators/solution_validator.cc +++ b/ortools/math_opt/validators/solution_validator.cc @@ -17,14 +17,12 @@ #include #include -#include "ortools/base/integral_types.h" #include "absl/status/status.h" #include "absl/strings/str_cat.h" #include "ortools/math_opt/core/math_opt_proto_utils.h" #include "ortools/math_opt/core/model_summary.h" #include "ortools/math_opt/core/sparse_vector_view.h" #include "ortools/math_opt/model_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" #include "ortools/math_opt/validators/ids_validator.h" @@ -38,43 +36,18 @@ namespace { constexpr double kInf = std::numeric_limits::infinity(); -} // namespace - -absl::Status ValidateResult(const SolveResultProto& result, - const ModelSolveParametersProto& parameters, - const ModelSummary& model_summary) { - for (int i = 0; i < result.primal_solutions_size(); ++i) { - RETURN_IF_ERROR(ValidatePrimalSolution(result.primal_solutions(i), - parameters.primal_variables_filter(), - model_summary)) - << "Invalid primal_solutions[" << i << "]"; +absl::Status ValidateSolutionStatus(const SolutionStatusProto& status) { + if (!SolutionStatusProto_IsValid(status)) { + return absl::InvalidArgumentError(absl::StrCat("status = ", status)); } - for (int i = 0; i < result.primal_rays_size(); ++i) { - RETURN_IF_ERROR(ValidatePrimalRay(result.primal_rays(i), - parameters.primal_variables_filter(), - model_summary)) - << "Invalid primal_rays[" << i << "]"; + if (status == SOLUTION_STATUS_UNSPECIFIED) { + return absl::InvalidArgumentError("status = SOLUTION_STATUS_UNSPECIFIED"); } - for (int i = 0; i < result.dual_solutions_size(); ++i) { - RETURN_IF_ERROR(ValidateDualSolution(result.dual_solutions(i), parameters, - model_summary)) - << "Invalid dual_solutions[" << i << "]"; - } - for (int i = 0; i < result.dual_rays_size(); ++i) { - RETURN_IF_ERROR( - ValidateDualRay(result.dual_rays(i), parameters, model_summary)) - << "Invalid dual_rays[" << i << "]"; - } - for (int i = 0; i < result.basis_size(); ++i) { - RETURN_IF_ERROR(ValidateBasis(result.basis(i), model_summary)); - } - - // TODO(b/174345677): validates all other contracts of the result (we have one - // solution when the termination_reason says so, ...) - return absl::OkStatus(); } +} // namespace + //////////////////////////////////////////////////////////////////////////////// // Solutions & Rays //////////////////////////////////////////////////////////////////////////////// @@ -142,11 +115,65 @@ absl::Status IsValidSolutionVector(const SparseDoubleVectorProto& vector, } // namespace +absl::Status ValidateSolution(const SolutionProto& solution, + const ModelSolveParametersProto& parameters, + const ModelSummary& model_summary) { + if (!solution.has_primal_solution() && !solution.has_dual_solution() && + !solution.has_basis()) { + return absl::InvalidArgumentError("empty solution"); + } + if (solution.has_primal_solution()) { + RETURN_IF_ERROR(ValidatePrimalSolution(solution.primal_solution(), + parameters.variable_values_filter(), + model_summary)) + << "Invalid primal_solution"; + } + if (solution.has_dual_solution()) { + RETURN_IF_ERROR(ValidateDualSolution(solution.dual_solution(), parameters, + model_summary)) + << "Invalid dual_solution"; + } + if (solution.has_basis()) { + RETURN_IF_ERROR(ValidateBasis(solution.basis(), model_summary)) + << "Invalid basis"; + } + // TODO(b/204457524): consider checking equality of statuses for single-sided + // LPs. + if (solution.has_basis() && solution.has_dual_solution()) { + if (solution.basis().basic_dual_feasibility() == SOLUTION_STATUS_FEASIBLE && + solution.dual_solution().feasibility_status() != + SOLUTION_STATUS_FEASIBLE) { + return absl::InvalidArgumentError( + "Incompatible basis and dual solution: basis is dual feasible, but " + "dual solution is not feasible"); + } + if (solution.dual_solution().feasibility_status() == + SOLUTION_STATUS_INFEASIBLE && + solution.basis().basic_dual_feasibility() != + SOLUTION_STATUS_INFEASIBLE) { + return absl::InvalidArgumentError( + "Incompatible basis and dual solution: dual solution is infeasible, " + "but basis is not dual infeasible"); + } + } + return absl::OkStatus(); +} + +absl::Status ValidatePrimalSolutionVector(const SparseDoubleVectorProto& vector, + const SparseVectorFilterProto& filter, + const ModelSummary& model_summary) { + RETURN_IF_ERROR( + IsValidSolutionVector(vector, filter, model_summary.variables)); + return absl::OkStatus(); +} + absl::Status ValidatePrimalSolution(const PrimalSolutionProto& primal_solution, const SparseVectorFilterProto& filter, const ModelSummary& model_summary) { - RETURN_IF_ERROR(IsValidSolutionVector(primal_solution.variable_values(), - filter, model_summary.variables)) + RETURN_IF_ERROR(ValidateSolutionStatus(primal_solution.feasibility_status())) + << "Invalid PrimalSolutionProto.feasibility_status"; + RETURN_IF_ERROR(ValidatePrimalSolutionVector( + primal_solution.variable_values(), filter, model_summary)) << "Invalid PrimalSolutionProto.variable_values"; RETURN_IF_ERROR(CheckScalarNoNanNoInf(primal_solution.objective_value())) << "Invalid PrimalSolutionProto.objective_value"; @@ -165,12 +192,14 @@ absl::Status ValidatePrimalRay(const PrimalRayProto& primal_ray, absl::Status ValidateDualSolution(const DualSolutionProto& dual_solution, const ModelSolveParametersProto& parameters, const ModelSummary& model_summary) { - RETURN_IF_ERROR(IsValidSolutionVector( - dual_solution.dual_values(), parameters.dual_linear_constraints_filter(), - model_summary.linear_constraints)) + RETURN_IF_ERROR(ValidateSolutionStatus(dual_solution.feasibility_status())) + << "Invalid DualSolutionProto.feasibility_status"; + RETURN_IF_ERROR(IsValidSolutionVector(dual_solution.dual_values(), + parameters.dual_values_filter(), + model_summary.linear_constraints)) << "Invalid DualSolutionProto.dual_values"; RETURN_IF_ERROR(IsValidSolutionVector(dual_solution.reduced_costs(), - parameters.dual_variables_filter(), + parameters.reduced_costs_filter(), model_summary.variables)) << "Invalid DualSolutionProto.reduced_costs"; RETURN_IF_ERROR(CheckScalarNoNanNoInf(dual_solution.objective_value())) @@ -181,12 +210,12 @@ absl::Status ValidateDualSolution(const DualSolutionProto& dual_solution, absl::Status ValidateDualRay(const DualRayProto& dual_ray, const ModelSolveParametersProto& parameters, const ModelSummary& model_summary) { - RETURN_IF_ERROR(IsValidSolutionVector( - dual_ray.dual_values(), parameters.dual_linear_constraints_filter(), - model_summary.linear_constraints)) + RETURN_IF_ERROR(IsValidSolutionVector(dual_ray.dual_values(), + parameters.dual_values_filter(), + model_summary.linear_constraints)) << "Invalid DualRayProto.dual_values"; RETURN_IF_ERROR(IsValidSolutionVector(dual_ray.reduced_costs(), - parameters.dual_variables_filter(), + parameters.reduced_costs_filter(), model_summary.variables)) << "Invalid DualRayProto.reduced_costs"; return absl::OkStatus(); @@ -200,20 +229,25 @@ absl::Status SparseBasisStatusVectorIsValid( const SparseVectorView& status_vector_view) { RETURN_IF_ERROR(CheckIdsAndValues(status_vector_view)); for (auto [id, value] : status_vector_view) { - if (!BasisStatus_IsValid(value)) { + if (!BasisStatusProto_IsValid(value)) { return absl::InvalidArgumentError( absl::StrCat("Invalid status: ", value, " for id ", id)); } - if (value == BasisStatus::INVALID) { + if (value == BASIS_STATUS_UNSPECIFIED) { return absl::InvalidArgumentError( - absl::StrCat("Found BasisStatus::INVALID for id ", id)); + absl::StrCat("Found BASIS_STATUS_UNSPECIFIED for id ", id)); } } return absl::OkStatus(); } absl::Status ValidateBasis(const BasisProto& basis, - const ModelSummary& model_summary) { + const ModelSummary& model_summary, + const bool check_dual_feasibility) { + if (check_dual_feasibility) { + RETURN_IF_ERROR(ValidateSolutionStatus(basis.basic_dual_feasibility())) + << "Invalid BasisProto.basic_dual_feasibility"; + } const auto constraint_status_view = MakeView(basis.constraint_status()); const auto variable_status_view = MakeView(basis.variable_status()); RETURN_IF_ERROR(SparseBasisStatusVectorIsValid(constraint_status_view)) @@ -230,12 +264,12 @@ absl::Status ValidateBasis(const BasisProto& basis, int non_basic_variables = 0; for (const auto [id, value] : constraint_status_view) { - if (value != BasisStatus::BASIC) { + if (value != BASIS_STATUS_BASIC) { non_basic_variables++; } } for (auto [id, value] : variable_status_view) { - if (value != BasisStatus::BASIC) { + if (value != BASIS_STATUS_BASIC) { non_basic_variables++; } } diff --git a/ortools/math_opt/validators/solution_validator.h b/ortools/math_opt/validators/solution_validator.h index e81d325b62..4f2dabac75 100644 --- a/ortools/math_opt/validators/solution_validator.h +++ b/ortools/math_opt/validators/solution_validator.h @@ -17,24 +17,28 @@ #include "absl/status/status.h" #include "ortools/math_opt/core/model_summary.h" #include "ortools/math_opt/model_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 { namespace math_opt { -// Runs in O(size of model) and allocates O(#variables + #linear constraints) -// memory. -class SparseVectorFilterProto; - -// Validates the input result. -absl::Status ValidateResult(const SolveResultProto& result, - const ModelSolveParametersProto& parameters, - const ModelSummary& model_summary); +absl::Status ValidateSolution(const SolutionProto& solution, + const ModelSolveParametersProto& parameters, + const ModelSummary& model_summary); absl::Status ValidatePrimalSolution(const PrimalSolutionProto& primal_solution, const SparseVectorFilterProto& filter, const ModelSummary& model_summary); + +// Used to validate callback solutions, which are represented by a +// SparseDoubleVectorProto and not the full PrimalSolutionProto message. Does +// the same checks on a SparseDoubleVectorProto that ValidatePrimalSolution does +// on the variable_values sub-message of a PrimalSolutionProto. +absl::Status ValidatePrimalSolutionVector(const SparseDoubleVectorProto& vector, + const SparseVectorFilterProto& filter, + const ModelSummary& model_summary); + absl::Status ValidatePrimalRay(const PrimalRayProto& primal_ray, const SparseVectorFilterProto& filter, const ModelSummary& model_summary); @@ -42,12 +46,14 @@ absl::Status ValidatePrimalRay(const PrimalRayProto& primal_ray, absl::Status ValidateDualSolution(const DualSolutionProto& dual_solution, const ModelSolveParametersProto& parameters, const ModelSummary& model_summary); + absl::Status ValidateDualRay(const DualRayProto& dual_ray, const ModelSolveParametersProto& parameters, const ModelSummary& model_summary); absl::Status ValidateBasis(const BasisProto& basis, - const ModelSummary& model_summary); + const ModelSummary& model_summary, + bool check_dual_feasibility = true); } // namespace math_opt } // namespace operations_research diff --git a/ortools/math_opt/validators/solve_stats_validator.cc b/ortools/math_opt/validators/solve_stats_validator.cc new file mode 100644 index 0000000000..d9df9d7e16 --- /dev/null +++ b/ortools/math_opt/validators/solve_stats_validator.cc @@ -0,0 +1,175 @@ +// Copyright 2010-2021 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/math_opt/validators/solve_stats_validator.h" + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/time/time.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/validators/scalar_validator.h" +#include "ortools/port/proto_utils.h" +#include "ortools/base/status_macros.h" +#include "ortools/base/protoutil.h" + +namespace operations_research { +namespace math_opt { +namespace { + +constexpr double kInf = std::numeric_limits::infinity(); + +absl::Status ValidateFeasibilityStatus(const FeasibilityStatusProto& status) { + if (!FeasibilityStatusProto_IsValid(status)) { + return absl::InvalidArgumentError(absl::StrCat("invalid status ", status)); + } + if (status == FEASIBILITY_STATUS_UNSPECIFIED) { + return absl::InvalidArgumentError( + "invalid status FEASIBILITY_STATUS_UNSPECIFIED"); + } + return absl::OkStatus(); +} +} // namespace + +absl::Status ValidateProblemStatus(const ProblemStatusProto& status) { + RETURN_IF_ERROR(ValidateFeasibilityStatus(status.primal_status())) + << "invalid primal_status"; + RETURN_IF_ERROR(ValidateFeasibilityStatus(status.dual_status())) + << "invalid dual_status"; + if (status.primal_or_dual_infeasible() && + (status.primal_status() != FEASIBILITY_STATUS_UNDETERMINED || + status.dual_status() != FEASIBILITY_STATUS_UNDETERMINED)) { + return absl::InvalidArgumentError(absl::StrCat( + "primal_or_dual_infeasible can be true only when primal status = dual " + "status = FEASIBILITY_STATUS_UNDETERMINED, and we have primal status " + "= ", + ProtoEnumToString(status.primal_status()), + " and dual status = ", ProtoEnumToString(status.dual_status()))); + } + return absl::OkStatus(); +} + +// Assumes ValidateProblemStatus(status) is ok. +absl::Status CheckPrimalStatusIs(const ProblemStatusProto& status, + const FeasibilityStatusProto required_status) { + const FeasibilityStatusProto actual_status = status.primal_status(); + if (actual_status == required_status) { + return absl::OkStatus(); + } + return absl::InvalidArgumentError( + absl::StrCat("expected problem_status.primal_status = ", + ProtoEnumToString(required_status), ", but was ", + ProtoEnumToString(actual_status))); +} + +// Assumes ValidateProblemStatus(status) is ok. +absl::Status CheckDualStatusIs(const ProblemStatusProto& status, + const FeasibilityStatusProto required_status, + const bool primal_or_dual_infeasible_also_ok) { + const FeasibilityStatusProto actual_status = status.dual_status(); + if (actual_status == required_status) { + return absl::OkStatus(); + } + if (primal_or_dual_infeasible_also_ok && status.primal_or_dual_infeasible()) { + // ValidateProblemStatus call above guarantees primal and dual statuses + // are FEASIBILITY_STATUS_UNDETERMINED here. + return absl::OkStatus(); + } + if (primal_or_dual_infeasible_also_ok) { + return absl::InvalidArgumentError(absl::StrCat( + "expected either problem_status.dual_status = ", + ProtoEnumToString(required_status), " (and was ", + ProtoEnumToString(actual_status), + ") or problem_status.primal_or_dual_infeasible = true (and " + "was false)")); + } + return absl::InvalidArgumentError( + absl::StrCat("expected problem_status.dual_status = ", + ProtoEnumToString(required_status), ", but was ", + ProtoEnumToString(actual_status))); +} + +namespace { +// Assumes ValidateSolveStats(solve_stats) is ok. +absl::Status ValidateSolveStatsConsistency(const SolveStatsProto& solve_stats) { + // TODO(b/204457524): refine validator once optimization direction is in + // model summary (i.e. avoid the absl::abs). + if (solve_stats.problem_status().primal_or_dual_infeasible() && + std::isfinite(solve_stats.best_primal_bound())) { + return absl::InvalidArgumentError( + "best_primal_bound is finite, but problem status is " + "primal_or_dual_infeasible"); + } + if (solve_stats.problem_status().primal_or_dual_infeasible() && + std::isfinite(solve_stats.best_dual_bound())) { + return absl::InvalidArgumentError( + "best_dual_bound is finite, but problem status is " + "primal_or_dual_infeasible"); + } + if (solve_stats.problem_status().primal_status() != + FEASIBILITY_STATUS_FEASIBLE && + std::isfinite(solve_stats.best_primal_bound())) { + return absl::InvalidArgumentError( + absl::StrCat("best_primal_bound is finite, but primal_status is not " + "feasible (primal_status = ", + solve_stats.problem_status().primal_status())); + } + if (solve_stats.problem_status().dual_status() != + FEASIBILITY_STATUS_FEASIBLE && + std::isfinite(solve_stats.best_dual_bound())) { + return absl::InvalidArgumentError( + absl::StrCat("best_dual_bound is finite, but dual_status is not " + "feasible (dual_status = ", + solve_stats.problem_status().dual_status())); + } + return absl::OkStatus(); +} +} // namespace + +absl::Status ValidateSolveStats(const SolveStatsProto& solve_stats) { + const absl::StatusOr solve_time = + util_time::DecodeGoogleApiProto(solve_stats.solve_time()); + if (!solve_time.ok()) { + return absl::InvalidArgumentError( + absl::StrCat("invalid solve_time, ", solve_time.status().message())); + } + if (solve_time.value() < absl::ZeroDuration()) { + return absl::InvalidArgumentError("solve_time must be non-negative"); + } + if (solve_stats.simplex_iterations() < 0) { + return absl::InvalidArgumentError( + "simplex_iterations must be non-negative"); + } + if (solve_stats.barrier_iterations() < 0) { + return absl::InvalidArgumentError( + "barrier_iterations must be non-negative"); + } + if (solve_stats.node_count() < 0) { + return absl::InvalidArgumentError("node_count must be non-negative"); + } + RETURN_IF_ERROR(ValidateProblemStatus(solve_stats.problem_status())); + const DoubleOptions nonan; + RETURN_IF_ERROR(CheckScalar(solve_stats.best_primal_bound(), nonan)) + << "in best_primal_bound"; + RETURN_IF_ERROR(CheckScalar(solve_stats.best_dual_bound(), nonan)) + << "in best_dual_bound"; + RETURN_IF_ERROR(ValidateSolveStatsConsistency(solve_stats)); + return absl::OkStatus(); +} + +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/math_opt/validators/solve_stats_validator.h b/ortools/math_opt/validators/solve_stats_validator.h new file mode 100644 index 0000000000..a07a69cb51 --- /dev/null +++ b/ortools/math_opt/validators/solve_stats_validator.h @@ -0,0 +1,42 @@ +// 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_VALIDATORS_SOLVE_STATS_VALIDATOR_H_ +#define OR_TOOLS_MATH_OPT_VALIDATORS_SOLVE_STATS_VALIDATOR_H_ + +#include "absl/status/status.h" +#include "ortools/math_opt/result.pb.h" + +namespace operations_research { +namespace math_opt { + +absl::Status ValidateProblemStatus(const ProblemStatusProto& status); +absl::Status ValidateSolveStats(const SolveStatsProto& solve_stats); + +// Returns absl::Ok only if status.primal_status = required_status. Assumes +// validateProblemStatus(status) returns absl::Ok. +absl::Status CheckPrimalStatusIs(const ProblemStatusProto& status, + FeasibilityStatusProto required_status); + +// If primal_or_dual_infeasible_also_ok is false, returns absl::Ok only if +// status.dual_status = required_status. If primal_or_dual_infeasible_also_ok +// is true, it returns absl::Ok when status.dual_status = required_status and +// when primal_or_dual_infeasible is true. Assumes validateProblemStatus(status) +// returns absl::Ok. +absl::Status CheckDualStatusIs(const ProblemStatusProto& status, + FeasibilityStatusProto required_status, + bool primal_or_dual_infeasible_also_ok = false); +} // namespace math_opt +} // namespace operations_research + +#endif // OR_TOOLS_MATH_OPT_VALIDATORS_SOLVE_STATS_VALIDATOR_H_ diff --git a/ortools/math_opt/validators/solver_parameters_validator.cc b/ortools/math_opt/validators/solver_parameters_validator.cc index 6d94a72bf4..c28d6f2b6d 100644 --- a/ortools/math_opt/validators/solver_parameters_validator.cc +++ b/ortools/math_opt/validators/solver_parameters_validator.cc @@ -27,16 +27,30 @@ namespace operations_research { namespace math_opt { absl::Status ValidateSolverParameters(const SolveParametersProto& parameters) { - RETURN_IF_ERROR(util_time::DecodeGoogleApiProto( - parameters.common_parameters().time_limit()) - .status()) - << "invalid parameters.common_parameters.time_limit"; + RETURN_IF_ERROR( + util_time::DecodeGoogleApiProto(parameters.time_limit()).status()) + << "invalid parameters.time_limit"; - if (parameters.common_parameters().has_threads()) { - if (parameters.common_parameters().threads() <= 0) { + if (parameters.has_threads()) { + if (parameters.threads() <= 0) { return absl::InvalidArgumentError( - absl::StrCat("parameters.common_parameters.threads = ", - parameters.common_parameters().threads(), " <= 0")); + absl::StrCat("parameters.threads = ", parameters.threads(), " <= 0")); + } + } + + if (parameters.has_relative_gap_limit()) { + if (parameters.relative_gap_limit() < 0) { + return absl::InvalidArgumentError(absl::StrCat( + "parameters.relative_gap_limit = ", parameters.relative_gap_limit(), + " < 0")); + } + } + + if (parameters.has_absolute_gap_limit()) { + if (parameters.absolute_gap_limit() < 0) { + return absl::InvalidArgumentError(absl::StrCat( + "parameters.absolute_gap_limit = ", parameters.absolute_gap_limit(), + " < 0")); } } diff --git a/ortools/math_opt/validators/sparse_matrix_validator.cc b/ortools/math_opt/validators/sparse_matrix_validator.cc new file mode 100644 index 0000000000..4f572729ff --- /dev/null +++ b/ortools/math_opt/validators/sparse_matrix_validator.cc @@ -0,0 +1,102 @@ +// 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/validators/sparse_matrix_validator.h" + +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/types/span.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/validators/ids_validator.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" + +namespace operations_research::math_opt { + +absl::Status SparseMatrixValid(const SparseDoubleMatrixProto& matrix, + const bool enforce_upper_triangular) { + const int nnz = matrix.row_ids_size(); + if (nnz != matrix.column_ids_size()) { + return util::InvalidArgumentErrorBuilder() + << "Expected row_id.size=" << nnz + << " equal to column_ids.size=" << matrix.column_ids_size(); + } + if (nnz != matrix.coefficients_size()) { + return util::InvalidArgumentErrorBuilder() + << "Expected row_id.size=" << nnz + << " equal to coefficients.size=" << matrix.coefficients_size(); + } + int64_t previous_row = -1; + int64_t previous_col = -1; + for (int i = 0; i < nnz; ++i) { + const int64_t row = matrix.row_ids(i); + if (row < 0) { + return util::InvalidArgumentErrorBuilder() + << "row_ids should be nonnegative, but found id: " << row + << " (at index: " << i << ")"; + } + const int64_t col = matrix.column_ids(i); + if (col < 0) { + return util::InvalidArgumentErrorBuilder() + << "column_ids should be nonnegative, but found id: " << col + << " (at index: " << i << ")"; + } + if (enforce_upper_triangular && row > col) { + return util::InvalidArgumentErrorBuilder() + << "lower triangular entry at [" << row << ", " << col + << "] (at index: " << i << ")"; + } + if (row < previous_row) { + return util::InvalidArgumentErrorBuilder() + << "row_ids should be nondecreasing, but found ids [" + << previous_row << ", " << row << "] at indices [" << i - 1 << ", " + << i << "]"; + } else if (row == previous_row) { + if (previous_col >= col) { + return util::InvalidArgumentErrorBuilder() + << "column_ids should be strictly increasing within a row, but " + "for row_id: " + << row << " found [" << previous_col << ", " << col + << "] at indices, [" << i - 1 << ", " << i << "]"; + } + } + // When row > previous_row, we have a new row, nothing to check. + if (!std::isfinite(matrix.coefficients(i))) { + return util::InvalidArgumentErrorBuilder() + << "Expected finite coefficients without NaN, but at row_id: " + << row << ", column_id: " << col + << " found coefficient: " << matrix.coefficients(i) + << " (at index: " << i << ")"; + } + previous_row = row; + previous_col = col; + } + return absl::OkStatus(); +} + +absl::Status SparseMatrixIdsAreKnown( + const SparseDoubleMatrixProto& matrix, + const absl::Span row_ids, + const absl::Span column_ids) { + RETURN_IF_ERROR(CheckSortedIdsSubset(matrix.row_ids(), row_ids)) + << "Unknown row_id"; + RETURN_IF_ERROR(CheckUnsortedIdsSubset(matrix.column_ids(), column_ids)) + << "Unknown column_id"; + return absl::OkStatus(); +} + +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/validators/sparse_matrix_validator.h b/ortools/math_opt/validators/sparse_matrix_validator.h new file mode 100644 index 0000000000..e4facea7a2 --- /dev/null +++ b/ortools/math_opt/validators/sparse_matrix_validator.h @@ -0,0 +1,44 @@ +// Copyright 2010-2021 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_MATH_OPT_VALIDATORS_SPARSE_MATRIX_VALIDATOR_H_ +#define OR_TOOLS_MATH_OPT_VALIDATORS_SPARSE_MATRIX_VALIDATOR_H_ + +#include + +#include "absl/status/status.h" +#include "absl/types/span.h" +#include "ortools/math_opt/model.pb.h" + +namespace operations_research::math_opt { + +// Validates that the input satisfies the following invariants: +// 1. matrix.row_ids, matrix.column_ids, and matrix.coefficients are all the +// same length. +// 2. matrix.row_ids and matrix.column_ids are nonnegative. +// 3. The matrix is in row major ordering with no repeats. +// 4. Each entry in matrix.coefficients is finite and not NaN. +// 5. If enforce_upper_triangular=true, then matrix must be upper triangular. +absl::Status SparseMatrixValid(const SparseDoubleMatrixProto& matrix, + bool enforce_upper_triangular = false); + +// Verifies that: +// 1. matrix.row_ids is a subset of row_ids. +// 2. matrix.column_ids is a subset of column_ids. +absl::Status SparseMatrixIdsAreKnown(const SparseDoubleMatrixProto& matrix, + absl::Span row_ids, + absl::Span column_ids); + +} // namespace operations_research::math_opt + +#endif // OR_TOOLS_MATH_OPT_VALIDATORS_SPARSE_MATRIX_VALIDATOR_H_ diff --git a/ortools/math_opt/validators/sparse_vector_validator.h b/ortools/math_opt/validators/sparse_vector_validator.h index 21ad0b6ab9..568c583ce8 100644 --- a/ortools/math_opt/validators/sparse_vector_validator.h +++ b/ortools/math_opt/validators/sparse_vector_validator.h @@ -38,7 +38,7 @@ absl::Status CheckIdsAndValuesSize(const SparseVectorView& vector_view, } template ::value> > + typename = std::enable_if_t::value> > absl::Status CheckValues(const SparseVectorView& vector_view, absl::string_view value_name = "values") { RETURN_IF_ERROR(CheckIdsAndValuesSize(vector_view, value_name)); @@ -46,7 +46,7 @@ absl::Status CheckValues(const SparseVectorView& vector_view, } template ::value> > + typename = std::enable_if_t::value> > absl::Status CheckIdsAndValues(const SparseVectorView& vector_view, absl::string_view value_name = "values") { RETURN_IF_ERROR(CheckIdsNonnegativeAndStrictlyIncreasing(vector_view.ids())); @@ -55,7 +55,7 @@ absl::Status CheckIdsAndValues(const SparseVectorView& vector_view, } template ::value> > + typename = std::enable_if_t::value> > absl::Status CheckValues(const SparseVectorView& vector_view, const DoubleOptions& options, absl::string_view value_name = "values") { @@ -69,7 +69,7 @@ absl::Status CheckValues(const SparseVectorView& vector_view, } template ::value> > + typename = std::enable_if_t::value> > absl::Status CheckIdsAndValues(const SparseVectorView& vector_view, const DoubleOptions& options, absl::string_view value_name = "values") {