experimental support for highs in model_builder

This commit is contained in:
Laurent Perron
2024-04-03 14:48:38 +02:00
parent 888d89b5e0
commit df147b7b03
5 changed files with 302 additions and 7 deletions

View File

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

View File

@@ -47,5 +47,6 @@ target_link_libraries(${NAME} PRIVATE
absl::str_format
$<$<BOOL:${USE_PDLP}>:Eigen3::Eigen>
$<$<BOOL:${USE_SCIP}>:libscip>
$<$<BOOL:${USE_HIGHS}>:highs::highs>
${PROJECT_NAMESPACE}::${PROJECT_NAME}_proto)
#add_library(${PROJECT_NAMESPACE}::linear_solver_proto_solver ALIAS ${NAME})

View File

@@ -14,25 +14,290 @@
#include "ortools/linear_solver/proto_solver/highs_proto_solver.h"
#include <cmath>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#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<MPSolutionResponse> HighsSolveProto(
LazyMutableCopy<MPModelRequest> request) {
return absl::UnimplementedError("Highs support is not yet implemented");
MPSolutionResponse response;
const absl::optional<LazyMutableCopy<MPModelProto>> 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<double> obj_coeffs(variable_size, 0);
std::vector<double> lb(variable_size);
std::vector<double> ub(variable_size);
std::vector<int> integrality(variable_size);
std::vector<const char*> 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<char**>(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<int> ct_variables;
std::vector<double> 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<double>::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<double>::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<double>* 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<std::string> 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<std::string> 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

View File

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

View File

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