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 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
28 changes: 28 additions & 0 deletions ortools/math_opt/solvers/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,31 @@ if(USE_HIGHS)
"$<LINK_LIBRARY:WHOLE_ARCHIVE,ortools::math_opt_status_tests>"
)
endif()

if(USE_XPRESS)
add_subdirectory(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_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>"
)
endif()
235 changes: 235 additions & 0 deletions ortools/math_opt/solvers/xpress/g_xpress.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// 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"

// The argument to this macro is the invocation of a XPRS function that
pet-mit marked this conversation as resolved.
Show resolved Hide resolved
// returns a status. If the function returns non-zero the macro aborts
// the program with an appropriate error message.
#define CHECK_STATUS(s) \
do { \
int const status_ = s; \
CHECK_EQ(0, status_); \
} while (0)

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(std::move(model))) {}
pet-mit marked this conversation as resolved.
Show resolved Hide resolved

absl::StatusOr<std::unique_ptr<Xpress>> Xpress::New(
const std::string& model_name) {
bool correctlyLoaded = initXpressEnv();
CHECK(correctlyLoaded);
XPRSprob* model;
CHECK_STATUS(XPRScreateprob(model));
DCHECK(model != nullptr); // should not be NULL if status=0
CHECK_STATUS(XPRSaddcbmessage(*model, optimizermsg, NULL, 0));
return absl::WrapUnique(new Xpress(*model));
}

void XPRS_CC optimizermsg(XPRSprob prob, void* data, const char* sMsg, int nLen,
int nMsgLvl) {
printf("%*s\n", nLen, sMsg);
pet-mit marked this conversation as resolved.
Show resolved Hide resolved
}

Xpress::~Xpress() {
CHECK_STATUS(XPRSdestroyprob(xpress_model_));
CHECK_STATUS(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) {
CHECK_EQ(vind.size(), vval.size());
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.

CHECK_EQ(ub.size(), num_vars);
CHECK_EQ(vtype.size(), num_vars);
double* c_obj = nullptr;
if (!obj.empty()) {
CHECK_EQ(obj.size(), num_vars);
c_obj = const_cast<double*>(obj.data());
}
if (!vbegin.empty()) {
CHECK_EQ(vbegin.size(), num_vars);
}

CHECK_STATUS(XPRSaddcols(xpress_model_, num_vars, 0, c_obj, nullptr, nullptr,
nullptr, lb.data(), ub.data()));
return absl::OkStatus();
}

absl::Status Xpress::AddConstrs(const absl::Span<const char> sense,
const absl::Span<const double> rhs) {
const int num_cons = static_cast<int>(sense.size());
CHECK_EQ(rhs.size(), num_cons);
return ToStatus(XPRSaddrows(xpress_model_, num_cons, 0, sense.data(),
rhs.data(), NULL, NULL, NULL, NULL));
}

absl::Status Xpress::SetObjective(bool maximize, double offset,
const absl::Span<const int> colind,
const absl::Span<const double> values) {
int n_cols = colind.size();
pet-mit marked this conversation as resolved.
Show resolved Hide resolved
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.

CHECK_STATUS(XPRSchgobj(xpress_model_, n_cols, c_colind, c_values));
static int indexes[1] = {-1};
double xprs_values[1] = {-offset};
CHECK_STATUS(XPRSchgobj(xpress_model_, 1, indexes, xprs_values));
CHECK_STATUS(XPRSchgobjsense(
xpress_model_, maximize ? XPRS_OBJ_MAXIMIZE : XPRS_OBJ_MINIMIZE));
return absl::OkStatus();
}

absl::Status Xpress::ChgCoeffs(absl::Span<const int> rowind,
absl::Span<const int> colind,
absl::Span<const double> values) {
int n_coefs = 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.

CHECK_STATUS(
XPRSchgmcoef(xpress_model_, n_coefs, c_rowind, c_colind, c_values));
return absl::OkStatus();
}

absl::StatusOr<int> Xpress::LpOptimizeAndGetStatus() {
RETURN_IF_ERROR(ToStatus(XPRSlpoptimize(xpress_model_, NULL)))
<< "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_, NULL)))
<< "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 nCols = GetNumberOfColumns();
XPRSgetintattrib(xpress_model_, XPRS_COLS, &nCols);
double values[nCols];
RETURN_IF_ERROR(
ToStatus(XPRSgetlpsol(xpress_model_, values, nullptr, nullptr, nullptr)))
<< "Error getting Xpress LP solution";
std::vector result(values, values + nCols);
pet-mit marked this conversation as resolved.
Show resolved Hide resolved
return std::move(result);
}

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

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

std::vector<double> Xpress::GetConstraintDuals() const {
int nRows = GetNumberOfRows();
double values[nRows];
CHECK_STATUS(XPRSgetlpsol(xpress_model_, nullptr, nullptr, values, nullptr));
return {values, values + nRows};
}
std::vector<double> Xpress::GetReducedCostValues() const {
int nCols = GetNumberOfColumns();
double values[nCols];
CHECK_STATUS(XPRSgetlpsol(xpress_model_, nullptr, nullptr, nullptr, values));
return {values, values + nCols};
}

std::vector<int> Xpress::GetVariableBasis() const {
int const nCols = GetNumberOfColumns();
std::vector<int> basis(nCols);
CHECK_STATUS(XPRSgetbasis(xpress_model_, basis.data(), 0));
return basis;
}

} // namespace operations_research::math_opt
pet-mit marked this conversation as resolved.
Show resolved Hide resolved
Loading