// Copyright 2010-2025 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "ortools/linear_solver/linear_solver.h" #if !defined(_MSC_VER) #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "absl/base/const_init.h" #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/flags/flag.h" #include "absl/log/check.h" #include "absl/log/log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/ascii.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/str_replace.h" #include "absl/strings/string_view.h" #include "absl/synchronization/mutex.h" #include "absl/synchronization/notification.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "google/protobuf/text_format.h" #include "ortools/base/accurate_sum.h" #include "ortools/base/map_util.h" #include "ortools/base/stl_util.h" #include "ortools/base/threadpool.h" #include "ortools/linear_solver/linear_expr.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/linear_solver_callback.h" #include "ortools/linear_solver/model_exporter.h" #include "ortools/linear_solver/model_validator.h" #include "ortools/port/file.h" #include "ortools/port/proto_utils.h" #include "ortools/util/fp_utils.h" #include "ortools/util/lazy_mutable_copy.h" #include "ortools/util/time_limit.h" ABSL_FLAG(bool, verify_solution, false, "Systematically verify the solution when calling Solve()" ", and change the return value of Solve() to ABNORMAL if" " an error was detected."); ABSL_FLAG(bool, log_verification_errors, true, "If --verify_solution is set: LOG(ERROR) all errors detected" " during the verification of the solution."); ABSL_FLAG(bool, linear_solver_enable_verbose_output, false, "If set, enables verbose output for the solver. Setting this flag" " is the same as calling MPSolver::EnableOutput()."); ABSL_FLAG(bool, mpsolver_bypass_model_validation, false, "If set, the user-provided Model won't be verified before Solve()." " Invalid models will typically trigger various error responses" " from the underlying solvers; sometimes crashes."); namespace operations_research { bool SolverTypeIsMip(MPModelRequest::SolverType solver_type) { switch (solver_type) { case MPModelRequest::PDLP_LINEAR_PROGRAMMING: case MPModelRequest::GLOP_LINEAR_PROGRAMMING: case MPModelRequest::CLP_LINEAR_PROGRAMMING: case MPModelRequest::GLPK_LINEAR_PROGRAMMING: case MPModelRequest::GUROBI_LINEAR_PROGRAMMING: case MPModelRequest::HIGHS_LINEAR_PROGRAMMING: case MPModelRequest::XPRESS_LINEAR_PROGRAMMING: case MPModelRequest::CPLEX_LINEAR_PROGRAMMING: return false; case MPModelRequest::SCIP_MIXED_INTEGER_PROGRAMMING: case MPModelRequest::GLPK_MIXED_INTEGER_PROGRAMMING: case MPModelRequest::CBC_MIXED_INTEGER_PROGRAMMING: case MPModelRequest::GUROBI_MIXED_INTEGER_PROGRAMMING: case MPModelRequest::KNAPSACK_MIXED_INTEGER_PROGRAMMING: case MPModelRequest::BOP_INTEGER_PROGRAMMING: case MPModelRequest::SAT_INTEGER_PROGRAMMING: case MPModelRequest::HIGHS_MIXED_INTEGER_PROGRAMMING: case MPModelRequest::XPRESS_MIXED_INTEGER_PROGRAMMING: case MPModelRequest::CPLEX_MIXED_INTEGER_PROGRAMMING: return true; } LOG(DFATAL) << "Invalid SolverType: " << solver_type; return false; } double MPConstraint::GetCoefficient(const MPVariable* const var) const { DLOG_IF(DFATAL, !interface_->solver_->OwnsVariable(var)) << var; if (var == nullptr) return 0.0; return gtl::FindWithDefault(coefficients_, var); } void MPConstraint::SetCoefficient(const MPVariable* const var, double coeff) { DLOG_IF(DFATAL, !interface_->solver_->OwnsVariable(var)) << var; if (var == nullptr) return; if (coeff == 0.0) { auto it = coefficients_.find(var); // If setting a coefficient to 0 when this coefficient did not // exist or was already 0, do nothing: skip // interface_->SetCoefficient() and do not store a coefficient in // the map. Note that if the coefficient being set to 0 did exist // and was not 0, we do have to keep a 0 in the coefficients_ map, // because the extraction of the constraint might rely on it, // depending on the underlying solver. if (it != coefficients_.end() && it->second != 0.0) { const double old_value = it->second; it->second = 0.0; interface_->SetCoefficient(this, var, 0.0, old_value); } return; } auto insertion_result = coefficients_.insert(std::make_pair(var, coeff)); const double old_value = insertion_result.second ? 0.0 : insertion_result.first->second; insertion_result.first->second = coeff; interface_->SetCoefficient(this, var, coeff, old_value); } void MPConstraint::Clear() { interface_->ClearConstraint(this); coefficients_.clear(); } void MPConstraint::SetBounds(double lb, double ub) { const bool change = lb != lb_ || ub != ub_; lb_ = lb; ub_ = ub; if (change && interface_->constraint_is_extracted(index_)) { interface_->SetConstraintBounds(index_, lb_, ub_); } } double MPConstraint::dual_value() const { if (!interface_->IsContinuous()) { LOG(DFATAL) << "Dual value only available for continuous problems"; return 0.0; } if (!interface_->CheckSolutionIsSynchronizedAndExists()) return 0.0; return dual_value_; } MPSolver::BasisStatus MPConstraint::basis_status() const { if (!interface_->IsContinuous()) { LOG(DFATAL) << "Basis status only available for continuous problems"; return MPSolver::FREE; } if (!interface_->CheckSolutionIsSynchronizedAndExists()) { return MPSolver::FREE; } // This is done lazily as this method is expected to be rarely used. return interface_->row_status(index_); } bool MPConstraint::ContainsNewVariables() { const int last_variable_index = interface_->last_variable_index(); for (const auto& entry : coefficients_) { const int variable_index = entry.first->index(); if (variable_index >= last_variable_index || !interface_->variable_is_extracted(variable_index)) { return true; } } return false; } // ----- MPObjective ----- double MPObjective::GetCoefficient(const MPVariable* const var) const { DLOG_IF(DFATAL, !interface_->solver_->OwnsVariable(var)) << var; if (var == nullptr) return 0.0; return gtl::FindWithDefault(coefficients_, var); } void MPObjective::SetCoefficient(const MPVariable* const var, double coeff) { DLOG_IF(DFATAL, !interface_->solver_->OwnsVariable(var)) << var; if (var == nullptr) return; if (coeff == 0.0) { auto it = coefficients_.find(var); // See the discussion on MPConstraint::SetCoefficient() for 0 coefficients, // the same reasoning applies here. if (it == coefficients_.end() || it->second == 0.0) return; it->second = 0.0; } else { coefficients_[var] = coeff; } interface_->SetObjectiveCoefficient(var, coeff); } void MPObjective::SetOffset(double value) { offset_ = value; interface_->SetObjectiveOffset(offset_); } namespace { void CheckLinearExpr(const MPSolver& solver, const LinearExpr& linear_expr) { for (auto var_value_pair : linear_expr.terms()) { CHECK(solver.OwnsVariable(var_value_pair.first)) << "Bad MPVariable* in LinearExpr, did you try adding an integer to an " "MPVariable* directly?"; } } } // namespace void MPObjective::OptimizeLinearExpr(const LinearExpr& linear_expr, bool is_maximization) { CheckLinearExpr(*interface_->solver_, linear_expr); interface_->ClearObjective(); coefficients_.clear(); SetOffset(linear_expr.offset()); for (const auto& kv : linear_expr.terms()) { SetCoefficient(kv.first, kv.second); } SetOptimizationDirection(is_maximization); } void MPObjective::AddLinearExpr(const LinearExpr& linear_expr) { CheckLinearExpr(*interface_->solver_, linear_expr); SetOffset(offset_ + linear_expr.offset()); for (const auto& kv : linear_expr.terms()) { SetCoefficient(kv.first, GetCoefficient(kv.first) + kv.second); } } void MPObjective::Clear() { interface_->ClearObjective(); coefficients_.clear(); offset_ = 0.0; SetMinimization(); } void MPObjective::SetOptimizationDirection(bool maximize) { // Note(user): The maximize_ bool would more naturally belong to the // MPObjective, but it actually has to be a member of MPSolverInterface, // because some implementations (such as GLPK) need that bool for the // MPSolverInterface constructor, i.e. at a time when the MPObjective is not // constructed yet (MPSolverInterface is always built before MPObjective // when a new MPSolver is constructed). interface_->maximize_ = maximize; interface_->SetOptimizationDirection(maximize); } bool MPObjective::maximization() const { return interface_->maximize_; } bool MPObjective::minimization() const { return !interface_->maximize_; } double MPObjective::Value() const { // Note(user): implementation-wise, the objective value belongs more // naturally to the MPSolverInterface, since all of its implementations write // to it directly. return interface_->objective_value(); } double MPObjective::BestBound() const { // Note(user): the best objective bound belongs to the interface for the // same reasons as the objective value does. return interface_->best_objective_bound(); } // ----- MPVariable ----- double MPVariable::solution_value() const { if (!interface_->CheckSolutionIsSynchronizedAndExists()) return 0.0; // If the underlying solver supports integer variables, and this is an integer // variable, we round the solution value (i.e., clients usually expect precise // integer values for integer variables). return (integer_ && interface_->IsMIP()) ? round(solution_value_) : solution_value_; } double MPVariable::unrounded_solution_value() const { if (!interface_->CheckSolutionIsSynchronizedAndExists()) return 0.0; return solution_value_; } double MPVariable::reduced_cost() const { if (!interface_->IsContinuous()) { LOG(DFATAL) << "Reduced cost only available for continuous problems"; return 0.0; } if (!interface_->CheckSolutionIsSynchronizedAndExists()) return 0.0; return reduced_cost_; } MPSolver::BasisStatus MPVariable::basis_status() const { if (!interface_->IsContinuous()) { LOG(DFATAL) << "Basis status only available for continuous problems"; return MPSolver::FREE; } if (!interface_->CheckSolutionIsSynchronizedAndExists()) { return MPSolver::FREE; } // This is done lazily as this method is expected to be rarely used. return interface_->column_status(index_); } void MPVariable::SetBounds(double lb, double ub) { const bool change = lb != lb_ || ub != ub_; lb_ = lb; ub_ = ub; if (change && interface_->variable_is_extracted(index_)) { interface_->SetVariableBounds(index_, lb_, ub_); } } void MPVariable::SetInteger(bool integer) { if (integer_ != integer) { integer_ = integer; if (interface_->variable_is_extracted(index_)) { interface_->SetVariableInteger(index_, integer); } } } void MPVariable::SetBranchingPriority(int priority) { if (priority == branching_priority_) return; branching_priority_ = priority; interface_->BranchingPriorityChangedForVariable(index_); } // ----- Interface shortcuts ----- bool MPSolver::IsMIP() const { return interface_->IsMIP(); } std::string MPSolver::SolverVersion() const { return interface_->SolverVersion(); } void* MPSolver::underlying_solver() { return interface_->underlying_solver(); } // ---- Solver-specific parameters ---- absl::Status MPSolver::SetNumThreads(int num_threads) { if (num_threads < 1) { return absl::InvalidArgumentError("num_threads must be a positive number."); } const absl::Status status = interface_->SetNumThreads(num_threads); if (status.ok()) { num_threads_ = num_threads; } return status; } bool MPSolver::SetSolverSpecificParametersAsString( const std::string& parameters) { solver_specific_parameter_string_ = parameters; return interface_->SetSolverSpecificParametersAsString(parameters); } // ----- Solver ----- namespace { MPSolverInterface* BuildSolverInterface(MPSolver* const solver) { DCHECK(solver != nullptr); MPSolverInterface* interface = MPSolverInterfaceFactoryRepository::GetInstance()->Create(solver); QCHECK(interface != nullptr) << "Unsupported problem type: '" << solver->ProblemType() << "'. Did you forget to link the library for this solver?"; return interface; } } // namespace namespace { int NumDigits(int n) { // Number of digits needed to write a non-negative integer in base 10. // Note(user): max(1, log(0) + 1) == max(1, -inf) == 1. #if defined(_MSC_VER) return static_cast(std::max(1.0L, log(1.0L * n) / log(10.0L) + 1.0)); #else return static_cast(std::max(1.0, log10(static_cast(n)) + 1.0)); #endif } } // namespace MPSolver::MPSolver(const std::string& name, OptimizationProblemType problem_type) : name_(name), problem_type_(problem_type), construction_time_(absl::Now()) { interface_.reset(BuildSolverInterface(this)); if (absl::GetFlag(FLAGS_linear_solver_enable_verbose_output)) { EnableOutput(); } objective_.reset(new MPObjective(interface_.get())); } MPSolver::~MPSolver() { Clear(); } // static bool MPSolver::SupportsProblemType(OptimizationProblemType problem_type) { return MPSolverInterfaceFactoryRepository::GetInstance()->Supports( problem_type); } // TODO(user): post c++ 14, instead use // std::pair // once pair gets a constexpr constructor. namespace { struct NamedOptimizationProblemType { MPSolver::OptimizationProblemType problem_type; absl::string_view name; }; } // namespace #if defined(_MSC_VER) const #else constexpr #endif NamedOptimizationProblemType kOptimizationProblemTypeNames[] = { {MPSolver::GLOP_LINEAR_PROGRAMMING, "glop"}, {MPSolver::CLP_LINEAR_PROGRAMMING, "clp"}, {MPSolver::GUROBI_LINEAR_PROGRAMMING, "gurobi_lp"}, {MPSolver::GLPK_LINEAR_PROGRAMMING, "glpk_lp"}, {MPSolver::HIGHS_LINEAR_PROGRAMMING, "highs_lp"}, {MPSolver::CPLEX_LINEAR_PROGRAMMING, "cplex_lp"}, {MPSolver::XPRESS_LINEAR_PROGRAMMING, "xpress_lp"}, {MPSolver::SCIP_MIXED_INTEGER_PROGRAMMING, "scip"}, {MPSolver::CBC_MIXED_INTEGER_PROGRAMMING, "cbc"}, {MPSolver::SAT_INTEGER_PROGRAMMING, "sat"}, {MPSolver::BOP_INTEGER_PROGRAMMING, "bop"}, {MPSolver::GUROBI_MIXED_INTEGER_PROGRAMMING, "gurobi"}, {MPSolver::GLPK_MIXED_INTEGER_PROGRAMMING, "glpk"}, {MPSolver::HIGHS_MIXED_INTEGER_PROGRAMMING, "highs"}, {MPSolver::PDLP_LINEAR_PROGRAMMING, "pdlp"}, {MPSolver::KNAPSACK_MIXED_INTEGER_PROGRAMMING, "knapsack"}, {MPSolver::CPLEX_MIXED_INTEGER_PROGRAMMING, "cplex"}, {MPSolver::XPRESS_MIXED_INTEGER_PROGRAMMING, "xpress"}, }; // static bool MPSolver::ParseSolverType(absl::string_view solver_id, MPSolver::OptimizationProblemType* type) { // Normalize the solver id. const std::string id = absl::StrReplaceAll(absl::AsciiStrToUpper(solver_id), {{"-", "_"}}); // Support the full enum name MPModelRequest::SolverType solver_type; if (MPModelRequest::SolverType_Parse(id, &solver_type)) { *type = static_cast(solver_type); return true; } // Names are stored in lower case. std::string lower_id = absl::AsciiStrToLower(id); // Remove any "_mip" suffix, since they are optional. if (absl::EndsWith(lower_id, "_mip")) { lower_id = lower_id.substr(0, lower_id.size() - 4); } // Rewrite CP-SAT into SAT. if (lower_id == "cp_sat") { lower_id = "sat"; } // Reverse lookup in the kOptimizationProblemTypeNames[] array. for (auto& named_solver : kOptimizationProblemTypeNames) { if (named_solver.name == lower_id) { *type = named_solver.problem_type; return true; } } return false; } absl::string_view ToString( MPSolver::OptimizationProblemType optimization_problem_type) { for (const auto& named_solver : kOptimizationProblemTypeNames) { if (named_solver.problem_type == optimization_problem_type) { return named_solver.name; } } LOG(FATAL) << "Unrecognized solver type: " << static_cast(optimization_problem_type); } bool AbslParseFlag(const absl::string_view text, MPSolver::OptimizationProblemType* solver_type, std::string* error) { DCHECK(solver_type != nullptr); DCHECK(error != nullptr); const bool result = MPSolver::ParseSolverType(text, solver_type); if (!result) { *error = absl::StrCat("Solver type: ", text, " does not exist."); } return result; } /* static */ MPSolver::OptimizationProblemType MPSolver::ParseSolverTypeOrDie( const std::string& solver_id) { MPSolver::OptimizationProblemType problem_type; CHECK(MPSolver::ParseSolverType(solver_id, &problem_type)) << solver_id; return problem_type; } /* static */ MPSolver* MPSolver::CreateSolver(const std::string& solver_id) { MPSolver::OptimizationProblemType problem_type; if (!MPSolver::ParseSolverType(solver_id, &problem_type)) { LOG(WARNING) << "Unrecognized solver type: " << solver_id; return nullptr; } if (!MPSolver::SupportsProblemType(problem_type)) { LOG(WARNING) << "Support for " << solver_id << " not linked in, or the license was not found."; return nullptr; } MPSolver* solver = new MPSolver("", problem_type); return solver; } MPVariable* MPSolver::LookupVariableOrNull(const std::string& var_name) const { if (!variable_name_to_index_) GenerateVariableNameIndex(); absl::flat_hash_map::const_iterator it = variable_name_to_index_->find(var_name); if (it == variable_name_to_index_->end()) return nullptr; return variables_[it->second]; } MPConstraint* MPSolver::LookupConstraintOrNull( const std::string& constraint_name) const { if (!constraint_name_to_index_) GenerateConstraintNameIndex(); const auto it = constraint_name_to_index_->find(constraint_name); if (it == constraint_name_to_index_->end()) return nullptr; return constraints_[it->second]; } // ----- Methods using protocol buffers ----- MPSolverResponseStatus MPSolver::LoadModelFromProto( const MPModelProto& input_model, std::string* error_message, bool clear_names) { Clear(); // The variable and constraint names are dropped, because we allow // duplicate names in the proto (they're not considered as 'ids'), // unlike the MPSolver C++ API which crashes if there are duplicate names. // Clearing the names makes the MPSolver generate unique names. return LoadModelFromProtoInternal( input_model, /*name_policy=*/ clear_names ? DEFAULT_CLEAR_NAMES : INVALID_MODEL_ON_DUPLICATE_NONEMPTY_NAMES, /*check_model_validity=*/true, error_message); } MPSolverResponseStatus MPSolver::LoadModelFromProtoWithUniqueNamesOrDie( const MPModelProto& input_model, std::string* error_message) { Clear(); // Force variable and constraint name indexing (which CHECKs name uniqueness). GenerateVariableNameIndex(); GenerateConstraintNameIndex(); return LoadModelFromProtoInternal( input_model, /*name_policy=*/DIE_ON_DUPLICATE_NONEMPTY_NAMES, /*check_model_validity=*/true, error_message); } namespace { // Iterates over all the variable names. See usage. class MPVariableNamesIterator { public: explicit MPVariableNamesIterator(const MPModelProto& model) : model_(model) {} int index() const { return index_; } absl::string_view name() const { return model_.variable(index_).name(); } static std::string DescribeIndex(int index) { return absl::StrFormat("variable[%d]", index); } void Advance() { ++index_; } bool AtEnd() const { return index_ == model_.variable_size(); } private: const MPModelProto& model_; int index_ = 0; }; // Iterates over all the constraint and general_constraint names. See usage. class MPConstraintNamesIterator { public: explicit MPConstraintNamesIterator(const MPModelProto& model) : model_(model), // To iterate both on the constraint[] and the general_constraint[] // field, we use the bit trick that i ≥ 0 corresponds to constraint[i], // and i < 0 corresponds to general_constraint[~i = -i-1]. index_(model_.constraint().empty() ? ~0 : 0) {} int index() const { return index_; } absl::string_view name() const { return index_ >= 0 ? model_.constraint(index_).name() // As of 2023-04, the only names of MPGeneralConstraints // that are actually ingested are the // MPIndicatorConstraint.constraint.name. : model_.general_constraint(~index_) .indicator_constraint() .constraint() .name(); } static std::string DescribeIndex(int index) { return index >= 0 ? absl::StrFormat("constraint[%d]", index) : absl::StrFormat( "general_constraint[%d].indicator_constraint.constraint", ~index); } void Advance() { if (index_ >= 0) { if (++index_ == model_.constraint_size()) index_ = ~0; } else { --index_; } } bool AtEnd() const { return ~index_ == model_.general_constraint_size(); } private: const MPModelProto& model_; int index_ = 0; }; // Looks at the `name` field of all the given MPModelProto fields, and if there // is a duplicate name, returns a descriptive error message. // If no duplicate is found, returns the empty string. template std::string FindDuplicateNamesError(NameIterator name_iterator) { absl::flat_hash_map name_to_index; for (; !name_iterator.AtEnd(); name_iterator.Advance()) { if (name_iterator.name().empty()) continue; const int index = name_to_index.insert({name_iterator.name(), name_iterator.index()}) .first->second; if (index != name_iterator.index()) { return absl::StrFormat( "Duplicate name '%s' in %s.name() and %s.name()", name_iterator.name(), NameIterator::DescribeIndex(index), NameIterator::DescribeIndex(name_iterator.index())); } } return ""; // No duplicate found. } } // namespace MPSolverResponseStatus MPSolver::LoadModelFromProtoInternal( const MPModelProto& input_model, ModelProtoNamesPolicy name_policy, bool check_model_validity, std::string* error_message) { CHECK(error_message != nullptr); std::string error; if (check_model_validity) { error = FindErrorInMPModelProto(input_model); } // We preemptively check for duplicate names even for the // DIE_ON_DUPLICATE_NONEMPTY_NAMES policy, because it yields more informative // error messages than if we wait for InsertIfNotPresent() to crash. if (error.empty() && name_policy != DEFAULT_CLEAR_NAMES) { error = FindDuplicateNamesError(MPVariableNamesIterator(input_model)); if (error.empty()) { error = FindDuplicateNamesError(MPConstraintNamesIterator(input_model)); } if (!error.empty() && name_policy == DIE_ON_DUPLICATE_NONEMPTY_NAMES) { LOG(FATAL) << error; } } const bool clear_names = name_policy == DEFAULT_CLEAR_NAMES; if (!error.empty()) { *error_message = error; LOG_IF(INFO, OutputIsEnabled()) << "Invalid model given to LoadModelFromProto(): " << error; if (absl::GetFlag(FLAGS_mpsolver_bypass_model_validation)) { LOG_IF(INFO, OutputIsEnabled()) << "Ignoring the model error(s) because of" << " --mpsolver_bypass_model_validation."; } else { return absl::StrContains(error, "Infeasible") ? MPSOLVER_INFEASIBLE : MPSOLVER_MODEL_INVALID; } } if (input_model.has_quadratic_objective()) { *error_message = "Optimizing a quadratic objective is only supported through direct " "proto solves. Please use MPSolver::SolveWithProto, or the solver's " "direct proto solve function."; return MPSOLVER_MODEL_INVALID; } MPObjective* const objective = MutableObjective(); // Passing empty names makes the MPSolver generate unique names. const std::string empty; for (int i = 0; i < input_model.variable_size(); ++i) { const MPVariableProto& var_proto = input_model.variable(i); MPVariable* variable = MakeNumVar(var_proto.lower_bound(), var_proto.upper_bound(), clear_names ? empty : var_proto.name()); variable->SetInteger(var_proto.is_integer()); if (var_proto.branching_priority() != 0) { variable->SetBranchingPriority(var_proto.branching_priority()); } objective->SetCoefficient(variable, var_proto.objective_coefficient()); } for (const MPConstraintProto& ct_proto : input_model.constraint()) { if (ct_proto.lower_bound() == -infinity() && ct_proto.upper_bound() == infinity()) { continue; } MPConstraint* const ct = MakeRowConstraint(ct_proto.lower_bound(), ct_proto.upper_bound(), clear_names ? empty : ct_proto.name()); ct->set_is_lazy(ct_proto.is_lazy()); for (int j = 0; j < ct_proto.var_index_size(); ++j) { ct->SetCoefficient(variables_[ct_proto.var_index(j)], ct_proto.coefficient(j)); } } for (const MPGeneralConstraintProto& general_constraint : input_model.general_constraint()) { switch (general_constraint.general_constraint_case()) { case MPGeneralConstraintProto::kIndicatorConstraint: { const auto& proto = general_constraint.indicator_constraint().constraint(); if (proto.lower_bound() == -infinity() && proto.upper_bound() == infinity()) { continue; } const int constraint_index = NumConstraints(); MPConstraint* const constraint = new MPConstraint( constraint_index, proto.lower_bound(), proto.upper_bound(), clear_names ? "" : proto.name(), interface_.get()); if (constraint_name_to_index_) { gtl::InsertOrDie(&*constraint_name_to_index_, constraint->name(), constraint_index); } constraints_.push_back(constraint); constraint_is_extracted_.push_back(false); constraint->set_is_lazy(proto.is_lazy()); for (int j = 0; j < proto.var_index_size(); ++j) { constraint->SetCoefficient(variables_[proto.var_index(j)], proto.coefficient(j)); } MPVariable* const variable = variables_[general_constraint.indicator_constraint().var_index()]; constraint->indicator_variable_ = variable; constraint->indicator_value_ = general_constraint.indicator_constraint().var_value(); if (!interface_->AddIndicatorConstraint(constraint)) { *error_message = "Solver doesn't support indicator constraints"; return MPSOLVER_MODEL_INVALID; } break; } default: *error_message = absl::StrFormat( "Optimizing general constraints of type %i is only supported " "through direct proto solves. Please use MPSolver::SolveWithProto, " "or the solver's direct proto solve function.", general_constraint.general_constraint_case()); return MPSOLVER_MODEL_INVALID; } } objective->SetOptimizationDirection(input_model.maximize()); if (input_model.has_objective_offset()) { objective->SetOffset(input_model.objective_offset()); } // Stores any hints about where to start the solve. solution_hint_.clear(); for (int i = 0; i < input_model.solution_hint().var_index_size(); ++i) { solution_hint_.push_back( std::make_pair(variables_[input_model.solution_hint().var_index(i)], input_model.solution_hint().var_value(i))); } return MPSOLVER_MODEL_IS_VALID; } namespace { MPSolverResponseStatus ResultStatusToMPSolverResponseStatus( MPSolver::ResultStatus status) { switch (status) { case MPSolver::OPTIMAL: return MPSOLVER_OPTIMAL; case MPSolver::FEASIBLE: return MPSOLVER_FEASIBLE; case MPSolver::INFEASIBLE: return MPSOLVER_INFEASIBLE; case MPSolver::UNBOUNDED: return MPSOLVER_UNBOUNDED; case MPSolver::ABNORMAL: return MPSOLVER_ABNORMAL; case MPSolver::MODEL_INVALID: return MPSOLVER_MODEL_INVALID; case MPSolver::NOT_SOLVED: return MPSOLVER_NOT_SOLVED; } return MPSOLVER_UNKNOWN_STATUS; } } // namespace void MPSolver::FillSolutionResponseProto(MPSolutionResponse* response) const { CHECK(response != nullptr); response->Clear(); response->set_status( ResultStatusToMPSolverResponseStatus(interface_->result_status_)); response->mutable_solve_info()->set_solve_wall_time_seconds(wall_time() / 1000.0); if (interface_->result_status_ == MPSolver::OPTIMAL || interface_->result_status_ == MPSolver::FEASIBLE) { response->set_objective_value(Objective().Value()); for (MPVariable* variable : variables_) { response->add_variable_value(variable->solution_value()); } if (interface_->IsMIP()) { response->set_best_objective_bound(interface_->best_objective_bound()); } else { // Dual values have no meaning in MIP. for (MPConstraint* constraint : constraints_) { response->add_dual_value(constraint->dual_value()); } // Reduced cost have no meaning in MIP. for (MPVariable* variable : variables_) { response->add_reduced_cost(variable->reduced_cost()); } } } } namespace { bool InCategory(int status, int category) { if (category == MPSOLVER_OPTIMAL) return status == MPSOLVER_OPTIMAL; while (status > category) status >>= 4; return status == category; } void AppendStatusStr(absl::string_view msg, MPSolutionResponse* response) { response->set_status_str( absl::StrCat(response->status_str(), (response->status_str().empty() ? "" : "\n"), msg)); } } // namespace // static void MPSolver::SolveWithProto(const MPModelRequest& model_request, MPSolutionResponse* response, std::atomic* interrupt) { return SolveLazyMutableRequest(model_request, response, interrupt); } // static void MPSolver::SolveLazyMutableRequest(LazyMutableCopy request, MPSolutionResponse* response, std::atomic* interrupt) { CHECK(response != nullptr); if (interrupt != nullptr && !SolverTypeSupportsInterruption(request->solver_type())) { response->set_status(MPSOLVER_INCOMPATIBLE_OPTIONS); response->set_status_str( "Called MPSolver::SolveWithProto with an underlying solver that " "doesn't support interruption."); return; } MPSolver solver( request->model().name(), static_cast(request->solver_type())); if (request->enable_internal_solver_output()) { solver.EnableOutput(); std::cout << "MPModelRequest info:\n" << GetMPModelRequestLoggingInfo(*request) << std::endl; } // If the solver supports it, we can std::move() the request since we will // return right after this in all cases. if (solver.interface_->SupportsDirectlySolveProto(interrupt)) { *response = solver.interface_->DirectlySolveProto(std::move(request), interrupt); return; } // Validate and extract model delta. Also deal with trivial problems. const std::optional> optional_model = GetMPModelOrPopulateResponse(request, response); if (!optional_model) return; std::string error_message; response->set_status(solver.LoadModelFromProtoInternal( **optional_model, /*name_policy=*/DEFAULT_CLEAR_NAMES, /*check_model_validity=*/false, &error_message)); // Even though we don't re-check model validity here, there can be some // problems found by LoadModelFromProto, eg. unsupported features. if (response->status() != MPSOLVER_MODEL_IS_VALID) { response->set_status_str(error_message); LOG_IF(WARNING, request->enable_internal_solver_output()) << "LoadModelFromProtoInternal() failed even though the model was " << "valid! Status: " << ProtoEnumToString(response->status()) << " (" << response->status() << "); Error: " << error_message; return; } if (request->has_solver_time_limit_seconds()) { solver.SetTimeLimit(absl::Seconds(request->solver_time_limit_seconds())); } std::string warning_message; if (request->has_solver_specific_parameters()) { if (!solver.SetSolverSpecificParametersAsString( request->solver_specific_parameters())) { if (request->ignore_solver_specific_parameters_failure()) { // We'll add a warning message in status_str after the solve. warning_message = "Warning: the solver specific parameters were not successfully " "applied"; } else { response->set_status(MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); return; } } } if (interrupt == nullptr) { // If we don't need interruption support, we can save some overhead by // running the solve in the current thread. solver.Solve(); solver.FillSolutionResponseProto(response); } else { const absl::Time start_time = absl::Now(); absl::Time interrupt_time; bool interrupted_by_user = false; { absl::Notification solve_finished; auto polling_func = [&interrupt, &solve_finished, &solver, &interrupted_by_user, &interrupt_time, &request]() { constexpr absl::Duration kPollDelay = absl::Microseconds(100); constexpr absl::Duration kMaxInterruptionDelay = absl::Seconds(10); while (!interrupt->load()) { if (solve_finished.HasBeenNotified()) return; absl::SleepFor(kPollDelay); } // If we get here, we received an interruption notification before the // solve finished "naturally". solver.InterruptSolve(); interrupt_time = absl::Now(); interrupted_by_user = true; // SUBTLE: our call to InterruptSolve() can be ignored by the // underlying solver for several reasons: // 1) The solver thread doesn't poll its 'interrupted' bit often // enough and takes too long to realize that it should return, or // its mere return + FillSolutionResponse() takes too long. // 2) The user interrupted the solve so early that Solve() hadn't // really started yet when we called InterruptSolve(). // In case 1), we should just wait a little longer. In case 2), we // should call InterruptSolve() again, maybe several times. To both // accommodate cases where the solver takes really a long time to // react to the interruption, while returning as quickly as possible, // we poll the solve_finished notification with increasing durations // and call InterruptSolve again, each time. for (absl::Duration poll_delay = kPollDelay; absl::Now() <= interrupt_time + kMaxInterruptionDelay; poll_delay *= 2) { if (solve_finished.WaitForNotificationWithTimeout(poll_delay)) { return; } else { solver.InterruptSolve(); } } LOG(DFATAL) << "MPSolver::InterruptSolve() seems to be ignored by the " "underlying solver, despite repeated calls over at least " << absl::FormatDuration(kMaxInterruptionDelay) << ". Solver type used: " << MPModelRequest_SolverType_Name(request->solver_type()); // Note that in opt builds, the polling thread terminates here with an // error message, but we let Solve() finish, ignoring the user // interruption request. }; // The choice to do polling rather than solving in the second thread is // not arbitrary, as we want to maintain any custom thread options set by // the user. They shouldn't matter for polling, but for solving we might // e.g. use a larger stack. ThreadPool thread_pool(/*num_threads=*/1); thread_pool.Schedule(polling_func); // Make sure the interruption notification didn't arrive while waiting to // be scheduled. if (!interrupt->load()) { solver.Solve(); solver.FillSolutionResponseProto(response); } else { // *interrupt == true response->set_status(MPSOLVER_CANCELLED_BY_USER); response->set_status_str( "Solve not started, because the user set the atomic in " "MPSolver::SolveWithProto() to true before solving could " "start."); } solve_finished.Notify(); // We block until the thread finishes when thread_pool goes out of scope. } if (interrupted_by_user) { // Despite the interruption, the solver might still have found a useful // result. If so, don't overwrite the status. if (InCategory(response->status(), MPSOLVER_NOT_SOLVED)) { response->set_status(MPSOLVER_CANCELLED_BY_USER); } AppendStatusStr( absl::StrFormat( "User interrupted MPSolver::SolveWithProto() by setting the " "atomic to true at %s (%s after solving started.)", absl::FormatTime(interrupt_time), absl::FormatDuration(interrupt_time - start_time)), response); } } if (!warning_message.empty()) { AppendStatusStr(warning_message, response); } } void MPSolver::ExportModelToProto(MPModelProto* output_model) const { DCHECK(output_model != nullptr); output_model->Clear(); // Name output_model->set_name(Name()); // Variables for (const MPVariable* var : variables_) { MPVariableProto* const variable_proto = output_model->add_variable(); // TODO(user): Add option to avoid filling the var name to avoid overly // large protocol buffers. variable_proto->set_name(var->name()); variable_proto->set_lower_bound(var->lb()); variable_proto->set_upper_bound(var->ub()); variable_proto->set_is_integer(var->integer()); if (objective_->GetCoefficient(var) != 0.0) { variable_proto->set_objective_coefficient( objective_->GetCoefficient(var)); } if (var->branching_priority() != 0) { variable_proto->set_branching_priority(var->branching_priority()); } } // Map the variables to their indices. This is needed to output the // variables in the order they were created, which in turn is needed to have // repeatable results with ExportModelAsLpFormat and ExportModelAsMpsFormat. // This step is needed as long as the variable indices are given by the // underlying solver at the time of model extraction. // TODO(user): remove this step. absl::flat_hash_map var_to_index; for (int j = 0; j < static_cast(variables_.size()); ++j) { var_to_index[variables_[j]] = j; } // Constraints for (MPConstraint* const constraint : constraints_) { MPConstraintProto* constraint_proto; if (constraint->indicator_variable() != nullptr) { MPGeneralConstraintProto* const general_constraint_proto = output_model->add_general_constraint(); general_constraint_proto->set_name(constraint->name()); MPIndicatorConstraint* const indicator_constraint_proto = general_constraint_proto->mutable_indicator_constraint(); indicator_constraint_proto->set_var_index( constraint->indicator_variable()->index()); indicator_constraint_proto->set_var_value(constraint->indicator_value()); constraint_proto = indicator_constraint_proto->mutable_constraint(); } else { constraint_proto = output_model->add_constraint(); } constraint_proto->set_name(constraint->name()); constraint_proto->set_lower_bound(constraint->lb()); constraint_proto->set_upper_bound(constraint->ub()); constraint_proto->set_is_lazy(constraint->is_lazy()); // Vector linear_term will contain pairs (variable index, coeff), that will // be sorted by variable index. std::vector> linear_term; for (const auto& entry : constraint->coefficients_) { const MPVariable* const var = entry.first; const int var_index = gtl::FindWithDefault(var_to_index, var, -1); DCHECK_NE(-1, var_index); const double coeff = entry.second; linear_term.push_back(std::pair(var_index, coeff)); } // The cost of sort is expected to be low as constraints usually have very // few terms. std::sort(linear_term.begin(), linear_term.end()); // Now use linear term. for (const std::pair& var_and_coeff : linear_term) { constraint_proto->add_var_index(var_and_coeff.first); constraint_proto->add_coefficient(var_and_coeff.second); } } output_model->set_maximize(Objective().maximization()); output_model->set_objective_offset(Objective().offset()); if (!solution_hint_.empty()) { PartialVariableAssignment* const hint = output_model->mutable_solution_hint(); for (const auto& var_value_pair : solution_hint_) { hint->add_var_index(var_value_pair.first->index()); hint->add_var_value(var_value_pair.second); } } } absl::Status MPSolver::LoadSolutionFromProto(const MPSolutionResponse& response, double tolerance) { interface_->result_status_ = static_cast(response.status()); if (response.status() != MPSOLVER_OPTIMAL && response.status() != MPSOLVER_FEASIBLE) { return absl::InvalidArgumentError(absl::StrCat( "Cannot load a solution unless its status is OPTIMAL or FEASIBLE" " (status was: ", ProtoEnumToString(response.status()), ")")); } // Before touching the variables, verify that the solution looks legit: // each variable of the MPSolver must have its value listed exactly once, and // each listed solution should correspond to a known variable. if (static_cast(response.variable_value_size()) != variables_.size()) { return absl::InvalidArgumentError(absl::StrCat( "Trying to load a solution whose number of variables (", response.variable_value_size(), ") does not correspond to the Solver's (", variables_.size(), ")")); } interface_->ExtractModel(); if (tolerance != infinity()) { // Look further: verify that the variable values are within the bounds. double largest_error = 0; int num_vars_out_of_bounds = 0; int last_offending_var = -1; for (int i = 0; i < response.variable_value_size(); ++i) { const double var_value = response.variable_value(i); MPVariable* var = variables_[i]; // TODO(user): Use parameter when they become available in this class. const double lb_error = var->lb() - var_value; const double ub_error = var_value - var->ub(); if (lb_error > tolerance || ub_error > tolerance) { ++num_vars_out_of_bounds; largest_error = std::max(largest_error, std::max(lb_error, ub_error)); last_offending_var = i; } } if (num_vars_out_of_bounds > 0) { return absl::InvalidArgumentError(absl::StrCat( "Loaded a solution whose variables matched the solver's, but ", num_vars_out_of_bounds, " of ", variables_.size(), " variables were out of their bounds, by more than the primal" " tolerance which is: ", tolerance, ". Max error: ", largest_error, ", last offender var is #", last_offending_var, ": '", variables_[last_offending_var]->name(), "'")); } } for (int i = 0; i < response.variable_value_size(); ++i) { variables_[i]->set_solution_value(response.variable_value(i)); } if (response.dual_value_size() > 0) { if (static_cast(response.dual_value_size()) != constraints_.size()) { return absl::InvalidArgumentError(absl::StrCat( "Trying to load a dual solution whose number of entries (", response.dual_value_size(), ") does not correspond to the Solver's (", constraints_.size(), ")")); } for (int i = 0; i < response.dual_value_size(); ++i) { constraints_[i]->set_dual_value(response.dual_value(i)); } } if (response.reduced_cost_size() > 0) { if (static_cast(response.reduced_cost_size()) != variables_.size()) { return absl::InvalidArgumentError(absl::StrCat( "Trying to load a reduced cost solution whose number of entries (", response.reduced_cost_size(), ") does not correspond to the Solver's (", variables_.size(), ")")); } for (int i = 0; i < response.reduced_cost_size(); ++i) { variables_[i]->set_reduced_cost(response.reduced_cost(i)); } } // Set the objective value, if is known. // NOTE(user): We do not verify the objective, even though we could! if (response.has_objective_value()) { interface_->objective_value_ = response.objective_value(); } if (response.has_best_objective_bound()) { interface_->best_objective_bound_ = response.best_objective_bound(); } // Mark the status as SOLUTION_SYNCHRONIZED, so that users may inspect the // solution normally. interface_->sync_status_ = MPSolverInterface::SOLUTION_SYNCHRONIZED; return absl::OkStatus(); } void MPSolver::Clear() { { absl::MutexLock lock(global_count_mutex_); global_num_variables_ += variables_.size(); global_num_constraints_ += constraints_.size(); } MutableObjective()->Clear(); gtl::STLDeleteElements(&variables_); gtl::STLDeleteElements(&constraints_); if (variable_name_to_index_) { variable_name_to_index_->clear(); } variable_is_extracted_.clear(); if (constraint_name_to_index_) { constraint_name_to_index_->clear(); } constraint_is_extracted_.clear(); interface_->Reset(); solution_hint_.clear(); } void MPSolver::Reset() { interface_->Reset(); } bool MPSolver::InterruptSolve() { return interface_->InterruptSolve(); } void MPSolver::SetStartingLpBasis( const std::vector& variable_statuses, const std::vector& constraint_statuses) { interface_->SetStartingLpBasis(variable_statuses, constraint_statuses); } double MPSolver::solver_infinity() { return interface_->infinity(); } MPVariable* MPSolver::MakeVar(double lb, double ub, bool integer, const std::string& name) { const int var_index = NumVariables(); MPVariable* v = new MPVariable(var_index, lb, ub, integer, name, interface_.get()); if (variable_name_to_index_) { gtl::InsertOrDie(&*variable_name_to_index_, v->name(), var_index); } variables_.push_back(v); variable_is_extracted_.push_back(false); interface_->AddVariable(v); return v; } MPVariable* MPSolver::MakeNumVar(double lb, double ub, const std::string& name) { return MakeVar(lb, ub, false, name); } MPVariable* MPSolver::MakeIntVar(double lb, double ub, const std::string& name) { return MakeVar(lb, ub, true, name); } MPVariable* MPSolver::MakeBoolVar(const std::string& name) { return MakeVar(0.0, 1.0, true, name); } void MPSolver::MakeVarArray(int nb, double lb, double ub, bool integer, const std::string& name, std::vector* vars) { DCHECK_GE(nb, 0); if (nb <= 0) return; const int num_digits = NumDigits(nb); for (int i = 0; i < nb; ++i) { if (name.empty()) { vars->push_back(MakeVar(lb, ub, integer, name)); } else { std::string vname = absl::StrFormat("%s%0*d", name.c_str(), num_digits, i); vars->push_back(MakeVar(lb, ub, integer, vname)); } } } void MPSolver::MakeNumVarArray(int nb, double lb, double ub, const std::string& name, std::vector* vars) { MakeVarArray(nb, lb, ub, false, name, vars); } void MPSolver::MakeIntVarArray(int nb, double lb, double ub, const std::string& name, std::vector* vars) { MakeVarArray(nb, lb, ub, true, name, vars); } void MPSolver::MakeBoolVarArray(int nb, const std::string& name, std::vector* vars) { MakeVarArray(nb, 0.0, 1.0, true, name, vars); } MPConstraint* MPSolver::MakeRowConstraint(double lb, double ub) { return MakeRowConstraint(lb, ub, ""); } MPConstraint* MPSolver::MakeRowConstraint() { return MakeRowConstraint(-infinity(), infinity(), ""); } MPConstraint* MPSolver::MakeRowConstraint(double lb, double ub, const std::string& name) { const int constraint_index = NumConstraints(); MPConstraint* const constraint = new MPConstraint(constraint_index, lb, ub, name, interface_.get()); if (constraint_name_to_index_) { gtl::InsertOrDie(&*constraint_name_to_index_, constraint->name(), constraint_index); } constraints_.push_back(constraint); constraint_is_extracted_.push_back(false); interface_->AddRowConstraint(constraint); return constraint; } MPConstraint* MPSolver::MakeRowConstraint(const std::string& name) { return MakeRowConstraint(-infinity(), infinity(), name); } MPConstraint* MPSolver::MakeRowConstraint(const LinearRange& range) { return MakeRowConstraint(range, ""); } MPConstraint* MPSolver::MakeRowConstraint(const LinearRange& range, const std::string& name) { CheckLinearExpr(*this, range.linear_expr()); MPConstraint* constraint = MakeRowConstraint(range.lower_bound(), range.upper_bound(), name); for (const auto& kv : range.linear_expr().terms()) { constraint->SetCoefficient(kv.first, kv.second); } return constraint; } int MPSolver::ComputeMaxConstraintSize(int min_constraint_index, int max_constraint_index) const { int max_constraint_size = 0; DCHECK_GE(min_constraint_index, 0); DCHECK_LE(max_constraint_index, constraints_.size()); for (int i = min_constraint_index; i < max_constraint_index; ++i) { MPConstraint* const ct = constraints_[i]; if (static_cast(ct->coefficients_.size()) > max_constraint_size) { max_constraint_size = ct->coefficients_.size(); } } return max_constraint_size; } bool MPSolver::HasInfeasibleConstraints() const { bool hasInfeasibleConstraints = false; for (int i = 0; i < static_cast(constraints_.size()); ++i) { if (constraints_[i]->lb() > constraints_[i]->ub()) { LOG(WARNING) << "Constraint " << constraints_[i]->name() << " (" << i << ") has contradictory bounds:" << " lower bound = " << constraints_[i]->lb() << " upper bound = " << constraints_[i]->ub(); hasInfeasibleConstraints = true; } } return hasInfeasibleConstraints; } bool MPSolver::HasIntegerVariables() const { for (const MPVariable* const variable : variables_) { if (variable->integer()) return true; } return false; } MPSolver::ResultStatus MPSolver::Solve() { MPSolverParameters default_param; return Solve(default_param); } MPSolver::ResultStatus MPSolver::Solve(const MPSolverParameters& param) { // Special case for infeasible constraints so that all solvers have // the same behavior. // TODO(user): replace this by model extraction to proto + proto validation // (the proto has very low overhead compared to the wrapper, both in // performance and memory, so it's ok). if (HasInfeasibleConstraints()) { interface_->result_status_ = MPSolver::INFEASIBLE; return interface_->result_status_; } MPSolver::ResultStatus status = interface_->Solve(param); if (absl::GetFlag(FLAGS_verify_solution)) { if (status != MPSolver::OPTIMAL && status != MPSolver::FEASIBLE) { VLOG(1) << "--verify_solution enabled, but the solver did not find a" << " solution: skipping the verification."; } else if (!VerifySolution( param.GetDoubleParam(MPSolverParameters::PRIMAL_TOLERANCE), absl::GetFlag(FLAGS_log_verification_errors))) { status = MPSolver::ABNORMAL; interface_->result_status_ = status; } } DCHECK_EQ(interface_->result_status_, status); return status; } void MPSolver::Write(const std::string& file_name) { interface_->Write(file_name); } namespace { std::string PrettyPrintVar(const MPVariable& var) { const std::string prefix = "Variable '" + var.name() + "': domain = "; if (var.lb() >= MPSolver::infinity() || var.ub() <= -MPSolver::infinity() || var.lb() > var.ub()) { return prefix + "∅"; // Empty set. } // Special case: integer variable with at most two possible values // (and potentially none). if (var.integer() && var.ub() - var.lb() <= 1) { const int64_t lb = static_cast(ceil(var.lb())); const int64_t ub = static_cast(floor(var.ub())); if (lb > ub) { return prefix + "∅"; } else if (lb == ub) { return absl::StrFormat("%s{ %d }", prefix.c_str(), lb); } else { return absl::StrFormat("%s{ %d, %d }", prefix.c_str(), lb, ub); } } // Special case: single (non-infinite) real value. if (var.lb() == var.ub()) { return absl::StrFormat("%s{ %f }", prefix.c_str(), var.lb()); } return prefix + (var.integer() ? "Integer" : "Real") + " in " + (var.lb() <= -MPSolver::infinity() ? std::string("]-∞") : absl::StrFormat("[%f", var.lb())) + ", " + (var.ub() >= MPSolver::infinity() ? std::string("+∞[") : absl::StrFormat("%f]", var.ub())); } std::string PrettyPrintConstraint(const MPConstraint& constraint) { std::string prefix = "Constraint '" + constraint.name() + "': "; if (constraint.lb() >= MPSolver::infinity() || constraint.ub() <= -MPSolver::infinity() || constraint.lb() > constraint.ub()) { return prefix + "ALWAYS FALSE"; } if (constraint.lb() <= -MPSolver::infinity() && constraint.ub() >= MPSolver::infinity()) { return prefix + "ALWAYS TRUE"; } prefix += ""; // Equality. if (constraint.lb() == constraint.ub()) { return absl::StrFormat("%s = %f", prefix.c_str(), constraint.lb()); } // Inequalities. if (constraint.lb() <= -MPSolver::infinity()) { return absl::StrFormat("%s ≤ %f", prefix.c_str(), constraint.ub()); } if (constraint.ub() >= MPSolver::infinity()) { return absl::StrFormat("%s ≥ %f", prefix.c_str(), constraint.lb()); } return absl::StrFormat("%s ∈ [%f, %f]", prefix.c_str(), constraint.lb(), constraint.ub()); } } // namespace absl::Status MPSolver::ClampSolutionWithinBounds() { interface_->ExtractModel(); for (MPVariable* const variable : variables_) { const double value = variable->solution_value(); if (std::isnan(value)) { return absl::InvalidArgumentError( absl::StrCat("NaN value for ", PrettyPrintVar(*variable))); } if (value < variable->lb()) { variable->set_solution_value(variable->lb()); } else if (value > variable->ub()) { variable->set_solution_value(variable->ub()); } } interface_->sync_status_ = MPSolverInterface::SOLUTION_SYNCHRONIZED; return absl::OkStatus(); } std::vector MPSolver::ComputeConstraintActivities() const { // TODO(user): test this failure case. if (!interface_->CheckSolutionIsSynchronizedAndExists()) return {}; std::vector activities(constraints_.size(), 0.0); for (int i = 0; i < static_cast(constraints_.size()); ++i) { const MPConstraint& constraint = *constraints_[i]; AccurateSum sum; for (const auto& entry : constraint.coefficients_) { sum.Add(entry.first->solution_value() * entry.second); } activities[i] = sum.Value(); } return activities; } // TODO(user): split. bool MPSolver::VerifySolution(double tolerance, bool log_errors) const { double max_observed_error = 0; if (tolerance < 0) tolerance = infinity(); int num_errors = 0; // Verify variables. for (MPVariable* variable : variables_) { const MPVariable& var = *variable; const double value = var.solution_value(); // Check for NaN. if (std::isnan(value)) { ++num_errors; max_observed_error = infinity(); LOG_IF(ERROR, log_errors) << "NaN value for " << PrettyPrintVar(var); continue; } // Check lower bound. if (var.lb() != -infinity()) { if (value < var.lb() - tolerance) { ++num_errors; max_observed_error = std::max(max_observed_error, var.lb() - value); LOG_IF(ERROR, log_errors) << "Value " << value << " too low for " << PrettyPrintVar(var); } } // Check upper bound. if (var.ub() != infinity()) { if (value > var.ub() + tolerance) { ++num_errors; max_observed_error = std::max(max_observed_error, value - var.ub()); LOG_IF(ERROR, log_errors) << "Value " << value << " too high for " << PrettyPrintVar(var); } } // Check integrality. if (IsMIP() && var.integer()) { if (fabs(value - round(value)) > tolerance) { ++num_errors; max_observed_error = std::max(max_observed_error, fabs(value - round(value))); LOG_IF(ERROR, log_errors) << "Non-integer value " << value << " for " << PrettyPrintVar(var); } } } if (!IsMIP() && HasIntegerVariables()) { LOG_IF(INFO, log_errors) << "Skipped variable integrality check, because " << "a continuous relaxation of the model was " << "solved (i.e., the selected solver does not " << "support integer variables)."; } // Verify constraints. const std::vector activities = ComputeConstraintActivities(); for (int i = 0; i < static_cast(constraints_.size()); ++i) { const MPConstraint& constraint = *constraints_[i]; const double activity = activities[i]; // Re-compute the activity with a inaccurate summing algorithm. double inaccurate_activity = 0.0; for (const auto& entry : constraint.coefficients_) { inaccurate_activity += entry.first->solution_value() * entry.second; } // Catch NaNs. if (std::isnan(activity) || std::isnan(inaccurate_activity)) { ++num_errors; max_observed_error = infinity(); LOG_IF(ERROR, log_errors) << "NaN value for " << PrettyPrintConstraint(constraint); continue; } // Check bounds. if (constraint.indicator_variable() == nullptr || std::round(constraint.indicator_variable()->solution_value()) == constraint.indicator_value()) { if (constraint.lb() != -infinity()) { if (activity < constraint.lb() - tolerance) { ++num_errors; max_observed_error = std::max(max_observed_error, constraint.lb() - activity); LOG_IF(ERROR, log_errors) << "Activity " << activity << " too low for " << PrettyPrintConstraint(constraint); } else if (inaccurate_activity < constraint.lb() - tolerance) { LOG_IF(WARNING, log_errors) << "Activity " << activity << ", computed with the (inaccurate)" << " standard sum of its terms, is too low for " << PrettyPrintConstraint(constraint); } } if (constraint.ub() != infinity()) { if (activity > constraint.ub() + tolerance) { ++num_errors; max_observed_error = std::max(max_observed_error, activity - constraint.ub()); LOG_IF(ERROR, log_errors) << "Activity " << activity << " too high for " << PrettyPrintConstraint(constraint); } else if (inaccurate_activity > constraint.ub() + tolerance) { LOG_IF(WARNING, log_errors) << "Activity " << activity << ", computed with the (inaccurate)" << " standard sum of its terms, is too high for " << PrettyPrintConstraint(constraint); } } } } // Verify that the objective value wasn't reported incorrectly. const MPObjective& objective = Objective(); AccurateSum objective_sum; objective_sum.Add(objective.offset()); double inaccurate_objective_value = objective.offset(); for (const auto& entry : objective.coefficients_) { const double term = entry.first->solution_value() * entry.second; objective_sum.Add(term); inaccurate_objective_value += term; } const double actual_objective_value = objective_sum.Value(); if (!AreWithinAbsoluteOrRelativeTolerances( objective.Value(), actual_objective_value, tolerance, tolerance)) { ++num_errors; max_observed_error = std::max( max_observed_error, fabs(actual_objective_value - objective.Value())); LOG_IF(ERROR, log_errors) << "Objective value " << objective.Value() << " isn't accurate" << ", it should be " << actual_objective_value << " (delta=" << actual_objective_value - objective.Value() << ")."; } else if (!AreWithinAbsoluteOrRelativeTolerances(objective.Value(), inaccurate_objective_value, tolerance, tolerance)) { LOG_IF(WARNING, log_errors) << "Objective value " << objective.Value() << " doesn't correspond" << " to the value computed with the standard (and therefore inaccurate)" << " sum of its terms."; } if (num_errors > 0) { LOG_IF(ERROR, log_errors) << "There were " << num_errors << " errors above the tolerance (" << tolerance << "), the largest was " << max_observed_error; return false; } return true; } bool MPSolver::OutputIsEnabled() const { return !interface_->quiet(); } void MPSolver::EnableOutput() { interface_->set_quiet(false); } void MPSolver::SuppressOutput() { interface_->set_quiet(true); } int64_t MPSolver::iterations() const { return interface_->iterations(); } int64_t MPSolver::nodes() const { return interface_->nodes(); } double MPSolver::ComputeExactConditionNumber() const { return interface_->ComputeExactConditionNumber(); } bool MPSolver::OwnsVariable(const MPVariable* var) const { if (var == nullptr) return false; if (var->index() >= 0 && var->index() < static_cast(variables_.size())) { // Then, verify that the variable with this index has the same address. return variables_[var->index()] == var; } return false; } bool MPSolver::ExportModelAsLpFormat(bool obfuscate, std::string* model_str) const { MPModelProto proto; ExportModelToProto(&proto); MPModelExportOptions options; options.obfuscate = obfuscate; const auto status_or = operations_research::ExportModelAsLpFormat(proto, options); *model_str = status_or.value_or(""); return status_or.ok(); } bool MPSolver::ExportModelAsMpsFormat(bool fixed_format, bool obfuscate, std::string* model_str) const { MPModelProto proto; ExportModelToProto(&proto); MPModelExportOptions options; options.obfuscate = obfuscate; const auto status_or = operations_research::ExportModelAsMpsFormat(proto, options); *model_str = status_or.value_or(""); return status_or.ok(); } void MPSolver::SetHint(std::vector> hint) { for (const auto& var_value_pair : hint) { CHECK(OwnsVariable(var_value_pair.first)) << "hint variable does not belong to this solver"; } solution_hint_ = std::move(hint); } void MPSolver::GenerateVariableNameIndex() const { if (variable_name_to_index_) return; variable_name_to_index_ = absl::flat_hash_map(); for (const MPVariable* const var : variables_) { gtl::InsertOrDie(&*variable_name_to_index_, var->name(), var->index()); } } void MPSolver::GenerateConstraintNameIndex() const { if (constraint_name_to_index_) return; constraint_name_to_index_ = absl::flat_hash_map(); for (const MPConstraint* const cst : constraints_) { gtl::InsertOrDie(&*constraint_name_to_index_, cst->name(), cst->index()); } } bool MPSolver::NextSolution() { return interface_->NextSolution(); } void MPSolver::SetCallback(MPCallback* mp_callback) { interface_->SetCallback(mp_callback); } bool MPSolver::SupportsCallbacks() const { return interface_->SupportsCallbacks(); } // Global counters. absl::Mutex MPSolver::global_count_mutex_(absl::kConstInit); int64_t MPSolver::global_num_variables_ = 0; int64_t MPSolver::global_num_constraints_ = 0; // static int64_t MPSolver::global_num_variables() { absl::MutexLock lock(global_count_mutex_); return global_num_variables_; } // static int64_t MPSolver::global_num_constraints() { absl::MutexLock lock(global_count_mutex_); return global_num_constraints_; } bool MPSolverResponseStatusIsRpcError(MPSolverResponseStatus status) { switch (status) { // Cases that don't yield an RPC error when they happen on the server. case MPSOLVER_OPTIMAL: case MPSOLVER_FEASIBLE: case MPSOLVER_INFEASIBLE: case MPSOLVER_NOT_SOLVED: case MPSOLVER_UNBOUNDED: case MPSOLVER_ABNORMAL: case MPSOLVER_UNKNOWN_STATUS: return false; // Cases that should never happen with the linear solver server. We prefer // to consider those as "not RPC errors". case MPSOLVER_MODEL_IS_VALID: case MPSOLVER_CANCELLED_BY_USER: return false; // Cases that yield an RPC error when they happen on the server. case MPSOLVER_MODEL_INVALID: case MPSOLVER_MODEL_INVALID_SOLUTION_HINT: case MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS: case MPSOLVER_SOLVER_TYPE_UNAVAILABLE: case MPSOLVER_INCOMPATIBLE_OPTIONS: return true; } LOG(DFATAL) << "MPSolverResponseStatusIsRpcError() called with invalid status " << "(value: " << status << ")"; return false; } // ---------- MPSolverInterface ---------- const int MPSolverInterface::kDummyVariableIndex = 0; // TODO(user): Initialize objective value and bound to +/- inf (depending on // optimization direction). MPSolverInterface::MPSolverInterface(MPSolver* const solver) : solver_(solver), sync_status_(MODEL_SYNCHRONIZED), result_status_(MPSolver::NOT_SOLVED), maximize_(false), last_constraint_index_(0), last_variable_index_(0), objective_value_(0.0), best_objective_bound_(0.0), quiet_(true) {} MPSolverInterface::~MPSolverInterface() {} void MPSolverInterface::Write(const std::string& filename) { LOG(WARNING) << "Writing model not implemented in this solver interface."; } void MPSolverInterface::ExtractModel() { switch (sync_status_) { case MUST_RELOAD: { ExtractNewVariables(); ExtractNewConstraints(); ExtractObjective(); last_constraint_index_ = solver_->constraints_.size(); last_variable_index_ = solver_->variables_.size(); sync_status_ = MODEL_SYNCHRONIZED; break; } case MODEL_SYNCHRONIZED: { // Everything has already been extracted. DCHECK_EQ(last_constraint_index_, solver_->constraints_.size()); DCHECK_EQ(last_variable_index_, solver_->variables_.size()); break; } case SOLUTION_SYNCHRONIZED: { // Nothing has changed since last solve. DCHECK_EQ(last_constraint_index_, solver_->constraints_.size()); DCHECK_EQ(last_variable_index_, solver_->variables_.size()); break; } } } // TODO(user): remove this method. void MPSolverInterface::ResetExtractionInformation() { sync_status_ = MUST_RELOAD; last_constraint_index_ = 0; last_variable_index_ = 0; solver_->variable_is_extracted_.assign(solver_->variables_.size(), false); solver_->constraint_is_extracted_.assign(solver_->constraints_.size(), false); } bool MPSolverInterface::CheckSolutionIsSynchronized() const { if (sync_status_ != SOLUTION_SYNCHRONIZED) { LOG(DFATAL) << "The model has been changed since the solution was last computed." << " MPSolverInterface::sync_status_ = " << sync_status_; return false; } return true; } // Default version that can be overwritten by a solver-specific // version to accommodate for the quirks of each solver. bool MPSolverInterface::CheckSolutionExists() const { if (result_status_ != MPSolver::OPTIMAL && result_status_ != MPSolver::FEASIBLE) { LOG(DFATAL) << "No solution exists. MPSolverInterface::result_status_ = " << result_status_; return false; } return true; } double MPSolverInterface::objective_value() const { if (!CheckSolutionIsSynchronizedAndExists()) return 0; return objective_value_; } double MPSolverInterface::best_objective_bound() const { const double trivial_worst_bound = maximize_ ? -std::numeric_limits::infinity() : std::numeric_limits::infinity(); if (!IsMIP()) { VLOG(1) << "Best objective bound only available for discrete problems."; return trivial_worst_bound; } if (!CheckSolutionIsSynchronized()) { return trivial_worst_bound; } // Special case for empty model. if (solver_->variables_.empty() && solver_->constraints_.empty()) { return solver_->Objective().offset(); } return best_objective_bound_; } void MPSolverInterface::InvalidateSolutionSynchronization() { if (sync_status_ == SOLUTION_SYNCHRONIZED) { sync_status_ = MODEL_SYNCHRONIZED; } } double MPSolverInterface::ComputeExactConditionNumber() const { // Override this method in interfaces that actually support it. LOG(DFATAL) << "ComputeExactConditionNumber not implemented for " << ProtoEnumToString( static_cast( solver_->ProblemType())); return 0.0; } void MPSolverInterface::SetCommonParameters(const MPSolverParameters& param) { // TODO(user): Overhaul the code that sets parameters to enable changing // GLOP parameters without issuing warnings. // By default, we let GLOP keep its own default tolerance, much more accurate // than for the rest of the solvers. // if (solver_->ProblemType() != MPSolver::GLOP_LINEAR_PROGRAMMING) { SetPrimalTolerance( param.GetDoubleParam(MPSolverParameters::PRIMAL_TOLERANCE)); SetDualTolerance(param.GetDoubleParam(MPSolverParameters::DUAL_TOLERANCE)); } SetPresolveMode(param.GetIntegerParam(MPSolverParameters::PRESOLVE)); // TODO(user): In the future, we could distinguish between the // algorithm to solve the root LP and the algorithm to solve node // LPs. Not sure if underlying solvers support it. int value = param.GetIntegerParam(MPSolverParameters::LP_ALGORITHM); if (value != MPSolverParameters::kDefaultIntegerParamValue) { SetLpAlgorithm(value); } } void MPSolverInterface::SetMIPParameters(const MPSolverParameters& param) { if (solver_->ProblemType() != MPSolver::GLOP_LINEAR_PROGRAMMING) { SetRelativeMipGap( param.GetDoubleParam(MPSolverParameters::RELATIVE_MIP_GAP)); } } void MPSolverInterface::SetUnsupportedDoubleParam( MPSolverParameters::DoubleParam param) { LOG(WARNING) << "Trying to set an unsupported parameter: " << param << "."; } void MPSolverInterface::SetUnsupportedIntegerParam( MPSolverParameters::IntegerParam param) { LOG(WARNING) << "Trying to set an unsupported parameter: " << param << "."; } void MPSolverInterface::SetDoubleParamToUnsupportedValue( MPSolverParameters::DoubleParam param, double value) { LOG(WARNING) << "Trying to set a supported parameter: " << param << " to an unsupported value: " << value; } void MPSolverInterface::SetIntegerParamToUnsupportedValue( MPSolverParameters::IntegerParam param, int value) { LOG(WARNING) << "Trying to set a supported parameter: " << param << " to an unsupported value: " << value; } absl::Status MPSolverInterface::SetNumThreads(int num_threads) { return absl::UnimplementedError( absl::StrFormat("SetNumThreads() not supported by %s.", SolverVersion())); } bool MPSolverInterface::SetSolverSpecificParametersAsString( const std::string& parameters) { if (parameters.empty()) { return true; } LOG(WARNING) << "SetSolverSpecificParametersAsString() not supported by " << SolverVersion(); return false; } // ---------- MPSolverParameters ---------- const double MPSolverParameters::kDefaultRelativeMipGap = 1e-4; // For the primal and dual tolerances, choose the same default as CLP and GLPK. const double MPSolverParameters::kDefaultPrimalTolerance = operations_research::kDefaultPrimalTolerance; const double MPSolverParameters::kDefaultDualTolerance = 1e-7; const MPSolverParameters::PresolveValues MPSolverParameters::kDefaultPresolve = MPSolverParameters::PRESOLVE_ON; const MPSolverParameters::IncrementalityValues MPSolverParameters::kDefaultIncrementality = MPSolverParameters::INCREMENTALITY_ON; const double MPSolverParameters::kDefaultDoubleParamValue = -1.0; const int MPSolverParameters::kDefaultIntegerParamValue = -1; const double MPSolverParameters::kUnknownDoubleParamValue = -2.0; const int MPSolverParameters::kUnknownIntegerParamValue = -2; // The constructor sets all parameters to their default value. MPSolverParameters::MPSolverParameters() : relative_mip_gap_value_(kDefaultRelativeMipGap), primal_tolerance_value_(kDefaultPrimalTolerance), dual_tolerance_value_(kDefaultDualTolerance), presolve_value_(kDefaultPresolve), scaling_value_(kDefaultIntegerParamValue), lp_algorithm_value_(kDefaultIntegerParamValue), incrementality_value_(kDefaultIncrementality), lp_algorithm_is_default_(true) {} void MPSolverParameters::SetDoubleParam(MPSolverParameters::DoubleParam param, double value) { switch (param) { case RELATIVE_MIP_GAP: { relative_mip_gap_value_ = value; break; } case PRIMAL_TOLERANCE: { primal_tolerance_value_ = value; break; } case DUAL_TOLERANCE: { dual_tolerance_value_ = value; break; } default: { LOG(ERROR) << "Trying to set an unknown parameter: " << param << "."; } } } void MPSolverParameters::SetIntegerParam(MPSolverParameters::IntegerParam param, int value) { switch (param) { case PRESOLVE: { if (value != PRESOLVE_OFF && value != PRESOLVE_ON) { LOG(ERROR) << "Trying to set a supported parameter: " << param << " to an unknown value: " << value; } presolve_value_ = value; break; } case SCALING: { if (value != SCALING_OFF && value != SCALING_ON) { LOG(ERROR) << "Trying to set a supported parameter: " << param << " to an unknown value: " << value; } scaling_value_ = value; break; } case LP_ALGORITHM: { if (value != DUAL && value != PRIMAL && value != BARRIER) { LOG(ERROR) << "Trying to set a supported parameter: " << param << " to an unknown value: " << value; } lp_algorithm_value_ = value; lp_algorithm_is_default_ = false; break; } case INCREMENTALITY: { if (value != INCREMENTALITY_OFF && value != INCREMENTALITY_ON) { LOG(ERROR) << "Trying to set a supported parameter: " << param << " to an unknown value: " << value; } incrementality_value_ = value; break; } default: { LOG(ERROR) << "Trying to set an unknown parameter: " << param << "."; } } } void MPSolverParameters::ResetDoubleParam( MPSolverParameters::DoubleParam param) { switch (param) { case RELATIVE_MIP_GAP: { relative_mip_gap_value_ = kDefaultRelativeMipGap; break; } case PRIMAL_TOLERANCE: { primal_tolerance_value_ = kDefaultPrimalTolerance; break; } case DUAL_TOLERANCE: { dual_tolerance_value_ = kDefaultDualTolerance; break; } default: { LOG(ERROR) << "Trying to reset an unknown parameter: " << param << "."; } } } void MPSolverParameters::ResetIntegerParam( MPSolverParameters::IntegerParam param) { switch (param) { case PRESOLVE: { presolve_value_ = kDefaultPresolve; break; } case SCALING: { scaling_value_ = kDefaultIntegerParamValue; break; } case LP_ALGORITHM: { lp_algorithm_is_default_ = true; break; } case INCREMENTALITY: { incrementality_value_ = kDefaultIncrementality; break; } default: { LOG(ERROR) << "Trying to reset an unknown parameter: " << param << "."; } } } void MPSolverParameters::Reset() { ResetDoubleParam(RELATIVE_MIP_GAP); ResetDoubleParam(PRIMAL_TOLERANCE); ResetDoubleParam(DUAL_TOLERANCE); ResetIntegerParam(PRESOLVE); ResetIntegerParam(SCALING); ResetIntegerParam(LP_ALGORITHM); ResetIntegerParam(INCREMENTALITY); } double MPSolverParameters::GetDoubleParam( MPSolverParameters::DoubleParam param) const { switch (param) { case RELATIVE_MIP_GAP: { return relative_mip_gap_value_; } case PRIMAL_TOLERANCE: { return primal_tolerance_value_; } case DUAL_TOLERANCE: { return dual_tolerance_value_; } default: { LOG(ERROR) << "Trying to get an unknown parameter: " << param << "."; return kUnknownDoubleParamValue; } } } int MPSolverParameters::GetIntegerParam( MPSolverParameters::IntegerParam param) const { switch (param) { case PRESOLVE: { return presolve_value_; } case LP_ALGORITHM: { if (lp_algorithm_is_default_) return kDefaultIntegerParamValue; return lp_algorithm_value_; } case INCREMENTALITY: { return incrementality_value_; } case SCALING: { return scaling_value_; } default: { LOG(ERROR) << "Trying to get an unknown parameter: " << param << "."; return kUnknownIntegerParamValue; } } } // static std::string MPSolver::GetMPModelRequestLoggingInfo( const MPModelRequest& request) { std::string out; #if !defined(__PORTABLE_PLATFORM__) MPModelRequest abbreviated_request; abbreviated_request = request; abbreviated_request.clear_model(); abbreviated_request.clear_model_delta(); google::protobuf::TextFormat::PrintToString(abbreviated_request, &out); #else // __PORTABLE_PLATFORM__ out = "\n"; #endif // __PORTABLE_PLATFORM__ if (request.model().has_name()) { absl::StrAppendFormat(&out, "model_name: '%s'\n", request.model().name()); } return out; } MPSolverInterfaceFactoryRepository* MPSolverInterfaceFactoryRepository::GetInstance() { static auto* const kInstance = new MPSolverInterfaceFactoryRepository(); return kInstance; } // This can't be covered by unit test coverage framework, because the Singleton // destruction occurs after it finished collecting coverage. // COV_NF_START MPSolverInterfaceFactoryRepository::~MPSolverInterfaceFactoryRepository() { absl::MutexLock lock(mutex_); map_.clear(); } // COV_NF_END void MPSolverInterfaceFactoryRepository::Register( MPSolverInterfaceFactory factory, MPSolver::OptimizationProblemType problem_type, std::function is_runtime_ready) { absl::MutexLock lock(mutex_); if (!is_runtime_ready) is_runtime_ready = []() { return true; }; map_[problem_type] = Entry{ .factory = std::move(factory), .is_runtime_ready = std::move(is_runtime_ready), }; } bool MPSolverInterfaceFactoryRepository::Unregister( MPSolver::OptimizationProblemType problem_type) { absl::MutexLock lock(mutex_); return map_.erase(problem_type) == 1; } MPSolverInterface* MPSolverInterfaceFactoryRepository::Create( MPSolver* solver) const { absl::MutexLock lock(mutex_); const Entry* entry = gtl::FindOrNull(map_, solver->ProblemType()); CHECK(entry != nullptr) << "No factory registered for problem type " << ToString(solver->ProblemType()); CHECK(entry->is_runtime_ready()) << "Solver for problem type " << ToString(solver->ProblemType()) << " is not ready."; return entry->factory(solver); } bool MPSolverInterfaceFactoryRepository::Supports( MPSolver::OptimizationProblemType problem_type) const { const Entry* entry = gtl::FindOrNull(map_, problem_type); if (entry == nullptr) return false; return entry->is_runtime_ready(); } std::vector MPSolverInterfaceFactoryRepository::ListAllRegisteredProblemTypes() const { std::vector out; for (auto it = map_.begin(); it != map_.end(); ++it) { out.push_back(it->first); } return out; } std::string MPSolverInterfaceFactoryRepository // NOLINT ::PrettyPrintAllRegisteredProblemTypes() const { std::string out; for (auto it = map_.begin(); it != map_.end(); ++it) { out += ProtoEnumToString( static_cast(it->first)) + "\n"; } return out; } } // namespace operations_research