Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Feature/xpress solver for mathopt #137

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
7 changes: 0 additions & 7 deletions ortools/linear_solver/xpress_interface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,6 @@ void interruptXPRESS(XPRSprob& xprsProb, CUSTOM_INTERRUPT_REASON reason) {
XPRSinterrupt(xprsProb, 1000 + reason);
}

enum XPRS_BASIS_STATUS {
XPRS_AT_LOWER = 0,
XPRS_BASIC = 1,
XPRS_AT_UPPER = 2,
XPRS_FREE_SUPER = 3
};

// In case we need to return a double but don't have a value for that
// we just return a NaN.
#if !defined(XPRS_NAN)
Expand Down
2 changes: 2 additions & 0 deletions ortools/math_opt/cpp/parameters.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ std::optional<absl::string_view> Enum<SolverType>::ToOptString(
return "highs";
case SolverType::kSantorini:
return "santorini";
case SolverType::kXpress:
return "xpress";
}
return std::nullopt;
}
Expand Down
6 changes: 6 additions & 0 deletions ortools/math_opt/cpp/parameters.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ enum class SolverType {
// Slow/not recommended for production. Not an LP solver (no dual information
// returned).
kSantorini = SOLVER_TYPE_SANTORINI,

// Fico XPRESS solver (third party).
//
// Supports LP, MIP, and nonconvex integer quadratic problems.
// A fast option, but has special licensing.
kXpress = SOLVER_TYPE_XPRESS
};

MATH_OPT_DEFINE_ENUM(SolverType, SOLVER_TYPE_UNSPECIFIED);
Expand Down
6 changes: 6 additions & 0 deletions ortools/math_opt/parameters.proto
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ enum SolverTypeProto {
// Slow/not recommended for production. Not an LP solver (no dual information
// returned).
SOLVER_TYPE_SANTORINI = 11;

// Fico XPRESS solver (third party).
//
// Supports LP, MIP, and nonconvex integer quadratic problems.
// A fast option, but has special licensing.
SOLVER_TYPE_XPRESS = 12;
}

// Selects an algorithm for solving linear programs.
Expand Down
6 changes: 6 additions & 0 deletions ortools/math_opt/solver_tests/base_solver_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ bool ActivatePrimalRay(const SolverType solver_type, SolveParameters& params) {
return false;
case SolverType::kHighs:
return false;
case SolverType::kXpress:
// TODO: support XPRESS
return false;
default:
LOG(FATAL)
<< "Solver " << solver_type
Expand Down Expand Up @@ -82,6 +85,9 @@ bool ActivateDualRay(const SolverType solver_type, SolveParameters& params) {
return false;
case SolverType::kHighs:
return false;
case SolverType::kXpress:
// TODO: support XPRESS
return false;
default:
LOG(FATAL)
<< "Solver " << solver_type
Expand Down
36 changes: 36 additions & 0 deletions ortools/math_opt/solvers/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ if(NOT USE_SCIP)
list(FILTER _SRCS EXCLUDE REGEX "/gscip_.*.h$")
list(FILTER _SRCS EXCLUDE REGEX "/gscip_.*.cc$")
endif()
if(NOT USE_XPRESS)
list(FILTER _SRCS EXCLUDE REGEX "/xpress/")
list(FILTER _SRCS EXCLUDE REGEX "/xpress_.*.h$")
list(FILTER _SRCS EXCLUDE REGEX "/xpress_.*.cc$")
endif()
target_sources(${NAME} PRIVATE ${_SRCS})
set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_include_directories(${NAME} PUBLIC
Expand Down Expand Up @@ -233,3 +238,34 @@ if(USE_HIGHS)
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_status_tests>"
)
endif()

if(USE_XPRESS)
ortools_cxx_test(
NAME
math_opt_solvers_xpress_solver_test
SOURCES
"xpress_solver_test.cc"
LINK_LIBRARIES
GTest::gmock
GTest::gmock_main
absl::status
ortools::math_opt_matchers
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_callback_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_invalid_input_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_generic_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_infeasible_subsystem_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_ip_model_solve_parameters_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_ip_parameter_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_logical_constraint_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_lp_incomplete_solve_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_lp_model_solve_parameters_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_lp_parameter_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_lp_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_mip_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_multi_objective_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_status_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_qp_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_second_order_cone_tests>"
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_qc_tests>"
)
endif()
263 changes: 263 additions & 0 deletions ortools/math_opt/solvers/xpress/g_xpress.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
// Copyright 2010-2024 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "ortools/math_opt/solvers/xpress/g_xpress.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "absl/log/check.h"
#include "absl/log/die_if_null.h"
#include "absl/memory/memory.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_format.h"
#include "absl/types/span.h"
#include "ortools/base/logging.h"
#include "ortools/base/source_location.h"
#include "ortools/base/status_builder.h"
#include "ortools/base/status_macros.h"
#include "ortools/xpress/environment.h"

namespace operations_research::math_opt {
constexpr int kXpressOk = 0;

absl::Status Xpress::ToStatus(const int xprs_err,
const absl::StatusCode code) const {
if (xprs_err == kXpressOk) {
return absl::OkStatus();
}
char errmsg[512];
XPRSgetlasterror(xpress_model_, errmsg);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory XPRSgetlasterror() can fail. To make this rock solid I suggest to check the return value of that function and set errmsg to some generic string in case the function returns non-zero.

return util::StatusBuilder(code)
<< "Xpress error code: " << xprs_err << ", message: " << errmsg;
}

Xpress::Xpress(XPRSprob& model) : xpress_model_(ABSL_DIE_IF_NULL(model)) {}

absl::StatusOr<std::unique_ptr<Xpress>> Xpress::New(
const std::string& model_name) {
bool correctlyLoaded = initXpressEnv();
CHECK(correctlyLoaded);
XPRSprob* model;
CHECK_EQ(kXpressOk, XPRScreateprob(model));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks wrong to me. XPRSprob already is a pointer type. I think this should read

XPRSprob model;
CHECK_EQ(kXpressOk, XPRScreateprob(&model));
DCHECK(model != nullptr);
return absl::WrapUnique(new Xpress(model));

DCHECK(model != nullptr); // should not be NULL if status=0
CHECK_EQ(kXpressOk, XPRSaddcbmessage(*model, printXpressMessage, NULL, 0));
return absl::WrapUnique(new Xpress(*model));
}

void XPRS_CC Xpress::printXpressMessage(XPRSprob prob, void* data,
const char* sMsg, int nLen,
int nMsgLvl) {
if (sMsg) {
std::cout << sMsg << std::endl;
}
}

Xpress::~Xpress() {
CHECK_EQ(kXpressOk, XPRSdestroyprob(xpress_model_));
CHECK_EQ(kXpressOk, XPRSfree());
}

absl::Status Xpress::AddVars(const absl::Span<const double> obj,
const absl::Span<const double> lb,
const absl::Span<const double> ub,
const absl::Span<const char> vtype) {
return AddVars({}, {}, {}, obj, lb, ub, vtype);
}

absl::Status Xpress::AddVars(const absl::Span<const int> vbegin,
const absl::Span<const int> vind,
const absl::Span<const double> vval,
const absl::Span<const double> obj,
const absl::Span<const double> lb,
const absl::Span<const double> ub,
const absl::Span<const char> vtype) {
const int num_vars = static_cast<int>(lb.size());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are not quite ready yet, but we are working up to int64 support (models with more than 2**31 variables). Not sure if xpress supports this, but if you do, maybe use int64_t instead of int here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XPRESS does support this but through separate API calls of course (in this instance, XPRSaddcols64 instead of XPRSaddcols). If it's OK for you, I'll add a TODO here to look into it, and add support for int64 later (it will need some reflexion on how to support both 32 and 64 archs, maybe we'll wait for you to do it for gurobi and then copy)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Xpress supports only 64bit counts of non-zeros. Number of variables, rows, etc. are all limited to 32bit signed integers. Since you are not adding any non-zeros here, the code is already safe for the kind of 64bit stuff that Xpress supports.
I still suggest that instead of static_cast<int>(lb.size()) you use a function that checks for overflow (lb.size() larger than INT_MAX) and raises an error if a user tries to create that many variables. I guess such a function will come in handy in other places as well.

if (vind.size() != vval.size() || ub.size() != num_vars ||
vtype.size() != num_vars || (!obj.empty() && obj.size() != num_vars) ||
(!vbegin.empty() && vbegin.size() != num_vars)) {
return absl::InvalidArgumentError(
"Xpress::AddVars arguments are of inconsistent sizes");
}
double* c_obj = nullptr;
if (!obj.empty()) {
c_obj = const_cast<double*>(obj.data());
}
// TODO: look into int64 support for number of vars (use XPRSaddcols64)
return ToStatus(XPRSaddcols(xpress_model_, num_vars, 0, c_obj, nullptr,
nullptr, nullptr, lb.data(), ub.data()));
}

absl::Status Xpress::AddConstrs(const absl::Span<const char> sense,
const absl::Span<const double> rhs,
const absl::Span<const double> rng) {
const int num_cons = static_cast<int>(sense.size());
if (rhs.size() != num_cons) {
return absl::InvalidArgumentError(
"RHS must have one element per constraint.");
}
return ToStatus(XPRSaddrows(xpress_model_, num_cons, 0, sense.data(),
rhs.data(), rng.data(), NULL, NULL, NULL));
}

absl::Status Xpress::SetObjective(bool maximize, double offset,
const absl::Span<const int> colind,
const absl::Span<const double> values) {
RETURN_IF_ERROR(ToStatus(XPRSchgobjsense(
xpress_model_, maximize ? XPRS_OBJ_MAXIMIZE : XPRS_OBJ_MINIMIZE)))
<< "Failed to change objective sense in XPRESS";

static int indexes[1] = {-1};
double xprs_values[1] = {-offset};
RETURN_IF_ERROR(ToStatus(XPRSchgobj(xpress_model_, 1, indexes, xprs_values)))
<< "Failed to set objective offset in XPRESS";

const int n_cols = static_cast<int>(colind.size());
auto c_colind = const_cast<int*>(colind.data());
auto c_values = const_cast<double*>(values.data());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why const_cast? There is no need to cast away the const since XPRSchgobj() takes const arguments.

return ToStatus(XPRSchgobj(xpress_model_, n_cols, c_colind, c_values));
}

absl::Status Xpress::ChgCoeffs(absl::Span<const int> rowind,
absl::Span<const int> colind,
absl::Span<const double> values) {
const int n_coefs = static_cast<int>(rowind.size());
auto c_rowind = const_cast<int*>(rowind.data());
auto c_colind = const_cast<int*>(colind.data());
auto c_values = const_cast<double*>(values.data());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, no need to cast away const. You should be able to directly pass rowind.data() etc. to XPRSchgmcoef().
Here I suggest to use XPRSchgmcoef64(). All data is already in the right format. You only have to change n_coefs to a 64bit integer. This then supports 64bit non-zero counts.

return ToStatus(
XPRSchgmcoef(xpress_model_, n_coefs, c_rowind, c_colind, c_values));
}

absl::StatusOr<int> Xpress::LpOptimizeAndGetStatus(std::string flags) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something that is required by ORtools? XPRSlpoptimize() solves any problem as an LP. For a MIP, it will just solve it as an LP.
In general, in recent versions of Xpress you are supposed to call XPRSoptimize() to "just optimize" your problem. This will pick the appropriate algorithm automatically.
If ORtools requires a function solve a MIP as an LP then the function implemented here is fine.

RETURN_IF_ERROR(ToStatus(XPRSlpoptimize(xpress_model_, flags.c_str())))
<< "XPRESS LP solve failed";
int xpress_status;
RETURN_IF_ERROR(
ToStatus(XPRSgetintattrib(xpress_model_, XPRS_LPSTATUS, &xpress_status)))
<< "Could not get XPRESS status";
return xpress_status;
}
absl::Status Xpress::PostSolve() {
return ToStatus(XPRSpostsolve(xpress_model_));
}

absl::StatusOr<int> Xpress::MipOptimizeAndGetStatus() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as for the LP optimizing function above. XPRSmipoptimize() looks a bit fishy since XPRSoptimize() should do what you want.

RETURN_IF_ERROR(ToStatus(XPRSmipoptimize(xpress_model_, nullptr)))
<< "XPRESS MIP solve failed";
int xpress_status;
RETURN_IF_ERROR(
ToStatus(XPRSgetintattrib(xpress_model_, XPRS_MIPSTATUS, &xpress_status)))
<< "Could not get XPRESS status";
return xpress_status;
}

void Xpress::Terminate() { XPRSinterrupt(xpress_model_, XPRS_STOP_USER); };

absl::StatusOr<int> Xpress::GetIntAttr(int attribute) const {
int result;
RETURN_IF_ERROR(ToStatus(XPRSgetintattrib(xpress_model_, attribute, &result)))
<< "Error getting Xpress int attribute: " << attribute;
return result;
}

absl::Status Xpress::SetIntAttr(int attribute, int value) {
return ToStatus(XPRSsetintcontrol(xpress_model_, attribute, value));
}

absl::StatusOr<double> Xpress::GetDoubleAttr(int attribute) const {
double result;
RETURN_IF_ERROR(ToStatus(XPRSgetdblattrib(xpress_model_, attribute, &result)))
<< "Error getting Xpress double attribute: " << attribute;
return result;
}

absl::StatusOr<std::vector<double>> Xpress::GetPrimalValues() const {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which context is this function supposed to be called? There are only very special situations (inside callbacks) in which you would want to use XPRSgetlpvalues() to get the x vector. You most likely want to use XPRSgetsolution() here. This also has the advantage that it returns a status that indicates whether a solution is available at all.

int nVars = GetNumberOfVariables();
XPRSgetintattrib(xpress_model_, XPRS_COLS, &nVars);
std::vector<double> values(nVars);
RETURN_IF_ERROR(ToStatus(
XPRSgetlpsol(xpress_model_, values.data(), nullptr, nullptr, nullptr)))
<< "Error getting Xpress LP solution";
return values;
}

int Xpress::GetNumberOfConstraints() const {
int n;
XPRSgetintattrib(xpress_model_, XPRS_ROWS, &n);
return n;
}

int Xpress::GetNumberOfVariables() const {
int n;
XPRSgetintattrib(xpress_model_, XPRS_COLS, &n);
return n;
}

absl::StatusOr<std::vector<double>> Xpress::GetConstraintDuals() const {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XPRSgetduals() is the preferred way to query duals.

int nCons = GetNumberOfConstraints();
double values[nCons];
RETURN_IF_ERROR(
ToStatus(XPRSgetlpsol(xpress_model_, nullptr, nullptr, values, nullptr)))
<< "Failed to retrieve LP solution from XPRESS";
std::vector<double> result(values, values + nCons);
return result;
}
absl::StatusOr<std::vector<double>> Xpress::GetReducedCostValues() const {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XPRSgetredcosts() is the preferred way to query reduced costs.

int nVars = GetNumberOfVariables();
double values[nVars];
RETURN_IF_ERROR(
ToStatus(XPRSgetlpsol(xpress_model_, nullptr, nullptr, nullptr, values)))
<< "Failed to retrieve LP solution from XPRESS";
std::vector<double> result(values, values + nVars);
return result;
}

absl::Status Xpress::GetBasis(std::vector<int>& rowBasis,
std::vector<int>& colBasis) const {
rowBasis.resize(GetNumberOfConstraints());
colBasis.resize(GetNumberOfVariables());
return ToStatus(
XPRSgetbasis(xpress_model_, rowBasis.data(), colBasis.data()));
}

absl::Status Xpress::SetStartingBasis(std::vector<int>& rowBasis,
std::vector<int>& colBasis) const {
return ToStatus(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe assert that rowBasis and colBasis have the right size?

XPRSloadbasis(xpress_model_, rowBasis.data(), colBasis.data()));
}

absl::StatusOr<std::vector<double>> Xpress::GetVarLb() const {
int nVars = GetNumberOfVariables();
std::vector<double> bounds;
bounds.reserve(nVars);
RETURN_IF_ERROR(
ToStatus(XPRSgetlb(xpress_model_, bounds.data(), 0, nVars - 1)))
<< "Failed to retrieve variable LB from XPRESS";
return bounds;
}
absl::StatusOr<std::vector<double>> Xpress::GetVarUb() const {
int nVars = GetNumberOfVariables();
std::vector<double> bounds;
bounds.reserve(nVars);
RETURN_IF_ERROR(
ToStatus(XPRSgetub(xpress_model_, bounds.data(), 0, nVars - 1)))
<< "Failed to retrieve variable UB from XPRESS";
return bounds;
}

} // namespace operations_research::math_opt
Loading