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

Conversation

pet-mit
Copy link
Collaborator

@pet-mit pet-mit commented Nov 28, 2024

No description provided.

ortools/math_opt/solvers/xpress/g_xpress.cc Outdated Show resolved Hide resolved
ortools/math_opt/solvers/xpress/g_xpress.cc Outdated Show resolved Hide resolved
ortools/math_opt/solvers/xpress/g_xpress.cc Outdated Show resolved Hide resolved
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.

ortools/math_opt/solvers/xpress/g_xpress.cc Outdated Show resolved Hide resolved
ortools/math_opt/solvers/xpress_solver_test.cc Outdated Show resolved Hide resolved
ortools/math_opt/solvers/xpress_solver.cc Outdated Show resolved Hide resolved
ortools/math_opt/solvers/xpress_solver.cc Outdated Show resolved Hide resolved
ortools/math_opt/solvers/xpress_solver.cc Outdated Show resolved Hide resolved
ortools/math_opt/solvers/xpress_solver.cc Show resolved Hide resolved
Copy link

@djunglas djunglas left a comment

Choose a reason for hiding this comment

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

Thanks a million for doing this!
I made a first review pass over the implementation (not the tests). In my comments I assume that we want to code against a somewhat recent version of Xpress (that already has XPRSoptmize(), XPRSgetsolution(), XPRSgetduals(), ...).

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.

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));

Copy link

Choose a reason for hiding this comment

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

We should try to only DCHECK if there is a performance reason to do so. In the common case, it is better to either just CHECK, or propagate a status error here. At least that is the convention we attempt to follow throughout the mathopt codebase (we are not 100% consistent).

Do we expect XPRSprob* to be nullptr if XPRScreateprob returns OK? It seems like things are already pretty busted in this case and we can just CHECK.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

you are right, I guess this is redundant and with no actual debug value. I'll remove it.

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());

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.


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.

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 xpress_->AddConstrs(constraint_sense, constraint_rhs, constraint_rng);
}

absl::Status XpressSolver::AddSingleObjective(const ObjectiveProto& objective) {

Choose a reason for hiding this comment

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

What is this function supposed to do? At the moment (unless I am mistaken) it only changes the coefficients of variables that appear in objective. If a variable is not in objective then its objective coefficient won't be changed. In particular, it would not be zeroed. Or is objective always dense?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This function is only called once, in LoadModel, after AddNewVariables. AddNewVariables actually calls XPRSaddcols with a NULL objective (meaning 0 coefficients). Then, in fact, AddSingleObjective only changes updates non-zero coefficients.
(I have not added support for model update between solves yet)

}
// Post-solve
if (!(is_mip_ ? (xpress_status_ == XPRS_MIP_OPTIMAL)
: (xpress_status_ == XPRS_LP_OPTIMAL))) {

Choose a reason for hiding this comment

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

As I commented in the other file, the preferred way to solve is XPRSoptimize() and then just check the solve and solution status returned by this function.

int lp_iter_limit = parameters.has_iteration_limit()
? parameters.iteration_limit()
: 2147483647;
// XPRESS default value for XPRS_BARITERLIMIT is 500

Choose a reason for hiding this comment

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

In order to get the default values, you should rather query the attributes right after calling XPRScreateprob(). This seems more future proof.

}

absl::StatusOr<double> XpressSolver::GetBestDualBound() const {
// TODO: setting LP primal value as best dual bound. Can this be improved?

Choose a reason for hiding this comment

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

There is XPRS_BESTBOUND for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

is it the same? the documentation says this is set by the MIP optimization (I'm looking for LP bound)

Copy link

Choose a reason for hiding this comment

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

I don't know what xpress does, but in general, for LP:

  • primal simplex does not give any dual bound until you have solved the problem to optimality
  • For dual simplex, if you complete phase one and enter phase two, you have a dual feasible solution which gives a dual bound.
  • With barrier, you don't really get anything until optimality, as you don't have primal/dual feasible solutions until the end.

Some solver specific work is typically needed to extract the above information.

Copy link
Collaborator Author

@pet-mit pet-mit Jan 6, 2025

Choose a reason for hiding this comment

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

@rma350 Thank you for your input, in fact my version was too simple. I corrected it in my last commit ("compute dual bound")


bool XpressSolver::isFeasible() const {
if (is_mip_) {
return xpress_status_ == XPRS_MIP_OPTIMAL;

Choose a reason for hiding this comment

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

There is another status that indicates "feasible but not proven optimal": XPRS_MIP_SOLUTION, see 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.

I'm focused on making LP problems work in this first PR, but I'll make this correction since I already started developing MIP support.

@pet-mit
Copy link
Collaborator Author

pet-mit commented Dec 11, 2024

Thanks a million for doing this! I made a first review pass over the implementation (not the tests). In my comments I assume that we want to code against a somewhat recent version of Xpress (that already has XPRSoptmize(), XPRSgetsolution(), XPRSgetduals(), ...).

Hi Daniel!

Thanks a lot for you valuable review!
You are right, we also prefer to target newer versions of XPRESS, and I'm definitely not familiar with the newer vs the older API calls.
I will take all your comments into account; however I'm on my way to my annual holidays until january, so I will do this in 2025 :)

Best regards !
Peter

@pet-mit pet-mit requested review from flomnes and djunglas January 14, 2025 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants