From df147b7b03f520772e7689a7b4b597f05f2dba79 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Wed, 3 Apr 2024 14:48:38 +0200 Subject: [PATCH] experimental support for highs in model_builder --- .../linear_solver/proto_solver/BUILD.bazel | 6 + .../linear_solver/proto_solver/CMakeLists.txt | 1 + .../proto_solver/highs_proto_solver.cc | 279 +++++++++++++++++- ortools/linear_solver/wrappers/BUILD.bazel | 2 + .../wrappers/model_builder_helper.cc | 21 ++ 5 files changed, 302 insertions(+), 7 deletions(-) diff --git a/ortools/linear_solver/proto_solver/BUILD.bazel b/ortools/linear_solver/proto_solver/BUILD.bazel index deaca746d4..57a1d82227 100644 --- a/ortools/linear_solver/proto_solver/BUILD.bazel +++ b/ortools/linear_solver/proto_solver/BUILD.bazel @@ -167,12 +167,18 @@ cc_library( name = "highs_proto_solver", srcs = ["highs_proto_solver.cc"], hdrs = ["highs_proto_solver.h"], + defines = select({ + "//ortools/linear_solver:use_highs": ["USE_HIGHS"], + "//conditions:default": [], + }), deps = [ + "//ortools/base:timer", "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/linear_solver:model_validator", "//ortools/port:proto_utils", "//ortools/util:lazy_mutable_copy", "@com_google_absl//absl/status:statusor", + "@highs", ], ) diff --git a/ortools/linear_solver/proto_solver/CMakeLists.txt b/ortools/linear_solver/proto_solver/CMakeLists.txt index 8a65d2af89..3ae5e16d83 100644 --- a/ortools/linear_solver/proto_solver/CMakeLists.txt +++ b/ortools/linear_solver/proto_solver/CMakeLists.txt @@ -47,5 +47,6 @@ target_link_libraries(${NAME} PRIVATE absl::str_format $<$:Eigen3::Eigen> $<$:libscip> + $<$:highs::highs> ${PROJECT_NAMESPACE}::${PROJECT_NAME}_proto) #add_library(${PROJECT_NAMESPACE}::linear_solver_proto_solver ALIAS ${NAME}) diff --git a/ortools/linear_solver/proto_solver/highs_proto_solver.cc b/ortools/linear_solver/proto_solver/highs_proto_solver.cc index d00189abdf..2c0a438843 100644 --- a/ortools/linear_solver/proto_solver/highs_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/highs_proto_solver.cc @@ -14,25 +14,290 @@ #include "ortools/linear_solver/proto_solver/highs_proto_solver.h" #include -#include -#include -#include #include -#include -#include #include +#include "Highs.h" #include "absl/status/statusor.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" +#include "google/protobuf/repeated_field.h" +#include "ortools/base/timer.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_validator.h" -#include "ortools/port/proto_utils.h" #include "ortools/util/lazy_mutable_copy.h" namespace operations_research { +absl::Status SetSolverSpecificParameters(const std::string& parameters, + Highs& highs); + absl::StatusOr HighsSolveProto( LazyMutableCopy request) { - return absl::UnimplementedError("Highs support is not yet implemented"); + MPSolutionResponse response; + const absl::optional> optional_model = + GetMPModelOrPopulateResponse(request, &response); + if (!optional_model) return response; + const MPModelProto& model = **optional_model; + + Highs highs; + // highs.setOptionValue("model_name", model.name()); + + if (request->has_solver_specific_parameters()) { + const auto parameters_status = SetSolverSpecificParameters( + request->solver_specific_parameters(), highs); + if (!parameters_status.ok()) { + response.set_status(MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); + response.set_status_str( + std::string(parameters_status.message())); // NOLINT + return response; + } + } + + if (request->solver_time_limit_seconds() > 0) { + HighsStatus status = highs.setOptionValue( + "time_limit", request->solver_time_limit_seconds()); + if (status == HighsStatus::kError) { + response.set_status(MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); + response.set_status_str("time_limit"); + return response; + } + } + + const int variable_size = model.variable_size(); + bool has_integer_variables = false; + { + std::vector obj_coeffs(variable_size, 0); + std::vector lb(variable_size); + std::vector ub(variable_size); + std::vector integrality(variable_size); + std::vector varnames(variable_size); + for (int v = 0; v < variable_size; ++v) { + const MPVariableProto& variable = model.variable(v); + obj_coeffs[v] = variable.objective_coefficient(); + lb[v] = variable.lower_bound(); + ub[v] = variable.upper_bound(); + integrality[v] = + variable.is_integer() && + request->solver_type() == + MPModelRequest::HIGHS_MIXED_INTEGER_PROGRAMMING + ? 1 + : 0; + if (variable.is_integer()) has_integer_variables = true; + if (!variable.name().empty()) varnames[v] = variable.name().c_str(); + } + + highs.addVars(variable_size, lb.data(), ub.data()); + for (int column = 0; column < variable_size; column++) { + assert(obj_coeffs.size() == variable_size); + highs.changeColCost(column, obj_coeffs[column]); + } + + // /*varnames=*/const_cast(varnames.data()))); + + // Set solution hints if any. + // for (int i = 0; i < model.solution_hint().var_index_size(); ++i) { + // RETURN_IF_GUROBI_ERROR(GRBsetdblattrelement( + // gurobi_model, GRB_DBL_ATTR_START, + // model.solution_hint().var_index(i), + // model.solution_hint().var_value(i))); + // } + } + + { + std::vector ct_variables; + std::vector ct_coefficients; + for (int c = 0; c < model.constraint_size(); ++c) { + const MPConstraintProto& constraint = model.constraint(c); + const int size = constraint.var_index_size(); + ct_variables.resize(size, 0); + ct_coefficients.resize(size, 0); + for (int i = 0; i < size; ++i) { + ct_variables[i] = constraint.var_index(i); + ct_coefficients[i] = constraint.coefficient(i); + } + + if (constraint.lower_bound() == + -std::numeric_limits::infinity()) { + HighsStatus status = highs.addRow(/*lhs=*/-kHighsInf, + /*rhs=*/constraint.upper_bound(), + /*numnz=*/size, + /*cind=*/ct_variables.data(), + /*cval=*/ct_coefficients.data()); + if (status == HighsStatus::kError) { + response.set_status(MPSOLVER_MODEL_INVALID); + response.set_status_str("ct addRow"); + return response; + } + } else if (constraint.upper_bound() == + std::numeric_limits::infinity()) { + HighsStatus status = highs.addRow(/*lhs=*/constraint.lower_bound(), + /*rhs=*/kHighsInf, + /*numnz=*/size, + /*cind=*/ct_variables.data(), + /*cval=*/ct_coefficients.data()); + if (status == HighsStatus::kError) { + response.set_status(MPSOLVER_MODEL_INVALID); + response.set_status_str("ct addRow"); + return response; + } + } else { + HighsStatus status = highs.addRow(/*lhs=*/constraint.lower_bound(), + /*rhs=*/constraint.upper_bound(), + /*numnz=*/size, + /*cind=*/ct_variables.data(), + /*cval=*/ct_coefficients.data()); + if (status == HighsStatus::kError) { + response.set_status(MPSOLVER_MODEL_INVALID); + response.set_status_str("ct addRow"); + return response; + } + + // /*constrname=*/constraint.name().c_str())); + } + } + + if (!model.general_constraint().empty()) { + response.set_status(MPSOLVER_MODEL_INVALID); + response.set_status_str("general constraints are not supported in Highs"); + return response; + } + } + if (model.maximize()) { + const ObjSense pass_sense = ObjSense::kMaximize; + highs.changeObjectiveSense(pass_sense); + } + + if (model.objective_offset()) { + const double offset = model.objective_offset(); + highs.changeObjectiveOffset(offset); + } + // if (model.has_quadratic_objective()) { + + const absl::Time time_before = absl::Now(); + UserTimer user_timer; + user_timer.Start(); + HighsStatus run_status = highs.run(); + switch (run_status) { + case HighsStatus::kError: { + response.set_status(MPSOLVER_NOT_SOLVED); + response.set_status_str("Error running HiGHS run()"); + return response; + } + case HighsStatus::kWarning: { + response.set_status_str("Warning HiGHS run()"); + break; + } + case HighsStatus::kOk: { + HighsModelStatus model_status = highs.getModelStatus(); + switch (model_status) { + case HighsModelStatus::kOptimal: + response.set_status(MPSOLVER_OPTIMAL); + break; + case HighsModelStatus::kUnboundedOrInfeasible: + // DLOG(INFO) << "HiGHSsolve returned kUnboundedOrInfeasible, which we + // treat as " + // "INFEASIBLE even though it may mean UNBOUNDED."; + response.set_status_str( + "The model may actually be unbounded: HiGHS returned " + "kUnboundedOrInfeasible"); + response.set_status(MPSOLVER_INFEASIBLE); + break; + case HighsModelStatus::kInfeasible: + response.set_status(MPSOLVER_INFEASIBLE); + break; + case HighsModelStatus::kUnbounded: + response.set_status(MPSOLVER_UNBOUNDED); + break; + default: { + // todo + // if (solution_count > 0) + break; + } + } + } + } + + const absl::Duration solving_duration = absl::Now() - time_before; + user_timer.Stop(); + // VLOG(1) << "Finished solving in GurobiSolveProto(), walltime = " + // << solving_duration << ", usertime = " << user_timer.GetDuration(); + response.mutable_solve_info()->set_solve_wall_time_seconds( + absl::ToDoubleSeconds(solving_duration)); + response.mutable_solve_info()->set_solve_user_time_seconds( + absl::ToDoubleSeconds(user_timer.GetDuration())); + + if (response.status() == MPSOLVER_OPTIMAL) { + double objective_value = highs.getObjectiveValue(); + response.set_objective_value(objective_value); + response.set_best_objective_bound(objective_value); + + response.mutable_variable_value()->Resize(variable_size, 0); + for (int column = 0; column < variable_size; column++) { + response.mutable_variable_value()->mutable_data()[column] = + highs.getSolution().col_value[column]; + } + + // NOTE, HighsSolveProto() is exposed to external clients via MPSolver API, + // which assumes the solution values of integer variables are rounded to + // integer values. + auto round_values_of_integer_variables_fn = + [&](google::protobuf::RepeatedField* values) { + for (int v = 0; v < variable_size; ++v) { + if (model.variable(v).is_integer()) { + (*values)[v] = std::round((*values)[v]); + } + } + }; + round_values_of_integer_variables_fn(response.mutable_variable_value()); + + if (!has_integer_variables && model.general_constraint_size() == 0) { + response.mutable_dual_value()->Resize(model.constraint_size(), 0); + for (int row = 0; row < model.constraint_size(); row++) { + response.mutable_variable_value()->mutable_data()[row] = + highs.getSolution().row_value[row]; + } + } + } + + return response; +} + +absl::Status SetSolverSpecificParameters(const std::string& parameters, + Highs& highs) { + if (parameters.empty()) return absl::OkStatus(); + std::vector error_messages; + for (absl::string_view line : absl::StrSplit(parameters, '\n')) { + // Comment tokens end at the next new-line, or the end of the string. + // The first character must be '#' + if (line[0] == '#') continue; + for (absl::string_view token : + absl::StrSplit(line, ',', absl::SkipWhitespace())) { + if (token.empty()) continue; + std::vector key_value = + absl::StrSplit(token, absl::ByAnyChar(" ="), absl::SkipWhitespace()); + // If one parameter fails, we keep processing the list of parameters. + if (key_value.size() != 2) { + const std::string current_message = + absl::StrCat("Cannot parse parameter '", token, + "'. Expected format is 'ParameterName value' or " + "'ParameterName=value'"); + error_messages.push_back(current_message); + continue; + } + HighsStatus status = highs.setOptionValue(key_value[0], key_value[1]); + if (status == HighsStatus::kError) { + const std::string current_message = + absl::StrCat("Error setting parameter '", key_value[0], + "' to value '", key_value[1], "': "); + error_messages.push_back(current_message); + continue; + } + } + } + + if (error_messages.empty()) return absl::OkStatus(); + return absl::InvalidArgumentError(absl::StrJoin(error_messages, "\n")); } } // namespace operations_research diff --git a/ortools/linear_solver/wrappers/BUILD.bazel b/ortools/linear_solver/wrappers/BUILD.bazel index 727d5eae32..f0f031b2c5 100644 --- a/ortools/linear_solver/wrappers/BUILD.bazel +++ b/ortools/linear_solver/wrappers/BUILD.bazel @@ -30,6 +30,7 @@ cc_library( srcs = ["model_builder_helper.cc"], hdrs = ["model_builder_helper.h"], copts = [ + "-DUSE_HIGHS", "-DUSE_PDLP", "-DUSE_SCIP", "-DUSE_LP_PARSER", @@ -42,6 +43,7 @@ cc_library( "//ortools/linear_solver:solve_mp_model", "//ortools/linear_solver/proto_solver:glop_proto_solver", "//ortools/linear_solver/proto_solver:gurobi_proto_solver", + "//ortools/linear_solver/proto_solver:highs_proto_solver", "//ortools/linear_solver/proto_solver:pdlp_proto_solver", "//ortools/linear_solver/proto_solver:sat_proto_solver", "//ortools/linear_solver/proto_solver:scip_proto_solver", diff --git a/ortools/linear_solver/wrappers/model_builder_helper.cc b/ortools/linear_solver/wrappers/model_builder_helper.cc index a1f24be23b..438eac97de 100644 --- a/ortools/linear_solver/wrappers/model_builder_helper.cc +++ b/ortools/linear_solver/wrappers/model_builder_helper.cc @@ -38,6 +38,9 @@ #if defined(USE_SCIP) #include "ortools/linear_solver/proto_solver/scip_proto_solver.h" #endif // defined(USE_SCIP) +#if defined(USE_HIGHS) +#include "ortools/linear_solver/proto_solver/highs_proto_solver.h" +#endif // defined(USE_HIGHS) #if defined(USE_PDLP) #include "ortools/linear_solver/proto_solver/pdlp_proto_solver.h" #endif // defined(USE_PDLP) @@ -534,6 +537,12 @@ bool ModelSolverHelper::SolverIsSupported() const { return true; } #endif // USE_SCIP +#ifdef USE_HIGHS + if (solver_type_.value() == MPModelRequest::HIGHS_LINEAR_PROGRAMMING || + solver_type_.value() == MPModelRequest::HIGHS_MIXED_INTEGER_PROGRAMMING) { + return true; + } +#endif // USE_HIGHS if (solver_type_.value() == MPModelRequest::GUROBI_MIXED_INTEGER_PROGRAMMING || solver_type_.value() == MPModelRequest::GUROBI_LINEAR_PROGRAMMING) { @@ -605,6 +614,18 @@ void ModelSolverHelper::Solve(const ModelBuilderHelper& model) { } break; } +#if defined(USE_HIGHS) + case MPModelRequest::HIGHS_LINEAR_PROGRAMMING: // ABSL_FALLTHROUGH_INTENDED + case MPModelRequest::HIGHS_MIXED_INTEGER_PROGRAMMING: { + // TODO(user): Enable log_callback support. + // TODO(user): Enable interrupt_solve. + const auto temp = HighsSolveProto(std::move(request)); + if (temp.ok()) { + response_ = std::move(temp.value()); + } + break; + } +#endif // defined(USE_HIGHS) case MPModelRequest:: XPRESS_LINEAR_PROGRAMMING: // ABSL_FALLTHROUGH_INTENDED case MPModelRequest::XPRESS_MIXED_INTEGER_PROGRAMMING: {