From 6e9ce9f833db29ca773ecdea9432477907357801 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 1 Oct 2025 16:26:52 +0200 Subject: [PATCH 01/70] Support the message callback in Xpress. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 10 +++ ortools/math_opt/solvers/xpress/g_xpress.h | 3 + ortools/math_opt/solvers/xpress_solver.cc | 78 ++++++++++++++++++- .../third_party_solvers/xpress_environment.cc | 2 + .../third_party_solvers/xpress_environment.h | 1 + 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index e94b674dec2..662904b826f 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -84,6 +84,16 @@ void XPRS_CC Xpress::printXpressMessage(XPRSprob, void*, const char* sMsg, int, } } +absl::Status Xpress::addCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata, int prio) +{ + return ToStatus(XPRSaddcbmessage(xpress_model_, cb, cbdata, prio)); +} + +absl::Status Xpress::removeCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata) +{ + return ToStatus(XPRSremovecbmessage(xpress_model_, cb, cbdata)); +} + Xpress::~Xpress() { CHECK_EQ(kXpressOk, XPRSdestroyprob(xpress_model_)); CHECK_EQ(kXpressOk, XPRSfree()); diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 70f5ba74c68..9a8ed59f723 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -110,6 +110,9 @@ class Xpress { const char* sMsg, int nLen, int nMsgLvl); + absl::Status addCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata, int prio = 0); + absl::Status removeCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata = nullptr); + int GetNumberOfConstraints() const; int GetNumberOfVariables() const; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index b94b31763d9..184155e837f 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -35,6 +35,7 @@ #include "ortools/base/status_macros.h" #include "ortools/math_opt/core/inverted_bounds.h" #include "ortools/math_opt/core/math_opt_proto_utils.h" +#include "ortools/math_opt/cpp/message_callback.h" #include "ortools/math_opt/core/solver_interface.h" #include "ortools/math_opt/core/sparse_vector_view.h" #include "ortools/math_opt/solvers/xpress/g_xpress.h" @@ -47,6 +48,73 @@ namespace operations_research { namespace math_opt { namespace { +/** Message callback support. + * Destructor unregisters the callback so that we can rely on RAII for + * cleanup. + */ +class ScopedMessageCallback { + // C-style callback function that is registered with Xpress. + static void XPRS_CC callback(XPRSprob, void *cbdata, char const *msg, int len, int type) { + if ( type != 1 && // info message + type != 3 && // warning message + type != 4 ) // error message + // message type 2 is not used, negative values mean "flush" + return; + ScopedMessageCallback *cb = reinterpret_cast(cbdata); + if ( len == 0 ) { + cb->cb(std::vector{{""}}); + } + else { + std::vector lines; + int start = 0; + // There are a few Xpress messages that span multiple lines. + // The MessageCallback contract says that messages must not contain + // newlines, so we have to split on newline. + while (start <= len) { // <= rather than < to catch message ending in '\n' + int end = start; + while (end < len && msg[end] != '\n') + ++end; + if (start < len) + lines.push_back(std::string(msg, start, end - start)); + else + lines.push_back(""); + start = end + 1; + } + cb->cb(lines); + } + } + Xpress *const prob; /**< Problem with which callback is registered. */ + MessageCallback cb; /**< ortools callback function to which messages are + * forwarded. + */ + bool attached; /**< Whether the callback was attached. */ +public: + /** Install message callback cb into prob. + * If cb is nullptr then nothing happens. + * Callback will be automatically removed by destructor. + */ + ScopedMessageCallback(Xpress *prob, MessageCallback cb) + : prob(prob) + , cb(cb) + , attached(false) + { + } + // This is not part of the constructor so that we can handle errors by + // return value rather than exception. + absl::Status attach() { + if ( !attached && cb ) { + RETURN_IF_ERROR(prob->addCbMessage(callback, reinterpret_cast(this), 0)); + attached = true; + } + return absl::OkStatus(); + } + ~ScopedMessageCallback() { + if ( attached ) { + CHECK_OK(prob->removeCbMessage(callback, reinterpret_cast(this))); + } + } +}; + absl::Status CheckParameters(const SolveParametersProto& parameters) { std::vector warnings; if (parameters.has_threads() && parameters.threads() > 1) { @@ -241,7 +309,8 @@ absl::Status XpressSolver::ChangeCoefficients( absl::StatusOr XpressSolver::Solve( const SolveParametersProto& parameters, - const ModelSolveParametersProto& model_parameters, MessageCallback, + const ModelSolveParametersProto& model_parameters, + MessageCallback message_callback, const CallbackRegistrationProto& callback_registration, Callback, const SolveInterrupter*) { RETURN_IF_ERROR(ModelSolveParametersAreSupported( @@ -253,6 +322,9 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(CheckParameters(parameters)); + ScopedMessageCallback cbMessage(xpress_.get(), message_callback); + RETURN_IF_ERROR(cbMessage.attach()); + // Check that bounds are not inverted just before solve // XPRESS returns "infeasible" when bounds are inverted { @@ -718,8 +790,10 @@ absl::StatusOr XpressSolver::Update(const ModelUpdateProto&) { absl::StatusOr XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, - MessageCallback, + MessageCallback message_callback, const SolveInterrupter*) { + ScopedMessageCallback cbMessage(xpress_.get(), message_callback); + RETURN_IF_ERROR(cbMessage.attach()); return absl::UnimplementedError( "XPRESS does not provide a method to compute an infeasible subsystem"); } diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index b41ca59b08a..4bd3cb716aa 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -106,6 +106,7 @@ std::function XPRSdelobj = nullptr; std::function XPRSaddcbintsol = nullptr; std::function XPRSremovecbintsol = nullptr; std::function XPRSaddcbmessage = nullptr; +std::function XPRSremovecbmessage = nullptr; std::function XPRSlpoptimize = nullptr; std::function XPRSmipoptimize = nullptr; std::function XPRSoptimize = nullptr; @@ -181,6 +182,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSaddcbintsol, "XPRSaddcbintsol"); xpress_dynamic_library->GetFunction(&XPRSremovecbintsol, "XPRSremovecbintsol"); xpress_dynamic_library->GetFunction(&XPRSaddcbmessage, "XPRSaddcbmessage"); + xpress_dynamic_library->GetFunction(&XPRSremovecbmessage, "XPRSremovecbmessage"); xpress_dynamic_library->GetFunction(&XPRSlpoptimize, "XPRSlpoptimize"); xpress_dynamic_library->GetFunction(&XPRSmipoptimize, "XPRSmipoptimize"); xpress_dynamic_library->GetFunction(&XPRSoptimize, "XPRSoptimize"); diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index b8a7bd0a2af..13c8bf0218c 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -539,6 +539,7 @@ extern std::function XPRSdelobj; extern std::function XPRSaddcbintsol; extern std::function XPRSremovecbintsol; extern std::function XPRSaddcbmessage; +extern std::function XPRSremovecbmessage; extern std::function XPRSlpoptimize; extern std::function XPRSmipoptimize; extern std::function XPRSoptimize; From 672be762a3ad045e6f57b50d2a3fd2a8adab69bd Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 1 Oct 2025 16:28:09 +0200 Subject: [PATCH 02/70] Remove the "always on" message handler. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 8 -------- ortools/math_opt/solvers/xpress/g_xpress.h | 4 ---- 2 files changed, 12 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 662904b826f..86025a23e91 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -64,7 +64,6 @@ absl::StatusOr> Xpress::New(absl::string_view) { CHECK(correctlyLoaded); XPRSprob model; CHECK_EQ(kXpressOk, XPRScreateprob(&model)); - CHECK_EQ(kXpressOk, XPRSaddcbmessage(model, printXpressMessage, nullptr, 0)); return absl::WrapUnique(new Xpress(model)); } @@ -77,13 +76,6 @@ absl::Status Xpress::SetProbName(absl::string_view name) { return ToStatus(XPRSsetprobname(xpress_model_, truncated.c_str())); } -void XPRS_CC Xpress::printXpressMessage(XPRSprob, void*, const char* sMsg, int, - int) { - if (sMsg) { - LOG(INFO) << sMsg; - } -} - absl::Status Xpress::addCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata, int prio) { return ToStatus(XPRSaddcbmessage(xpress_model_, cb, cbdata, prio)); diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 9a8ed59f723..5e93727531f 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -106,10 +106,6 @@ class Xpress { absl::Status SetStartingBasis(std::vector& rowBasis, std::vector& colBasis) const; - static void XPRS_CC printXpressMessage(XPRSprob prob, void* data, - const char* sMsg, int nLen, - int nMsgLvl); - absl::Status addCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata, int prio = 0); absl::Status removeCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata = nullptr); From a396c974c49b359330f5c581b33c09ddf4289ac4 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 1 Oct 2025 17:32:31 +0200 Subject: [PATCH 03/70] Mark message callback as supported in test suite. --- ortools/math_opt/solvers/xpress_solver_test.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 23e4c8d767b..1df64dcc038 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -96,7 +96,7 @@ GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(IncrementalLpTest); INSTANTIATE_TEST_SUITE_P(XpressMessageCallbackTest, MessageCallbackTest, testing::Values(MessageCallbackTestParams( SolverType::kXpress, - /*support_message_callback=*/false, + /*support_message_callback=*/true, /*support_interrupter=*/false, /*integer_variables=*/false, ""))); From 17d842f9936b3f0984273d4c14475fcca30393e3 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 2 Oct 2025 10:13:26 +0200 Subject: [PATCH 04/70] Prepare for more callbacks being added and handle exceptions. --- ortools/math_opt/solvers/xpress_solver.cc | 134 ++++++++++++++++------ 1 file changed, 97 insertions(+), 37 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 184155e837f..e3b49d1cf71 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -48,55 +48,108 @@ namespace operations_research { namespace math_opt { namespace { -/** Message callback support. - * Destructor unregisters the callback so that we can rely on RAII for - * cleanup. +/** Capturing of exceptions in callbacks. + * We cannot let exceptions escape from callbacks since that would just + * unroll the stack until some function that catches the exception. + * In particular, it would bypass any cleanup code implemented in the C code + * of the solver. So we must capture exceptions, interrupt the solve and + * handle the exception once the solver returned. */ -class ScopedMessageCallback { - // C-style callback function that is registered with Xpress. - static void XPRS_CC callback(XPRSprob, void *cbdata, char const *msg, int len, int type) { - if ( type != 1 && // info message - type != 3 && // warning message - type != 4 ) // error message - // message type 2 is not used, negative values mean "flush" - return; - ScopedMessageCallback *cb = reinterpret_cast(cbdata); - if ( len == 0 ) { - cb->cb(std::vector{{""}}); +class ExceptionInCallback { + mutable absl::Mutex mutex; + std::exception_ptr ex; +public: + /** Store a new exception from a callback. + * Will not overwrite an existing exception. + */ + void setException(std::exception_ptr exception) { + const absl::MutexLock lock(&mutex); + if ( !ex ) + ex = exception; + } + /** Create a status from the captured exception. + * If there is no exception captured then the status is Ok. + */ + absl::Status toStatus() const { + if ( ex ) { + return util::StatusBuilder(absl::StatusCode::kUnknown) + << "exception in Xpress callback, check log for details "; } else { - std::vector lines; - int start = 0; - // There are a few Xpress messages that span multiple lines. - // The MessageCallback contract says that messages must not contain - // newlines, so we have to split on newline. - while (start <= len) { // <= rather than < to catch message ending in '\n' - int end = start; - while (end < len && msg[end] != '\n') - ++end; - if (start < len) - lines.push_back(std::string(msg, start, end - start)); - else - lines.push_back(""); - start = end + 1; - } - cb->cb(lines); + return absl::OkStatus(); } } - Xpress *const prob; /**< Problem with which callback is registered. */ +}; + +/** Base class for scoped callbacks. */ +class ScopedCallback { +protected: + Xpress *const prob; /**< Problem in which callback is installed. */ + ExceptionInCallback *const ex; /**< Callback exception handler. */ + bool attached; /**< Whether the callback was attached. */ + ScopedCallback(Xpress *prob, ExceptionInCallback *ex) + : prob(prob) + , ex(ex) + , attached(false) + { + } + virtual ~ScopedCallback() {} +}; + +/** Message callback support. + * Destructor unregisters the callback so that we can rely on RAII for + * cleanup. + */ +class ScopedMessageCallback : public ScopedCallback { MessageCallback cb; /**< ortools callback function to which messages are * forwarded. */ - bool attached; /**< Whether the callback was attached. */ + /** C-style callback function that is registered with Xpress. */ + static void XPRS_CC callback(XPRSprob cbprob, void *cbdata, char const *msg, int len, int type) { + ScopedMessageCallback *cb = reinterpret_cast(cbdata); + try { + if ( type == 1 || // info message + type == 3 || // warning message + type == 4 ) // error message + // message type 2 is not used, negative values mean "flush" + { + + if ( len == 0 ) { + cb->cb(std::vector{{""}}); + } + else { + std::vector lines; + int start = 0; + // There are a few Xpress messages that span multiple lines. + // The MessageCallback contract says that messages must not contain + // newlines, so we have to split on newline. + while (start <= len) { // <= rather than < to catch message ending in '\n' + int end = start; + while (end < len && msg[end] != '\n') + ++end; + if (start < len) + lines.push_back(std::string(msg, start, end - start)); + else + lines.push_back(""); + start = end + 1; + } + cb->cb(lines); + } + } + } + catch (...) { + XPRSinterrupt(cbprob, XPRS_STOP_USER); + cb->ex->setException(std::current_exception()); + } + } public: /** Install message callback cb into prob. * If cb is nullptr then nothing happens. * Callback will be automatically removed by destructor. */ - ScopedMessageCallback(Xpress *prob, MessageCallback cb) - : prob(prob) + ScopedMessageCallback(Xpress *prob, ExceptionInCallback *ex, MessageCallback cb) + : ScopedCallback(prob, ex) , cb(cb) - , attached(false) { } // This is not part of the constructor so that we can handle errors by @@ -322,7 +375,9 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(CheckParameters(parameters)); - ScopedMessageCallback cbMessage(xpress_.get(), message_callback); + ExceptionInCallback callbackException; + ScopedMessageCallback cbMessage(xpress_.get(), &callbackException, + message_callback); RETURN_IF_ERROR(cbMessage.attach()); // Check that bounds are not inverted just before solve @@ -340,6 +395,8 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(CallXpressSolve(parameters)) << "Error during XPRESS solve"; + RETURN_IF_ERROR(callbackException.toStatus()); + ASSIGN_OR_RETURN( SolveResultProto solve_result, ExtractSolveResultProto(start, model_parameters, parameters)); @@ -792,8 +849,11 @@ absl::StatusOr XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, MessageCallback message_callback, const SolveInterrupter*) { - ScopedMessageCallback cbMessage(xpress_.get(), message_callback); + ExceptionInCallback callbackException; + ScopedMessageCallback cbMessage(xpress_.get(), &callbackException, + message_callback); RETURN_IF_ERROR(cbMessage.attach()); + RETURN_IF_ERROR(callbackException.toStatus()); return absl::UnimplementedError( "XPRESS does not provide a method to compute an infeasible subsystem"); } From cdf7055a37d111c06232b58f3a92960474101a8e Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 2 Oct 2025 10:16:43 +0200 Subject: [PATCH 05/70] Small code optimization. --- ortools/math_opt/solvers/xpress_solver.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index e3b49d1cf71..911ca3a1da3 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -128,7 +128,7 @@ class ScopedMessageCallback : public ScopedCallback { while (end < len && msg[end] != '\n') ++end; if (start < len) - lines.push_back(std::string(msg, start, end - start)); + lines.emplace_back(std::string(msg, start, end - start)); else lines.push_back(""); start = end + 1; From 54bb914f1929ebd9fd8040b214542fe65961af44 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 2 Oct 2025 11:01:16 +0200 Subject: [PATCH 06/70] Simplify handling errors in callback registration. --- ortools/math_opt/solvers/xpress_solver.cc | 72 ++++++++++++++--------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 911ca3a1da3..d859856405d 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -54,11 +54,20 @@ namespace { * In particular, it would bypass any cleanup code implemented in the C code * of the solver. So we must capture exceptions, interrupt the solve and * handle the exception once the solver returned. + * A captured exception can be reraised either by explicitly calling + * reraise() or by letting the destructor throw it. */ class ExceptionInCallback { - mutable absl::Mutex mutex; - std::exception_ptr ex; + absl::Mutex mutable mutex; + std::exception_ptr mutable ex; public: + /** If there is a captured exception that was not yet reraised then + * it is reraised now. + */ + ~ExceptionInCallback() { + if (ex) + std::rethrow_exception(ex); + } /** Store a new exception from a callback. * Will not overwrite an existing exception. */ @@ -67,20 +76,24 @@ class ExceptionInCallback { if ( !ex ) ex = exception; } - /** Create a status from the captured exception. - * If there is no exception captured then the status is Ok. + /** If there is a captured exception then reraise it now. */ - absl::Status toStatus() const { + void reraise() const { if ( ex ) { - return util::StatusBuilder(absl::StatusCode::kUnknown) - << "exception in Xpress callback, check log for details "; - } - else { - return absl::OkStatus(); + std::exception_ptr old = ex; + ex = nullptr; + std::rethrow_exception(old); } } }; +/** Exception that is raised when registration of a callback failed. */ +class CallbackRegistrationException { +public: + absl::Status error; /**< The error returned from callback registration. */ + CallbackRegistrationException(absl::Status const &error) : error(error) {} +}; + /** Base class for scoped callbacks. */ class ScopedCallback { protected: @@ -151,18 +164,14 @@ class ScopedMessageCallback : public ScopedCallback { : ScopedCallback(prob, ex) , cb(cb) { - } - // This is not part of the constructor so that we can handle errors by - // return value rather than exception. - absl::Status attach() { - if ( !attached && cb ) { - RETURN_IF_ERROR(prob->addCbMessage(callback, reinterpret_cast(this), 0)); - attached = true; + if (cb) { + absl::Status status = prob->addCbMessage(callback, reinterpret_cast(this), 0); + if (status.code() != absl::StatusCode::kOk) + throw CallbackRegistrationException(status); } - return absl::OkStatus(); } ~ScopedMessageCallback() { - if ( attached ) { + if ( cb ) { CHECK_OK(prob->removeCbMessage(callback, reinterpret_cast(this))); } } @@ -376,9 +385,9 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(CheckParameters(parameters)); ExceptionInCallback callbackException; - ScopedMessageCallback cbMessage(xpress_.get(), &callbackException, - message_callback); - RETURN_IF_ERROR(cbMessage.attach()); + try { + ScopedMessageCallback const cbMessage(xpress_.get(), &callbackException, + message_callback); // Check that bounds are not inverted just before solve // XPRESS returns "infeasible" when bounds are inverted @@ -394,14 +403,17 @@ absl::StatusOr XpressSolver::Solve( } RETURN_IF_ERROR(CallXpressSolve(parameters)) << "Error during XPRESS solve"; - - RETURN_IF_ERROR(callbackException.toStatus()); + callbackException.reraise(); ASSIGN_OR_RETURN( SolveResultProto solve_result, ExtractSolveResultProto(start, model_parameters, parameters)); return solve_result; + } + catch (CallbackRegistrationException const &cbRegistrationException) { + return cbRegistrationException.error; + } } std::string XpressSolver::GetLpOptimizationFlags( @@ -850,12 +862,16 @@ XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, MessageCallback message_callback, const SolveInterrupter*) { ExceptionInCallback callbackException; - ScopedMessageCallback cbMessage(xpress_.get(), &callbackException, - message_callback); - RETURN_IF_ERROR(cbMessage.attach()); - RETURN_IF_ERROR(callbackException.toStatus()); + try { + ScopedMessageCallback const cbMessage(xpress_.get(), &callbackException, + message_callback); + return absl::UnimplementedError( "XPRESS does not provide a method to compute an infeasible subsystem"); + } + catch (CallbackRegistrationException const &cbRegistrationException) { + return cbRegistrationException.error; + } } absl::StatusOr XpressSolver::ListInvertedBounds() const { From 242dd6b4ae5f9ac3ea0b89c591229aa3d11c2c9e Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 2 Oct 2025 11:07:39 +0200 Subject: [PATCH 07/70] Apply `clang-format`. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 10 +- ortools/math_opt/solvers/xpress/g_xpress.h | 8 +- ortools/math_opt/solvers/xpress_solver.cc | 169 ++++++++++---------- 3 files changed, 93 insertions(+), 94 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 86025a23e91..22b10be71ec 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -76,13 +76,15 @@ absl::Status Xpress::SetProbName(absl::string_view name) { return ToStatus(XPRSsetprobname(xpress_model_, truncated.c_str())); } -absl::Status Xpress::addCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata, int prio) -{ +absl::Status Xpress::addCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, + char const*, int, int), + void* cbdata, int prio) { return ToStatus(XPRSaddcbmessage(xpress_model_, cb, cbdata, prio)); } -absl::Status Xpress::removeCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata) -{ +absl::Status Xpress::removeCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, + char const*, int, int), + void* cbdata) { return ToStatus(XPRSremovecbmessage(xpress_model_, cb, cbdata)); } diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 5e93727531f..3bd455f9121 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -106,8 +106,12 @@ class Xpress { absl::Status SetStartingBasis(std::vector& rowBasis, std::vector& colBasis) const; - absl::Status addCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata, int prio = 0); - absl::Status removeCbMessage(void (XPRS_CC *cb)(XPRSprob, void *, char const *, int, int), void *cbdata = nullptr); + absl::Status addCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, int, + int), + void* cbdata, int prio = 0); + absl::Status removeCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, + int, int), + void* cbdata = nullptr); int GetNumberOfConstraints() const; int GetNumberOfVariables() const; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index d859856405d..22741c66692 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -35,9 +35,9 @@ #include "ortools/base/status_macros.h" #include "ortools/math_opt/core/inverted_bounds.h" #include "ortools/math_opt/core/math_opt_proto_utils.h" -#include "ortools/math_opt/cpp/message_callback.h" #include "ortools/math_opt/core/solver_interface.h" #include "ortools/math_opt/core/sparse_vector_view.h" +#include "ortools/math_opt/cpp/message_callback.h" #include "ortools/math_opt/solvers/xpress/g_xpress.h" #include "ortools/math_opt/validators/callback_validator.h" #include "ortools/port/proto_utils.h" @@ -60,26 +60,25 @@ namespace { class ExceptionInCallback { absl::Mutex mutable mutex; std::exception_ptr mutable ex; -public: + + public: /** If there is a captured exception that was not yet reraised then * it is reraised now. */ ~ExceptionInCallback() { - if (ex) - std::rethrow_exception(ex); + if (ex) std::rethrow_exception(ex); } /** Store a new exception from a callback. * Will not overwrite an existing exception. */ void setException(std::exception_ptr exception) { const absl::MutexLock lock(&mutex); - if ( !ex ) - ex = exception; + if (!ex) ex = exception; } /** If there is a captured exception then reraise it now. */ void reraise() const { - if ( ex ) { + if (ex) { std::exception_ptr old = ex; ex = nullptr; std::rethrow_exception(old); @@ -89,23 +88,19 @@ class ExceptionInCallback { /** Exception that is raised when registration of a callback failed. */ class CallbackRegistrationException { -public: + public: absl::Status error; /**< The error returned from callback registration. */ - CallbackRegistrationException(absl::Status const &error) : error(error) {} + CallbackRegistrationException(absl::Status const& error) : error(error) {} }; /** Base class for scoped callbacks. */ class ScopedCallback { -protected: - Xpress *const prob; /**< Problem in which callback is installed. */ - ExceptionInCallback *const ex; /**< Callback exception handler. */ + protected: + Xpress* const prob; /**< Problem in which callback is installed. */ + ExceptionInCallback* const ex; /**< Callback exception handler. */ bool attached; /**< Whether the callback was attached. */ - ScopedCallback(Xpress *prob, ExceptionInCallback *ex) - : prob(prob) - , ex(ex) - , attached(false) - { - } + ScopedCallback(Xpress* prob, ExceptionInCallback* ex) + : prob(prob), ex(ex), attached(false) {} virtual ~ScopedCallback() {} }; @@ -114,65 +109,65 @@ class ScopedCallback { * cleanup. */ class ScopedMessageCallback : public ScopedCallback { - MessageCallback cb; /**< ortools callback function to which messages are - * forwarded. - */ + MessageCallback cb; /**< ortools callback function to which messages are + * forwarded. + */ /** C-style callback function that is registered with Xpress. */ - static void XPRS_CC callback(XPRSprob cbprob, void *cbdata, char const *msg, int len, int type) { - ScopedMessageCallback *cb = reinterpret_cast(cbdata); + static void XPRS_CC callback(XPRSprob cbprob, void* cbdata, char const* msg, + int len, int type) { + ScopedMessageCallback* cb = + reinterpret_cast(cbdata); try { - if ( type == 1 || // info message - type == 3 || // warning message - type == 4 ) // error message - // message type 2 is not used, negative values mean "flush" + if (type == 1 || // info message + type == 3 || // warning message + type == 4) // error message + // message type 2 is not used, negative values mean "flush" { - - if ( len == 0 ) { - cb->cb(std::vector{{""}}); - } - else { - std::vector lines; - int start = 0; - // There are a few Xpress messages that span multiple lines. - // The MessageCallback contract says that messages must not contain - // newlines, so we have to split on newline. - while (start <= len) { // <= rather than < to catch message ending in '\n' - int end = start; - while (end < len && msg[end] != '\n') - ++end; - if (start < len) - lines.emplace_back(std::string(msg, start, end - start)); - else - lines.push_back(""); - start = end + 1; - } - cb->cb(lines); - } + if (len == 0) { + cb->cb(std::vector{{""}}); + } else { + std::vector lines; + int start = 0; + // There are a few Xpress messages that span multiple lines. + // The MessageCallback contract says that messages must not contain + // newlines, so we have to split on newline. + while (start <= + len) { // <= rather than < to catch message ending in '\n' + int end = start; + while (end < len && msg[end] != '\n') ++end; + if (start < len) + lines.emplace_back(std::string(msg, start, end - start)); + else + lines.push_back(""); + start = end + 1; + } + cb->cb(lines); + } } - } - catch (...) { + } catch (...) { XPRSinterrupt(cbprob, XPRS_STOP_USER); cb->ex->setException(std::current_exception()); } } -public: + + public: /** Install message callback cb into prob. * If cb is nullptr then nothing happens. * Callback will be automatically removed by destructor. */ - ScopedMessageCallback(Xpress *prob, ExceptionInCallback *ex, MessageCallback cb) - : ScopedCallback(prob, ex) - , cb(cb) - { + ScopedMessageCallback(Xpress* prob, ExceptionInCallback* ex, + MessageCallback cb) + : ScopedCallback(prob, ex), cb(cb) { if (cb) { - absl::Status status = prob->addCbMessage(callback, reinterpret_cast(this), 0); + absl::Status status = + prob->addCbMessage(callback, reinterpret_cast(this), 0); if (status.code() != absl::StatusCode::kOk) - throw CallbackRegistrationException(status); + throw CallbackRegistrationException(status); } } ~ScopedMessageCallback() { - if ( cb ) { - CHECK_OK(prob->removeCbMessage(callback, reinterpret_cast(this))); + if (cb) { + CHECK_OK(prob->removeCbMessage(callback, reinterpret_cast(this))); } } }; @@ -386,32 +381,31 @@ absl::StatusOr XpressSolver::Solve( ExceptionInCallback callbackException; try { - ScopedMessageCallback const cbMessage(xpress_.get(), &callbackException, - message_callback); - - // Check that bounds are not inverted just before solve - // XPRESS returns "infeasible" when bounds are inverted - { - ASSIGN_OR_RETURN(const InvertedBounds inverted_bounds, - ListInvertedBounds()); - RETURN_IF_ERROR(inverted_bounds.ToStatus()); - } + ScopedMessageCallback const cbMessage(xpress_.get(), &callbackException, + message_callback); + + // Check that bounds are not inverted just before solve + // XPRESS returns "infeasible" when bounds are inverted + { + ASSIGN_OR_RETURN(const InvertedBounds inverted_bounds, + ListInvertedBounds()); + RETURN_IF_ERROR(inverted_bounds.ToStatus()); + } - // Set initial basis - if (model_parameters.has_initial_basis()) { - RETURN_IF_ERROR(SetXpressStartingBasis(model_parameters.initial_basis())); - } + // Set initial basis + if (model_parameters.has_initial_basis()) { + RETURN_IF_ERROR(SetXpressStartingBasis(model_parameters.initial_basis())); + } - RETURN_IF_ERROR(CallXpressSolve(parameters)) << "Error during XPRESS solve"; - callbackException.reraise(); + RETURN_IF_ERROR(CallXpressSolve(parameters)) << "Error during XPRESS solve"; + callbackException.reraise(); - ASSIGN_OR_RETURN( - SolveResultProto solve_result, - ExtractSolveResultProto(start, model_parameters, parameters)); + ASSIGN_OR_RETURN( + SolveResultProto solve_result, + ExtractSolveResultProto(start, model_parameters, parameters)); - return solve_result; - } - catch (CallbackRegistrationException const &cbRegistrationException) { + return solve_result; + } catch (CallbackRegistrationException const& cbRegistrationException) { return cbRegistrationException.error; } } @@ -863,13 +857,12 @@ XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, const SolveInterrupter*) { ExceptionInCallback callbackException; try { - ScopedMessageCallback const cbMessage(xpress_.get(), &callbackException, - message_callback); + ScopedMessageCallback const cbMessage(xpress_.get(), &callbackException, + message_callback); - return absl::UnimplementedError( - "XPRESS does not provide a method to compute an infeasible subsystem"); - } - catch (CallbackRegistrationException const &cbRegistrationException) { + return absl::UnimplementedError( + "XPRESS does not provide a method to compute an infeasible subsystem"); + } catch (CallbackRegistrationException const& cbRegistrationException) { return cbRegistrationException.error; } } From a56f7f3b35bee5a45667675f762801b7412b042f Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 2 Oct 2025 16:03:00 +0200 Subject: [PATCH 08/70] Rewrite callback handling. --- ortools/math_opt/solvers/xpress_solver.cc | 218 ++++++++++++++-------- 1 file changed, 143 insertions(+), 75 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 22741c66692..607cfb8a329 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -93,84 +93,150 @@ class CallbackRegistrationException { CallbackRegistrationException(absl::Status const& error) : error(error) {} }; -/** Base class for scoped callbacks. */ +/** Registered callback that is auto-removed in the destructor. + * This implements a singleton pattern for which moving is ok: ownership + * will be transferred to the destination and the source will be marked so + * that it will not remove the callback in the destructor. + */ +template class ScopedCallback { - protected: - Xpress* const prob; /**< Problem in which callback is installed. */ - ExceptionInCallback* const ex; /**< Callback exception handler. */ - bool attached; /**< Whether the callback was attached. */ - ScopedCallback(Xpress* prob, ExceptionInCallback* ex) - : prob(prob), ex(ex), attached(false) {} - virtual ~ScopedCallback() {} +private: + ScopedCallback(ScopedCallback const &) = delete; + ScopedCallback &operator=(ScopedCallback const &) = delete; + ScopedCallback &operator=(ScopedCallback &&) = delete; + Xpress* prob; /**< Where callback is installed. */ + typename T::FunctionType callback; /**< Low-level callback function. */ + absl::Status status; /**< Status from adding the callback. */ +public: + + ExceptionInCallback* ex; /**< Callback exception handler. */ + typename T::DataType callbackData; /**< OR tools callback function. */ + ScopedCallback(Xpress* prob, ExceptionInCallback* ex, + typename T::FunctionType callback, typename T::DataType) + : prob(prob), callback(callback), ex(ex), + callbackData(callbackData) + { + if ( callbackData ) + status = T::add(prob, callback, reinterpret_cast(this)); + else + status = absl::OkStatus(); + } + /** Transfers responsibility for removing the callback from other to this + * instance. + */ + ScopedCallback(ScopedCallback && other) + : prob(other.prob) + , callback(other.callback) + , status(other.status) + , ex(other.ex) + , callbackData(other.callbackData) + { + other.callbackData = nullptr; // Do not remove callback in other's dtor. + } + virtual ~ScopedCallback() { + if (callbackData && status.code() == absl::StatusCode::kOk) + T::remove(prob, callback, reinterpret_cast(this)); + } + absl::Status getStatus() const { return status; } }; -/** Message callback support. - * Destructor unregisters the callback so that we can rely on RAII for - * cleanup. +/** Helper to register a callback and create a guard. + * Returns either an error status or a callback guard that will unregister + * the callback in its destructor. + * This is called from the AddXXXCallback function generated by the + * DEFINE_CALLBACK macro below. */ -class ScopedMessageCallback : public ScopedCallback { - MessageCallback cb; /**< ortools callback function to which messages are - * forwarded. - */ - /** C-style callback function that is registered with Xpress. */ - static void XPRS_CC callback(XPRSprob cbprob, void* cbdata, char const* msg, - int len, int type) { - ScopedMessageCallback* cb = - reinterpret_cast(cbdata); - try { - if (type == 1 || // info message - type == 3 || // warning message - type == 4) // error message - // message type 2 is not used, negative values mean "flush" - { - if (len == 0) { - cb->cb(std::vector{{""}}); - } else { - std::vector lines; - int start = 0; - // There are a few Xpress messages that span multiple lines. - // The MessageCallback contract says that messages must not contain - // newlines, so we have to split on newline. - while (start <= - len) { // <= rather than < to catch message ending in '\n' - int end = start; - while (end < len && msg[end] != '\n') ++end; - if (start < len) - lines.emplace_back(std::string(msg, start, end - start)); - else - lines.push_back(""); - start = end + 1; - } - cb->cb(lines); - } - } - } catch (...) { - XPRSinterrupt(cbprob, XPRS_STOP_USER); - cb->ex->setException(std::current_exception()); - } - } +template +absl::StatusOr> +AddCallback(Xpress *xpress, ExceptionInCallback *ex, + typename T::FunctionType callback, + typename T::DataType callbackData) +{ + ScopedCallback guard(xpress, ex, callback, callbackData); + if (guard.getStatus().code() == absl::StatusCode::kOk) + return std::move(guard); + else + return guard.getStatus(); +} - public: - /** Install message callback cb into prob. - * If cb is nullptr then nothing happens. - * Callback will be automatically removed by destructor. - */ - ScopedMessageCallback(Xpress* prob, ExceptionInCallback* ex, - MessageCallback cb) - : ScopedCallback(prob, ex), cb(cb) { - if (cb) { - absl::Status status = - prob->addCbMessage(callback, reinterpret_cast(this), 0); - if (status.code() != absl::StatusCode::kOk) - throw CallbackRegistrationException(status); - } - } - ~ScopedMessageCallback() { - if (cb) { - CHECK_OK(prob->removeCbMessage(callback, reinterpret_cast(this))); +/** Define everything required for supporting a callback of type name. + * Use like so + * DEFINE_CALLBACK(CallbackName, ORToolsCallback, XpressReturn, (...)) { + * + * } + * where + * CallbackName is the name of the callback (Message, ...) + * ORToolsCallback the OR tools callback we are wrapping + * XpressReturn return type of the low-level Xpress callback + * (...) arguments to the Xpress low-level callback. + * code for the low-level Xpress callback + * The effect of the macro is a function Add##name##Callback that adds + * the callback and returns either an error or a guard that will remove the + * callback in its destructor. Use this like + * ASSIGN_OR_RETURN(auto guard, Add##name##Callback(...)); + */ +#define DEFINE_CALLBACK(name,datatype,ret,args) \ + struct name##Traits { \ + typedef datatype DataType; \ + typedef ret (*FunctionType) args; \ + static absl::Status add(Xpress *xpress, ret (*fptr)args, void *data) { \ + return xpress->addCb##name(fptr, data, 0); \ + } \ + static void remove(Xpress *xpress, ret (*fptr)args, void *data) { \ + CHECK_OK(xpress->removeCb##name(fptr, data)); \ + } \ + }; \ + static ret xpress##name args; \ + auto Add##name##Callback(Xpress *xpress, \ + ExceptionInCallback *ex, \ + datatype callbackArg) \ + { \ + return AddCallback(xpress, ex, \ + xpress##name, callbackArg); \ + } \ + static ret xpress##name args + +/** Define the message callback. */ +DEFINE_CALLBACK(Message, + MessageCallback, + void, + (XPRSprob cbprob, void* cbdata, char const* msg, + int len, int type)) +{ + ScopedCallback *cb = + reinterpret_cast *>(cbdata); + try { + if (type == 1 || // info message + type == 3 || // warning message + type == 4) // error message + // message type 2 is not used by Xpress, negative values mean "flush" + { + if (len == 0) { + cb->callbackData(std::vector{{""}}); + } else { + std::vector lines; + int start = 0; + // There are a few Xpress messages that span multiple lines. + // The MessageCallback contract says that messages must not contain + // newlines, so we have to split on newline. + while (start <= + len) { // <= rather than < to catch message ending in '\n' + int end = start; + while (end < len && msg[end] != '\n') ++end; + if (start < len) + lines.emplace_back(std::string(msg, start, end - start)); + else + lines.push_back(""); + start = end + 1; + } + cb->callbackData(lines); + } } + } catch (...) { + XPRSinterrupt(cbprob, XPRS_STOP_USER); + cb->ex->setException(std::current_exception()); } -}; +} absl::Status CheckParameters(const SolveParametersProto& parameters) { std::vector warnings; @@ -381,8 +447,9 @@ absl::StatusOr XpressSolver::Solve( ExceptionInCallback callbackException; try { - ScopedMessageCallback const cbMessage(xpress_.get(), &callbackException, - message_callback); + ASSIGN_OR_RETURN(auto cbMessage, AddMessageCallback(xpress_.get(), + &callbackException, + message_callback)); // Check that bounds are not inverted just before solve // XPRESS returns "infeasible" when bounds are inverted @@ -857,8 +924,9 @@ XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, const SolveInterrupter*) { ExceptionInCallback callbackException; try { - ScopedMessageCallback const cbMessage(xpress_.get(), &callbackException, - message_callback); + ASSIGN_OR_RETURN(auto cbMessage, AddMessageCallback(xpress_.get(), + &callbackException, + message_callback)); return absl::UnimplementedError( "XPRESS does not provide a method to compute an infeasible subsystem"); From 058892a34804ab820d586dbe0cc9e545bf80287a Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Mon, 6 Oct 2025 13:14:59 +0200 Subject: [PATCH 09/70] Rewrite callback handling again. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 4 + ortools/math_opt/solvers/xpress/g_xpress.h | 2 + ortools/math_opt/solvers/xpress_solver.cc | 275 +++++++++----------- 3 files changed, 133 insertions(+), 148 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 22b10be71ec..c43d2d07693 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -320,4 +320,8 @@ absl::StatusOr> Xpress::GetVarUb() const { return bounds; } +absl::Status Xpress::Interrupt(int reason) { + return ToStatus(XPRSinterrupt(xpress_model_, reason)); +} + } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 3bd455f9121..55d585bc98d 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -119,6 +119,8 @@ class Xpress { absl::StatusOr> GetVarLb() const; absl::StatusOr> GetVarUb() const; + absl::Status Interrupt(int reason); + private: XPRSprob xpress_model_; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 607cfb8a329..eae3e065772 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -48,50 +48,7 @@ namespace operations_research { namespace math_opt { namespace { -/** Capturing of exceptions in callbacks. - * We cannot let exceptions escape from callbacks since that would just - * unroll the stack until some function that catches the exception. - * In particular, it would bypass any cleanup code implemented in the C code - * of the solver. So we must capture exceptions, interrupt the solve and - * handle the exception once the solver returned. - * A captured exception can be reraised either by explicitly calling - * reraise() or by letting the destructor throw it. - */ -class ExceptionInCallback { - absl::Mutex mutable mutex; - std::exception_ptr mutable ex; - - public: - /** If there is a captured exception that was not yet reraised then - * it is reraised now. - */ - ~ExceptionInCallback() { - if (ex) std::rethrow_exception(ex); - } - /** Store a new exception from a callback. - * Will not overwrite an existing exception. - */ - void setException(std::exception_ptr exception) { - const absl::MutexLock lock(&mutex); - if (!ex) ex = exception; - } - /** If there is a captured exception then reraise it now. - */ - void reraise() const { - if (ex) { - std::exception_ptr old = ex; - ex = nullptr; - std::rethrow_exception(old); - } - } -}; - -/** Exception that is raised when registration of a callback failed. */ -class CallbackRegistrationException { - public: - absl::Status error; /**< The error returned from callback registration. */ - CallbackRegistrationException(absl::Status const& error) : error(error) {} -}; +class SolveContext; /** Registered callback that is auto-removed in the destructor. * This implements a singleton pattern for which moving is ok: ownership @@ -102,63 +59,23 @@ template class ScopedCallback { private: ScopedCallback(ScopedCallback const &) = delete; + ScopedCallback(ScopedCallback &&) = delete; ScopedCallback &operator=(ScopedCallback const &) = delete; ScopedCallback &operator=(ScopedCallback &&) = delete; - Xpress* prob; /**< Where callback is installed. */ - typename T::FunctionType callback; /**< Low-level callback function. */ - absl::Status status; /**< Status from adding the callback. */ + SolveContext *context; public: - - ExceptionInCallback* ex; /**< Callback exception handler. */ typename T::DataType callbackData; /**< OR tools callback function. */ - ScopedCallback(Xpress* prob, ExceptionInCallback* ex, - typename T::FunctionType callback, typename T::DataType) - : prob(prob), callback(callback), ex(ex), - callbackData(callbackData) + ScopedCallback() + : context(nullptr) + , callbackData(nullptr) { - if ( callbackData ) - status = T::add(prob, callback, reinterpret_cast(this)); - else - status = absl::OkStatus(); } - /** Transfers responsibility for removing the callback from other to this - * instance. - */ - ScopedCallback(ScopedCallback && other) - : prob(other.prob) - , callback(other.callback) - , status(other.status) - , ex(other.ex) - , callbackData(other.callbackData) - { - other.callbackData = nullptr; // Do not remove callback in other's dtor. - } - virtual ~ScopedCallback() { - if (callbackData && status.code() == absl::StatusCode::kOk) - T::remove(prob, callback, reinterpret_cast(this)); - } - absl::Status getStatus() const { return status; } + inline absl::Status Add(SolveContext *ctx, typename T::DataType data); + inline void Interrupt(int reason); + inline void SetCallbackException(std::exception_ptr ex); + ~ScopedCallback(); }; -/** Helper to register a callback and create a guard. - * Returns either an error status or a callback guard that will unregister - * the callback in its destructor. - * This is called from the AddXXXCallback function generated by the - * DEFINE_CALLBACK macro below. - */ -template -absl::StatusOr> -AddCallback(Xpress *xpress, ExceptionInCallback *ex, - typename T::FunctionType callback, - typename T::DataType callbackData) -{ - ScopedCallback guard(xpress, ex, callback, callbackData); - if (guard.getStatus().code() == absl::StatusCode::kOk) - return std::move(guard); - else - return guard.getStatus(); -} - /** Define everything required for supporting a callback of type name. * Use like so * DEFINE_CALLBACK(CallbackName, ORToolsCallback, XpressReturn, (...)) { @@ -175,26 +92,19 @@ AddCallback(Xpress *xpress, ExceptionInCallback *ex, * callback in its destructor. Use this like * ASSIGN_OR_RETURN(auto guard, Add##name##Callback(...)); */ -#define DEFINE_CALLBACK(name,datatype,ret,args) \ - struct name##Traits { \ - typedef datatype DataType; \ - typedef ret (*FunctionType) args; \ - static absl::Status add(Xpress *xpress, ret (*fptr)args, void *data) { \ - return xpress->addCb##name(fptr, data, 0); \ +#define DEFINE_CALLBACK(name,datatype,ret,args) \ + static ret name##Traits_LowLevelCallback args; \ + struct name##Traits { \ + typedef datatype DataType; \ + static ret LowLevelCallback args; \ + static absl::Status add(Xpress *xpress, void *data) { \ + return xpress->addCb##name(name##Traits_LowLevelCallback, data, 0); \ } \ - static void remove(Xpress *xpress, ret (*fptr)args, void *data) { \ - CHECK_OK(xpress->removeCb##name(fptr, data)); \ + static void remove(Xpress *xpress, void *data) { \ + CHECK_OK(xpress->removeCb##name(name##Traits_LowLevelCallback, data)); \ } \ }; \ - static ret xpress##name args; \ - auto Add##name##Callback(Xpress *xpress, \ - ExceptionInCallback *ex, \ - datatype callbackArg) \ - { \ - return AddCallback(xpress, ex, \ - xpress##name, callbackArg); \ - } \ - static ret xpress##name args + static ret name##Traits_LowLevelCallback args /** Define the message callback. */ DEFINE_CALLBACK(Message, @@ -233,11 +143,89 @@ DEFINE_CALLBACK(Message, } } } catch (...) { - XPRSinterrupt(cbprob, XPRS_STOP_USER); - cb->ex->setException(std::current_exception()); + cb->Interrupt(XPRS_STOP_USER); + cb->SetCallbackException(std::current_exception()); } } +/** Temporary settings for a solve. + * Instances of this class capture settings in the XPRSprob instance that are + * made only temporarily for a solve. + * This includes for example callbacks. + * This is a RAII class that will undo all settings when it goes out of scope. + */ +class SolveContext { + /** Mutex for accessing callbackException. */ + absl::Mutex mutable mutex; + /** Capturing of exceptions in callbacks. + * We cannot let exceptions escape from callbacks since that would just + * unroll the stack until some function that catches the exception. + * In particular, it would bypass any cleanup code implemented in the C code + * of the solver. So we must capture exceptions, interrupt the solve and + * handle the exception once the solver returned. + */ + std::exception_ptr mutable callbackException; + /** Installed message callback (if any). */ + ScopedCallback messageCallback; +public: + Xpress *const xpress; + SolveContext(Xpress *xpress) + : xpress(xpress) + { + } + absl::Status AddCallbacks(MessageCallback message_callback) { + if ( message_callback ) + RETURN_IF_ERROR(messageCallback.Add(this, message_callback)); + return absl::OkStatus(); + } + /** Interrupt the current solve with the given reason. */ + void Interrupt(int reason) { + CHECK_OK(xpress->Interrupt(reason)); + } + /** Reraise any pending exception from a callback. */ + void ReraiseCallbackException() { + if (callbackException) { + std::exception_ptr old = callbackException; + callbackException = nullptr; + std::rethrow_exception(old); + } + } + /** Set exception raised in callback. + * Will not overwrite an existing pending exception. + */ + void SetCallbackException(std::exception_ptr ex) { + const absl::MutexLock lock(&mutex); + if (!callbackException) callbackException = ex; + } + + ~SolveContext() { + // If pending callback exception was not reraised yet then do it now + if (callbackException) std::rethrow_exception(callbackException); + } +}; + +template +absl::Status ScopedCallback::Add(SolveContext *ctx, typename T::DataType data) { + RETURN_IF_ERROR(T::add(ctx->xpress, reinterpret_cast(this))); + callbackData = data; + context = ctx; + return absl::OkStatus(); +} +template +void ScopedCallback::Interrupt(int reason) { + context->Interrupt(reason); +} +template +void ScopedCallback::SetCallbackException(std::exception_ptr ex) { + context->SetCallbackException(ex); +} + +template +ScopedCallback::~ScopedCallback() { + if (context) + T::remove(context->xpress, reinterpret_cast(this)); +} + absl::Status CheckParameters(const SolveParametersProto& parameters) { std::vector warnings; if (parameters.has_threads() && parameters.threads() > 1) { @@ -445,36 +433,30 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(CheckParameters(parameters)); - ExceptionInCallback callbackException; - try { - ASSIGN_OR_RETURN(auto cbMessage, AddMessageCallback(xpress_.get(), - &callbackException, - message_callback)); + SolveContext solveContext(xpress_.get()); + RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback)); - // Check that bounds are not inverted just before solve - // XPRESS returns "infeasible" when bounds are inverted - { - ASSIGN_OR_RETURN(const InvertedBounds inverted_bounds, - ListInvertedBounds()); - RETURN_IF_ERROR(inverted_bounds.ToStatus()); - } + // Check that bounds are not inverted just before solve + // XPRESS returns "infeasible" when bounds are inverted + { + ASSIGN_OR_RETURN(const InvertedBounds inverted_bounds, + ListInvertedBounds()); + RETURN_IF_ERROR(inverted_bounds.ToStatus()); + } - // Set initial basis - if (model_parameters.has_initial_basis()) { - RETURN_IF_ERROR(SetXpressStartingBasis(model_parameters.initial_basis())); - } + // Set initial basis + if (model_parameters.has_initial_basis()) { + RETURN_IF_ERROR(SetXpressStartingBasis(model_parameters.initial_basis())); + } - RETURN_IF_ERROR(CallXpressSolve(parameters)) << "Error during XPRESS solve"; - callbackException.reraise(); + RETURN_IF_ERROR(CallXpressSolve(parameters)) << "Error during XPRESS solve"; + solveContext.ReraiseCallbackException(); - ASSIGN_OR_RETURN( - SolveResultProto solve_result, - ExtractSolveResultProto(start, model_parameters, parameters)); + ASSIGN_OR_RETURN( + SolveResultProto solve_result, + ExtractSolveResultProto(start, model_parameters, parameters)); - return solve_result; - } catch (CallbackRegistrationException const& cbRegistrationException) { - return cbRegistrationException.error; - } + return solve_result; } std::string XpressSolver::GetLpOptimizationFlags( @@ -922,17 +904,14 @@ absl::StatusOr XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, MessageCallback message_callback, const SolveInterrupter*) { - ExceptionInCallback callbackException; - try { - ASSIGN_OR_RETURN(auto cbMessage, AddMessageCallback(xpress_.get(), - &callbackException, - message_callback)); - - return absl::UnimplementedError( - "XPRESS does not provide a method to compute an infeasible subsystem"); - } catch (CallbackRegistrationException const& cbRegistrationException) { - return cbRegistrationException.error; - } + SolveContext solveContext(xpress_.get()); + RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback)); + + return absl::UnimplementedError( + "XPRESS does not provide a method to compute an infeasible subsystem"); + /** TODO: + solveContext.ReraiseCallbackException(); + */ } absl::StatusOr XpressSolver::ListInvertedBounds() const { From f60b696403a609a406cefbbb5f7d1d0c1217eb5d Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Mon, 6 Oct 2025 13:30:39 +0200 Subject: [PATCH 10/70] Add support for interrupters. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 10 ++ ortools/math_opt/solvers/xpress/g_xpress.h | 8 +- ortools/math_opt/solvers/xpress_solver.cc | 103 ++++++++++++++---- .../math_opt/solvers/xpress_solver_test.cc | 2 +- .../third_party_solvers/xpress_environment.cc | 4 + .../third_party_solvers/xpress_environment.h | 2 + 6 files changed, 103 insertions(+), 26 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index c43d2d07693..4f620bb944a 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -88,6 +88,16 @@ absl::Status Xpress::removeCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, return ToStatus(XPRSremovecbmessage(xpress_model_, cb, cbdata)); } +absl::Status Xpress::addCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), + void* cbdata, int prio) { + return ToStatus(XPRSaddcbchecktime(xpress_model_, cb, cbdata, prio)); +} + +absl::Status Xpress::removeCbMessage(int(XPRS_CC* cb)(XPRSprob, void*), + void* cbdata) { + return ToStatus(XPRSremovecbchecktime(xpress_model_, cb, cbdata)); +} + Xpress::~Xpress() { CHECK_EQ(kXpressOk, XPRSdestroyprob(xpress_model_)); CHECK_EQ(kXpressOk, XPRSfree()); diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 55d585bc98d..a7bef7f2b3f 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -106,12 +106,16 @@ class Xpress { absl::Status SetStartingBasis(std::vector& rowBasis, std::vector& colBasis) const; - absl::Status addCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, int, + absl::Status AddCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, int, int), void* cbdata, int prio = 0); - absl::Status removeCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, + absl::Status RemoveCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, int, int), void* cbdata = nullptr); + absl::Status AddCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), + void* cbdata, int prio = 0); + absl::Status RemoveCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), + void* cbdata = nullptr); int GetNumberOfConstraints() const; int GetNumberOfVariables() const; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index eae3e065772..24ea08e2453 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -51,9 +51,10 @@ namespace { class SolveContext; /** Registered callback that is auto-removed in the destructor. - * This implements a singleton pattern for which moving is ok: ownership - * will be transferred to the destination and the source will be marked so - * that it will not remove the callback in the destructor. + * Use Add() to add a callback to a solve context. + * The class also provides convenience functions SetCallbackException() + * and Interrupt() that are required in every callback implementation to + * capture exceptions from user code and reraise them appropriately. */ template class ScopedCallback { @@ -78,15 +79,17 @@ class ScopedCallback { /** Define everything required for supporting a callback of type name. * Use like so - * DEFINE_CALLBACK(CallbackName, ORToolsCallback, XpressReturn, (...)) { + * DEFINE_CALLBACK(CallbackName, ORToolsData, XpressReturn, (...)) { * * } * where - * CallbackName is the name of the callback (Message, ...) - * ORToolsCallback the OR tools callback we are wrapping - * XpressReturn return type of the low-level Xpress callback - * (...) arguments to the Xpress low-level callback. - * code for the low-level Xpress callback + * CallbackName is the name of the callback (Message, Checktime, ...) + * ORToolsData the OR tools data that is required to forward the + * callback invocation from the low-level Xpress callback + * to OR tools. + * XpressReturn return type of the low-level Xpress callback + * (...) arguments to the Xpress low-level callback. + * code for the low-level Xpress callback * The effect of the macro is a function Add##name##Callback that adds * the callback and returns either an error or a guard that will remove the * callback in its destructor. Use this like @@ -96,17 +99,18 @@ class ScopedCallback { static ret name##Traits_LowLevelCallback args; \ struct name##Traits { \ typedef datatype DataType; \ - static ret LowLevelCallback args; \ - static absl::Status add(Xpress *xpress, void *data) { \ - return xpress->addCb##name(name##Traits_LowLevelCallback, data, 0); \ + static absl::Status Add(Xpress *xpress, void *data) { \ + return xpress->AddCb##name(name##Traits_LowLevelCallback, data, 0); \ } \ - static void remove(Xpress *xpress, void *data) { \ - CHECK_OK(xpress->removeCb##name(name##Traits_LowLevelCallback, data)); \ + static void Remove(Xpress *xpress, void *data) { \ + CHECK_OK(xpress->RemoveCb##name(name##Traits_LowLevelCallback, data)); \ } \ }; \ static ret name##Traits_LowLevelCallback args -/** Define the message callback. */ +/** Define the message callback. + * This forwards messages from Xpress to an ortools message callback. + */ DEFINE_CALLBACK(Message, MessageCallback, void, @@ -148,6 +152,24 @@ DEFINE_CALLBACK(Message, } } +/** Define the checktime callback. + * This callbacks checks an interrupter for whether the solve was interrupted. + */ +DEFINE_CALLBACK(Checktime, + SolveInterrupter const *, + int, + (XPRSprob cbprob, void* cbdata)) { + ScopedCallback *cb = + reinterpret_cast *>(cbdata); + try { + return cb->callbackData->IsInterrupted() ? 1 : 0; + } catch (...) { + cb->Interrupt(XPRS_STOP_USER); + cb->SetCallbackException(std::current_exception()); + return 1; + } +} + /** Temporary settings for a solve. * Instances of this class capture settings in the XPRSprob instance that are * made only temporarily for a solve. @@ -167,17 +189,50 @@ class SolveContext { std::exception_ptr mutable callbackException; /** Installed message callback (if any). */ ScopedCallback messageCallback; + /** Installed interrupter (if any). */ + ScopedCallback checktimeCallback; + /** If we installed an interrupter callback then this removes it. */ + std::function removeInterrupterCallback; public: Xpress *const xpress; SolveContext(Xpress *xpress) : xpress(xpress) + , removeInterrupterCallback(nullptr) { } - absl::Status AddCallbacks(MessageCallback message_callback) { + absl::Status AddCallbacks(MessageCallback message_callback, + const SolveInterrupter *interrupter) { if ( message_callback ) RETURN_IF_ERROR(messageCallback.Add(this, message_callback)); - return absl::OkStatus(); + if ( interrupter ) { + /* To be extra safe we add two ways to interrupt Xpress: + * 1. We register a checktime callback that polls the interrupter. + * 2. We register a callback with the interrupter that will call + * XPRSinterrupt(). + * Eventually we should assess whether the first thing is a performance + * hit and if so, remove it. + */ + RETURN_IF_ERROR(checktimeCallback.Add(this, interrupter)); + SolveInterrupter::CallbackId const id = interrupter->AddInterruptionCallback([=]{ + CHECK_OK(xpress->Interrupt(XPRS_STOP_USER)); + }); + removeInterrupterCallback = [=]{ interrupter->RemoveInterruptionCallback(id); }; + /** TODO: Support + * CallbackRegistrationProto and Callback and install the + * ortools callback as required. + * Note that this is only for Solve(), not for + * ComputeInfeasibleSubsystem() + */ + } + return absl::OkStatus(); } + /** TODO: Implement this. + * absl::Status ApplyParameters(const SolveParametersProto& parameters); + */ + /** TODO: Implement this (only for Solve(), not for + * ComputeInfeasibleSubsystem()) + * absl::Status ApplyParameters(const ModelSolveParametersProto& model_parameters); + */ /** Interrupt the current solve with the given reason. */ void Interrupt(int reason) { CHECK_OK(xpress->Interrupt(reason)); @@ -199,6 +254,8 @@ class SolveContext { } ~SolveContext() { + if (removeInterrupterCallback) + removeInterrupterCallback(); // If pending callback exception was not reraised yet then do it now if (callbackException) std::rethrow_exception(callbackException); } @@ -206,7 +263,7 @@ class SolveContext { template absl::Status ScopedCallback::Add(SolveContext *ctx, typename T::DataType data) { - RETURN_IF_ERROR(T::add(ctx->xpress, reinterpret_cast(this))); + RETURN_IF_ERROR(T::Add(ctx->xpress, reinterpret_cast(this))); callbackData = data; context = ctx; return absl::OkStatus(); @@ -223,7 +280,7 @@ void ScopedCallback::SetCallbackException(std::exception_ptr ex) { template ScopedCallback::~ScopedCallback() { if (context) - T::remove(context->xpress, reinterpret_cast(this)); + T::Remove(context->xpress, reinterpret_cast(this)); } absl::Status CheckParameters(const SolveParametersProto& parameters) { @@ -423,7 +480,7 @@ absl::StatusOr XpressSolver::Solve( const ModelSolveParametersProto& model_parameters, MessageCallback message_callback, const CallbackRegistrationProto& callback_registration, Callback, - const SolveInterrupter*) { + const SolveInterrupter* interrupter) { RETURN_IF_ERROR(ModelSolveParametersAreSupported( model_parameters, kXpressSupportedStructures, "XPRESS")); const absl::Time start = absl::Now(); @@ -434,7 +491,7 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(CheckParameters(parameters)); SolveContext solveContext(xpress_.get()); - RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback)); + RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); // Check that bounds are not inverted just before solve // XPRESS returns "infeasible" when bounds are inverted @@ -903,9 +960,9 @@ absl::StatusOr XpressSolver::Update(const ModelUpdateProto&) { absl::StatusOr XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, MessageCallback message_callback, - const SolveInterrupter*) { + const SolveInterrupter* interrupter) { SolveContext solveContext(xpress_.get()); - RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback)); + RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); return absl::UnimplementedError( "XPRESS does not provide a method to compute an infeasible subsystem"); diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 1df64dcc038..2b5c4da7705 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -97,7 +97,7 @@ INSTANTIATE_TEST_SUITE_P(XpressMessageCallbackTest, MessageCallbackTest, testing::Values(MessageCallbackTestParams( SolverType::kXpress, /*support_message_callback=*/true, - /*support_interrupter=*/false, + /*support_interrupter=*/true, /*integer_variables=*/false, ""))); INSTANTIATE_TEST_SUITE_P( diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index 4bd3cb716aa..5fac4ce9834 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -107,6 +107,8 @@ std::function XPRSremovecbintsol = nullptr; std::function XPRSaddcbmessage = nullptr; std::function XPRSremovecbmessage = nullptr; +std::function XPRSaddcbchecktime = nullptr; +std::function XPRSremovecbchecktime = nullptr; std::function XPRSlpoptimize = nullptr; std::function XPRSmipoptimize = nullptr; std::function XPRSoptimize = nullptr; @@ -183,6 +185,8 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSremovecbintsol, "XPRSremovecbintsol"); xpress_dynamic_library->GetFunction(&XPRSaddcbmessage, "XPRSaddcbmessage"); xpress_dynamic_library->GetFunction(&XPRSremovecbmessage, "XPRSremovecbmessage"); + xpress_dynamic_library->GetFunction(&XPRSaddcbchecktime, "XPRSaddcbchecktime"); + xpress_dynamic_library->GetFunction(&XPRSremovecbchecktime, "XPRSremovecbchecktime"); xpress_dynamic_library->GetFunction(&XPRSlpoptimize, "XPRSlpoptimize"); xpress_dynamic_library->GetFunction(&XPRSmipoptimize, "XPRSmipoptimize"); xpress_dynamic_library->GetFunction(&XPRSoptimize, "XPRSoptimize"); diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index 13c8bf0218c..501c55cb703 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -540,6 +540,8 @@ extern std::function XPRSremovecbintsol; extern std::function XPRSaddcbmessage; extern std::function XPRSremovecbmessage; +extern std::function XPRSaddcbchecktime; +extern std::function XPRSremovecbchecktime; extern std::function XPRSlpoptimize; extern std::function XPRSmipoptimize; extern std::function XPRSoptimize; From 19b3f5c7baca99e303cb774381156fdf74e1b81f Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Mon, 6 Oct 2025 13:50:11 +0200 Subject: [PATCH 11/70] Formatting. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 2 +- ortools/math_opt/solvers/xpress/g_xpress.h | 6 +- ortools/math_opt/solvers/xpress_solver.cc | 178 ++++++++++---------- 3 files changed, 89 insertions(+), 97 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 4f620bb944a..9ab528beb0d 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -89,7 +89,7 @@ absl::Status Xpress::removeCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, } absl::Status Xpress::addCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), - void* cbdata, int prio) { + void* cbdata, int prio) { return ToStatus(XPRSaddcbchecktime(xpress_model_, cb, cbdata, prio)); } diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index a7bef7f2b3f..6d6744a2baa 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -112,10 +112,10 @@ class Xpress { absl::Status RemoveCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, int, int), void* cbdata = nullptr); - absl::Status AddCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), - void* cbdata, int prio = 0); + absl::Status AddCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), void* cbdata, + int prio = 0); absl::Status RemoveCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), - void* cbdata = nullptr); + void* cbdata = nullptr); int GetNumberOfConstraints() const; int GetNumberOfVariables() const; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 24ea08e2453..548dd70c1db 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -48,7 +48,7 @@ namespace operations_research { namespace math_opt { namespace { -class SolveContext; +class SolveContext; // forward /** Registered callback that is auto-removed in the destructor. * Use Add() to add a callback to a solve context. @@ -56,22 +56,21 @@ class SolveContext; * and Interrupt() that are required in every callback implementation to * capture exceptions from user code and reraise them appropriately. */ -template +template class ScopedCallback { -private: - ScopedCallback(ScopedCallback const &) = delete; - ScopedCallback(ScopedCallback &&) = delete; - ScopedCallback &operator=(ScopedCallback const &) = delete; - ScopedCallback &operator=(ScopedCallback &&) = delete; - SolveContext *context; -public: - typename T::DataType callbackData; /**< OR tools callback function. */ - ScopedCallback() - : context(nullptr) - , callbackData(nullptr) - { - } - inline absl::Status Add(SolveContext *ctx, typename T::DataType data); + private: + ScopedCallback(ScopedCallback const&) = delete; + ScopedCallback(ScopedCallback&&) = delete; + ScopedCallback& operator=(ScopedCallback const&) = delete; + ScopedCallback& operator=(ScopedCallback&&) = delete; + SolveContext* context; + + public: + typename T::DataType callbackData; /**< OR tools callback function. */ + ScopedCallback() : context(nullptr), callbackData(nullptr) {} + // Below functions cannot be defined here since they need the definition + // of SolveContext available. + inline absl::Status Add(SolveContext* ctx, typename T::DataType data); inline void Interrupt(int reason); inline void SetCallbackException(std::exception_ptr ex); ~ScopedCallback(); @@ -95,55 +94,52 @@ class ScopedCallback { * callback in its destructor. Use this like * ASSIGN_OR_RETURN(auto guard, Add##name##Callback(...)); */ -#define DEFINE_CALLBACK(name,datatype,ret,args) \ - static ret name##Traits_LowLevelCallback args; \ - struct name##Traits { \ - typedef datatype DataType; \ - static absl::Status Add(Xpress *xpress, void *data) { \ - return xpress->AddCb##name(name##Traits_LowLevelCallback, data, 0); \ - } \ - static void Remove(Xpress *xpress, void *data) { \ +#define DEFINE_CALLBACK(name, datatype, ret, args) \ + static ret name##Traits_LowLevelCallback args; \ + struct name##Traits { \ + typedef datatype DataType; \ + static absl::Status Add(Xpress* xpress, void* data) { \ + return xpress->AddCb##name(name##Traits_LowLevelCallback, data, 0); \ + } \ + static void Remove(Xpress* xpress, void* data) { \ CHECK_OK(xpress->RemoveCb##name(name##Traits_LowLevelCallback, data)); \ - } \ - }; \ + } \ + }; \ static ret name##Traits_LowLevelCallback args /** Define the message callback. * This forwards messages from Xpress to an ortools message callback. */ -DEFINE_CALLBACK(Message, - MessageCallback, - void, - (XPRSprob cbprob, void* cbdata, char const* msg, - int len, int type)) -{ - ScopedCallback *cb = - reinterpret_cast *>(cbdata); +DEFINE_CALLBACK(Message, MessageCallback, void, + (XPRSprob cbprob, void* cbdata, char const* msg, int len, + int type)) { + ScopedCallback* cb = + reinterpret_cast*>(cbdata); try { if (type == 1 || // info message - type == 3 || // warning message - type == 4) // error message - // message type 2 is not used by Xpress, negative values mean "flush" + type == 3 || // warning message + type == 4) // error message + // message type 2 is not used by Xpress, negative values mean "flush" { if (len == 0) { - cb->callbackData(std::vector{{""}}); + cb->callbackData(std::vector{{""}}); } else { - std::vector lines; - int start = 0; - // There are a few Xpress messages that span multiple lines. - // The MessageCallback contract says that messages must not contain - // newlines, so we have to split on newline. - while (start <= - len) { // <= rather than < to catch message ending in '\n' - int end = start; - while (end < len && msg[end] != '\n') ++end; - if (start < len) - lines.emplace_back(std::string(msg, start, end - start)); - else - lines.push_back(""); - start = end + 1; - } - cb->callbackData(lines); + std::vector lines; + int start = 0; + // There are a few Xpress messages that span multiple lines. + // The MessageCallback contract says that messages must not contain + // newlines, so we have to split on newline. + while (start <= + len) { // <= rather than < to catch message ending in '\n' + int end = start; + while (end < len && msg[end] != '\n') ++end; + if (start < len) + lines.emplace_back(std::string(msg, start, end - start)); + else + lines.push_back(""); + start = end + 1; + } + cb->callbackData(lines); } } } catch (...) { @@ -155,12 +151,10 @@ DEFINE_CALLBACK(Message, /** Define the checktime callback. * This callbacks checks an interrupter for whether the solve was interrupted. */ -DEFINE_CALLBACK(Checktime, - SolveInterrupter const *, - int, - (XPRSprob cbprob, void* cbdata)) { - ScopedCallback *cb = - reinterpret_cast *>(cbdata); +DEFINE_CALLBACK(Checktime, SolveInterrupter const*, int, + (XPRSprob cbprob, void* cbdata)) { + ScopedCallback* cb = + reinterpret_cast*>(cbdata); try { return cb->callbackData->IsInterrupted() ? 1 : 0; } catch (...) { @@ -193,18 +187,16 @@ class SolveContext { ScopedCallback checktimeCallback; /** If we installed an interrupter callback then this removes it. */ std::function removeInterrupterCallback; -public: - Xpress *const xpress; - SolveContext(Xpress *xpress) - : xpress(xpress) - , removeInterrupterCallback(nullptr) - { - } + + public: + Xpress* const xpress; + SolveContext(Xpress* xpress) + : xpress(xpress), removeInterrupterCallback(nullptr) {} absl::Status AddCallbacks(MessageCallback message_callback, - const SolveInterrupter *interrupter) { - if ( message_callback ) + const SolveInterrupter* interrupter) { + if (message_callback) RETURN_IF_ERROR(messageCallback.Add(this, message_callback)); - if ( interrupter ) { + if (interrupter) { /* To be extra safe we add two ways to interrupt Xpress: * 1. We register a checktime callback that polls the interrupter. * 2. We register a callback with the interrupter that will call @@ -213,10 +205,12 @@ class SolveContext { * hit and if so, remove it. */ RETURN_IF_ERROR(checktimeCallback.Add(this, interrupter)); - SolveInterrupter::CallbackId const id = interrupter->AddInterruptionCallback([=]{ - CHECK_OK(xpress->Interrupt(XPRS_STOP_USER)); - }); - removeInterrupterCallback = [=]{ interrupter->RemoveInterruptionCallback(id); }; + SolveInterrupter::CallbackId const id = + interrupter->AddInterruptionCallback( + [=] { CHECK_OK(xpress->Interrupt(XPRS_STOP_USER)); }); + removeInterrupterCallback = [=] { + interrupter->RemoveInterruptionCallback(id); + }; /** TODO: Support * CallbackRegistrationProto and Callback and install the * ortools callback as required. @@ -231,12 +225,11 @@ class SolveContext { */ /** TODO: Implement this (only for Solve(), not for * ComputeInfeasibleSubsystem()) - * absl::Status ApplyParameters(const ModelSolveParametersProto& model_parameters); + * absl::Status ApplyParameters(const ModelSolveParametersProto& + * model_parameters); */ /** Interrupt the current solve with the given reason. */ - void Interrupt(int reason) { - CHECK_OK(xpress->Interrupt(reason)); - } + void Interrupt(int reason) { CHECK_OK(xpress->Interrupt(reason)); } /** Reraise any pending exception from a callback. */ void ReraiseCallbackException() { if (callbackException) { @@ -254,33 +247,32 @@ class SolveContext { } ~SolveContext() { - if (removeInterrupterCallback) - removeInterrupterCallback(); + if (removeInterrupterCallback) removeInterrupterCallback(); // If pending callback exception was not reraised yet then do it now if (callbackException) std::rethrow_exception(callbackException); } }; -template -absl::Status ScopedCallback::Add(SolveContext *ctx, typename T::DataType data) { - RETURN_IF_ERROR(T::Add(ctx->xpress, reinterpret_cast(this))); +template +absl::Status ScopedCallback::Add(SolveContext* ctx, + typename T::DataType data) { + RETURN_IF_ERROR(T::Add(ctx->xpress, reinterpret_cast(this))); callbackData = data; context = ctx; - return absl::OkStatus(); + return absl::OkStatus(); } -template +template void ScopedCallback::Interrupt(int reason) { context->Interrupt(reason); } -template +template void ScopedCallback::SetCallbackException(std::exception_ptr ex) { context->SetCallbackException(ex); } -template +template ScopedCallback::~ScopedCallback() { - if (context) - T::Remove(context->xpress, reinterpret_cast(this)); + if (context) T::Remove(context->xpress, reinterpret_cast(this)); } absl::Status CheckParameters(const SolveParametersProto& parameters) { @@ -497,7 +489,7 @@ absl::StatusOr XpressSolver::Solve( // XPRESS returns "infeasible" when bounds are inverted { ASSIGN_OR_RETURN(const InvertedBounds inverted_bounds, - ListInvertedBounds()); + ListInvertedBounds()); RETURN_IF_ERROR(inverted_bounds.ToStatus()); } @@ -510,8 +502,8 @@ absl::StatusOr XpressSolver::Solve( solveContext.ReraiseCallbackException(); ASSIGN_OR_RETURN( - SolveResultProto solve_result, - ExtractSolveResultProto(start, model_parameters, parameters)); + SolveResultProto solve_result, + ExtractSolveResultProto(start, model_parameters, parameters)); return solve_result; } @@ -965,7 +957,7 @@ XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); return absl::UnimplementedError( - "XPRESS does not provide a method to compute an infeasible subsystem"); + "XPRESS does not provide a method to compute an infeasible subsystem"); /** TODO: solveContext.ReraiseCallbackException(); */ From be015674732f4ca19756920c5ba24654794ad463 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 7 Oct 2025 15:52:22 +0200 Subject: [PATCH 12/70] Fix compilation problems. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 9ab528beb0d..53d3948f8cb 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -76,25 +76,25 @@ absl::Status Xpress::SetProbName(absl::string_view name) { return ToStatus(XPRSsetprobname(xpress_model_, truncated.c_str())); } -absl::Status Xpress::addCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, +absl::Status Xpress::AddCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, int, int), void* cbdata, int prio) { return ToStatus(XPRSaddcbmessage(xpress_model_, cb, cbdata, prio)); } -absl::Status Xpress::removeCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, +absl::Status Xpress::RemoveCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, int, int), void* cbdata) { return ToStatus(XPRSremovecbmessage(xpress_model_, cb, cbdata)); } -absl::Status Xpress::addCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), +absl::Status Xpress::AddCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), void* cbdata, int prio) { return ToStatus(XPRSaddcbchecktime(xpress_model_, cb, cbdata, prio)); } -absl::Status Xpress::removeCbMessage(int(XPRS_CC* cb)(XPRSprob, void*), - void* cbdata) { +absl::Status Xpress::RemoveCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), + void* cbdata) { return ToStatus(XPRSremovecbchecktime(xpress_model_, cb, cbdata)); } From 1c32a13cc736369ae6176640a9f7561d33bac7c6 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 7 Oct 2025 15:52:57 +0200 Subject: [PATCH 13/70] clang-format. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 2 +- ortools/math_opt/solvers/xpress_solver.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 53d3948f8cb..8b3af95b56a 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -94,7 +94,7 @@ absl::Status Xpress::AddCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), } absl::Status Xpress::RemoveCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), - void* cbdata) { + void* cbdata) { return ToStatus(XPRSremovecbchecktime(xpress_model_, cb, cbdata)); } diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 548dd70c1db..5ba6a01c9d2 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -48,7 +48,7 @@ namespace operations_research { namespace math_opt { namespace { -class SolveContext; // forward +class SolveContext; // forward /** Registered callback that is auto-removed in the destructor. * Use Add() to add a callback to a solve context. From cea9f0fda0c183e166286cddc98404c7a9cc3443 Mon Sep 17 00:00:00 2001 From: Francesco Cavaliere Date: Fri, 10 Oct 2025 15:31:19 +0200 Subject: [PATCH 14/70] Review refactor, needs comments --- ortools/math_opt/solvers/xpress_solver.cc | 301 +++++++++++----------- 1 file changed, 151 insertions(+), 150 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 5ba6a01c9d2..77ad1d5d9be 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -48,7 +49,21 @@ namespace operations_research { namespace math_opt { namespace { -class SolveContext; // forward +struct SharedSolveContext { + Xpress* xpress; + + /** Mutex for accessing callbackException. */ + absl::Mutex mutex; + + /** Capturing of exceptions in callbacks. + * We cannot let exceptions escape from callbacks since that would just + * unroll the stack until some function that catches the exception. + * In particular, it would bypass any cleanup code implemented in the C code + * of the solver. So we must capture exceptions, interrupt the solve and + * handle the exception once the solver returned. + */ + std::exception_ptr callbackException; +}; /** Registered callback that is auto-removed in the destructor. * Use Add() to add a callback to a solve context. @@ -56,112 +71,142 @@ class SolveContext; // forward * and Interrupt() that are required in every callback implementation to * capture exceptions from user code and reraise them appropriately. */ -template +template class ScopedCallback { - private: - ScopedCallback(ScopedCallback const&) = delete; - ScopedCallback(ScopedCallback&&) = delete; - ScopedCallback& operator=(ScopedCallback const&) = delete; - ScopedCallback& operator=(ScopedCallback&&) = delete; - SolveContext* context; + using proto_type = typename ProtoT::proto_type; + SharedSolveContext* ctx; + + ScopedCallback(ScopedCallback const&) = delete; + ScopedCallback(ScopedCallback&&) = delete; + ScopedCallback& operator=(ScopedCallback const&) = delete; + ScopedCallback& operator=(ScopedCallback&&) = delete; + + // We intercept and store any exception throw by a callback defining a static + // wrapper function that invokes the callback within a try/carch block. For + // this to work, we need to deduce the callback return type and arguments. + template + struct ExWrapper; + + // Specialization to deduce the callback return and arguments types + template + struct ExWrapper { + // The static function that will be directly invoked by Xpress + static auto low_level_cb(XPRSprob prob, void* cbdata, Args... args) try { + return ProtoT::glueFn(prob, cbdata, args...); + } catch (...) { + // Catch any exception and terminate Xpress gracefully + ScopedCallback* cb = reinterpret_cast(cbdata); + cb->Interrupt(XPRS_STOP_USER); + cb->SetCallbackException(std::current_exception()); + if constexpr (std::is_convertible_v) return static_cast(1); + } + }; + static constexpr proto_type low_level_cb = ExWrapper::low_level_cb; public: - typename T::DataType callbackData; /**< OR tools callback function. */ - ScopedCallback() : context(nullptr), callbackData(nullptr) {} - // Below functions cannot be defined here since they need the definition - // of SolveContext available. - inline absl::Status Add(SolveContext* ctx, typename T::DataType data); - inline void Interrupt(int reason); - inline void SetCallbackException(std::exception_ptr ex); - ~ScopedCallback(); + CbT or_tools_cb; + + ScopedCallback() : ctx(nullptr) {} + + inline absl::Status Add(SharedSolveContext* context, CbT cb) { + ctx = context; + RETURN_IF_ERROR( + ProtoT::Add(ctx->xpress, low_level_cb, reinterpret_cast(this))); + return absl::OkStatus(); + } + + inline void Interrupt(int reason) { + CHECK_OK(ctx->xpress->Interrupt(reason)); + } + + inline void SetCallbackException(std::exception_ptr ex) { + const absl::MutexLock lock(&ctx->mutex); + if (!ctx->callbackException) ctx->callbackException = ex; + } + + ~ScopedCallback() { + if (ctx) + ProtoT::Remove(ctx->xpress, low_level_cb, reinterpret_cast(this)); + } }; /** Define everything required for supporting a callback of type name. * Use like so - * DEFINE_CALLBACK(CallbackName, ORToolsData, XpressReturn, (...)) { + * DEFINE_SCOPED_CB(CB_NAME, ORTOOLS_CB, CB_RET_TYPE, (...ARGS)) { * * } * where - * CallbackName is the name of the callback (Message, Checktime, ...) - * ORToolsData the OR tools data that is required to forward the - * callback invocation from the low-level Xpress callback - * to OR tools. - * XpressReturn return type of the low-level Xpress callback - * (...) arguments to the Xpress low-level callback. - * code for the low-level Xpress callback - * The effect of the macro is a function Add##name##Callback that adds - * the callback and returns either an error or a guard that will remove the - * callback in its destructor. Use this like - * ASSIGN_OR_RETURN(auto guard, Add##name##Callback(...)); + * CB_NAME is the name of the callback (Message, Checktime, ...) + * ORTOOLS_CB the Or-Tools callbacks (function object) that get provided + * to the low-level static callback as user data, and then + * invoked. + * CB_RET_TYPE return type of the low-level Xpress callback. + * (...ARGS) arguments to the Xpress low-level callback. + * code for the low-level Xpress callback + * The effect of the macro is an alias CB_NAME####ScopedCb = ScopedCallback<...>. */ -#define DEFINE_CALLBACK(name, datatype, ret, args) \ - static ret name##Traits_LowLevelCallback args; \ - struct name##Traits { \ - typedef datatype DataType; \ - static absl::Status Add(Xpress* xpress, void* data) { \ - return xpress->AddCb##name(name##Traits_LowLevelCallback, data, 0); \ - } \ - static void Remove(Xpress* xpress, void* data) { \ - CHECK_OK(xpress->RemoveCb##name(name##Traits_LowLevelCallback, data)); \ - } \ - }; \ - static ret name##Traits_LowLevelCallback args +#define DEFINE_SCOPED_CB(CB_NAME, ORTOOLS_CB, CB_RET_TYPE, ARGS) \ + CB_RET_TYPE CB_NAME##GlueFn ARGS; \ + struct CB_NAME##Traits { \ + using proto_type = CB_RET_TYPE(XPRS_CC*) ARGS; \ + static constexpr proto_type glueFn = CB_NAME##GlueFn; \ + static absl::Status Add(Xpress* xpress, proto_type fn, void* data) { \ + return xpress->AddCb##CB_NAME(fn, data, 0); \ + } \ + static void Remove(Xpress* xpress, proto_type fn, void* data) { \ + CHECK_OK(xpress->RemoveCb##CB_NAME(fn, data)); \ + } \ + }; \ + using CB_NAME##ScopedCb = ScopedCallback; \ + CB_RET_TYPE CB_NAME##GlueFn ARGS /** Define the message callback. * This forwards messages from Xpress to an ortools message callback. */ -DEFINE_CALLBACK(Message, MessageCallback, void, - (XPRSprob cbprob, void* cbdata, char const* msg, int len, - int type)) { - ScopedCallback* cb = - reinterpret_cast*>(cbdata); - try { - if (type == 1 || // info message - type == 3 || // warning message - type == 4) // error message +DEFINE_SCOPED_CB(Message, MessageCallback, void, + (XPRSprob prob, void* cbdata, char const* msg, int len, + int type)) { + auto cb = reinterpret_cast(cbdata); + + if (type != 1 && // info message + type != 3 && // warning message + type != 4) { // error message // message type 2 is not used by Xpress, negative values mean "flush" - { - if (len == 0) { - cb->callbackData(std::vector{{""}}); - } else { - std::vector lines; - int start = 0; - // There are a few Xpress messages that span multiple lines. - // The MessageCallback contract says that messages must not contain - // newlines, so we have to split on newline. - while (start <= - len) { // <= rather than < to catch message ending in '\n' - int end = start; - while (end < len && msg[end] != '\n') ++end; - if (start < len) - lines.emplace_back(std::string(msg, start, end - start)); - else - lines.push_back(""); - start = end + 1; - } - cb->callbackData(lines); - } + return; + } + + if (len == 0) { + cb->or_tools_cb(std::vector{""}); + return; + } + + std::vector lines; + int start = 0; + // There are a few Xpress messages that span multiple lines. + // The MessageCallback contract says that messages must not contain + // newlines, so we have to split on newline. + while (start <= len) { // <= rather than < to catch message ending in '\n' + int end = start; + while (end < len && msg[end] != '\n') { + ++end; + } + if (start < len) { + lines.emplace_back(msg, start, end - start); + } else { + lines.push_back(""); } - } catch (...) { - cb->Interrupt(XPRS_STOP_USER); - cb->SetCallbackException(std::current_exception()); + start = end + 1; } + cb->or_tools_cb(lines); } /** Define the checktime callback. * This callbacks checks an interrupter for whether the solve was interrupted. */ -DEFINE_CALLBACK(Checktime, SolveInterrupter const*, int, - (XPRSprob cbprob, void* cbdata)) { - ScopedCallback* cb = - reinterpret_cast*>(cbdata); - try { - return cb->callbackData->IsInterrupted() ? 1 : 0; - } catch (...) { - cb->Interrupt(XPRS_STOP_USER); - cb->SetCallbackException(std::current_exception()); - return 1; - } +DEFINE_SCOPED_CB(Checktime, SolveInterrupter const*, int, + (XPRSprob prob, void* cbdata)) { + auto cb = reinterpret_cast(cbdata); + return cb->or_tools_cb->IsInterrupted() ? 1 : 0; } /** Temporary settings for a solve. @@ -170,32 +215,24 @@ DEFINE_CALLBACK(Checktime, SolveInterrupter const*, int, * This includes for example callbacks. * This is a RAII class that will undo all settings when it goes out of scope. */ -class SolveContext { - /** Mutex for accessing callbackException. */ - absl::Mutex mutable mutex; - /** Capturing of exceptions in callbacks. - * We cannot let exceptions escape from callbacks since that would just - * unroll the stack until some function that catches the exception. - * In particular, it would bypass any cleanup code implemented in the C code - * of the solver. So we must capture exceptions, interrupt the solve and - * handle the exception once the solver returned. - */ - std::exception_ptr mutable callbackException; +class ScopedSolverContext { + /** Solver context data shared by callbacks */ + SharedSolveContext shared_ctx; /** Installed message callback (if any). */ - ScopedCallback messageCallback; + MessageScopedCb messageCallback; /** Installed interrupter (if any). */ - ScopedCallback checktimeCallback; + ChecktimeScopedCb checktimeCallback; /** If we installed an interrupter callback then this removes it. */ std::function removeInterrupterCallback; public: - Xpress* const xpress; - SolveContext(Xpress* xpress) - : xpress(xpress), removeInterrupterCallback(nullptr) {} + ScopedSolverContext(Xpress* xpress) : removeInterrupterCallback(nullptr) { + shared_ctx.xpress = xpress; + } absl::Status AddCallbacks(MessageCallback message_callback, const SolveInterrupter* interrupter) { if (message_callback) - RETURN_IF_ERROR(messageCallback.Add(this, message_callback)); + RETURN_IF_ERROR(messageCallback.Add(&shared_ctx, message_callback)); if (interrupter) { /* To be extra safe we add two ways to interrupt Xpress: * 1. We register a checktime callback that polls the interrupter. @@ -204,10 +241,10 @@ class SolveContext { * Eventually we should assess whether the first thing is a performance * hit and if so, remove it. */ - RETURN_IF_ERROR(checktimeCallback.Add(this, interrupter)); + RETURN_IF_ERROR(checktimeCallback.Add(&shared_ctx, interrupter)); SolveInterrupter::CallbackId const id = interrupter->AddInterruptionCallback( - [=] { CHECK_OK(xpress->Interrupt(XPRS_STOP_USER)); }); + [=] { CHECK_OK(shared_ctx.xpress->Interrupt(XPRS_STOP_USER)); }); removeInterrupterCallback = [=] { interrupter->RemoveInterruptionCallback(id); }; @@ -228,53 +265,15 @@ class SolveContext { * absl::Status ApplyParameters(const ModelSolveParametersProto& * model_parameters); */ - /** Interrupt the current solve with the given reason. */ - void Interrupt(int reason) { CHECK_OK(xpress->Interrupt(reason)); } - /** Reraise any pending exception from a callback. */ - void ReraiseCallbackException() { - if (callbackException) { - std::exception_ptr old = callbackException; - callbackException = nullptr; - std::rethrow_exception(old); - } - } - /** Set exception raised in callback. - * Will not overwrite an existing pending exception. - */ - void SetCallbackException(std::exception_ptr ex) { - const absl::MutexLock lock(&mutex); - if (!callbackException) callbackException = ex; - } - ~SolveContext() { + ~ScopedSolverContext() { if (removeInterrupterCallback) removeInterrupterCallback(); // If pending callback exception was not reraised yet then do it now - if (callbackException) std::rethrow_exception(callbackException); + if (shared_ctx.callbackException) + std::rethrow_exception(shared_ctx.callbackException); } }; -template -absl::Status ScopedCallback::Add(SolveContext* ctx, - typename T::DataType data) { - RETURN_IF_ERROR(T::Add(ctx->xpress, reinterpret_cast(this))); - callbackData = data; - context = ctx; - return absl::OkStatus(); -} -template -void ScopedCallback::Interrupt(int reason) { - context->Interrupt(reason); -} -template -void ScopedCallback::SetCallbackException(std::exception_ptr ex) { - context->SetCallbackException(ex); -} - -template -ScopedCallback::~ScopedCallback() { - if (context) T::Remove(context->xpress, reinterpret_cast(this)); -} - absl::Status CheckParameters(const SolveParametersProto& parameters) { std::vector warnings; if (parameters.has_threads() && parameters.threads() > 1) { @@ -482,9 +481,6 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(CheckParameters(parameters)); - SolveContext solveContext(xpress_.get()); - RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); - // Check that bounds are not inverted just before solve // XPRESS returns "infeasible" when bounds are inverted { @@ -498,8 +494,13 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(SetXpressStartingBasis(model_parameters.initial_basis())); } - RETURN_IF_ERROR(CallXpressSolve(parameters)) << "Error during XPRESS solve"; - solveContext.ReraiseCallbackException(); + // Register callbacks and create scoped context to automatically if an + // exception has been thrown during optimization. + { + ScopedSolverContext solveContext(xpress_.get()); + RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); + RETURN_IF_ERROR(CallXpressSolve(parameters)) << "Error during XPRESS solve"; + } ASSIGN_OR_RETURN( SolveResultProto solve_result, @@ -953,7 +954,7 @@ absl::StatusOr XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, MessageCallback message_callback, const SolveInterrupter* interrupter) { - SolveContext solveContext(xpress_.get()); + ScopedSolverContext solveContext(xpress_.get()); RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); return absl::UnimplementedError( From 6e1c8d1908fb80738cf93424adb5da1f4f0dbd0a Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Mon, 13 Oct 2025 10:07:13 +0200 Subject: [PATCH 15/70] clang-format. --- ortools/math_opt/solvers/xpress_solver.cc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 77ad1d5d9be..c68da1368d4 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -73,7 +73,7 @@ struct SharedSolveContext { */ template class ScopedCallback { - using proto_type = typename ProtoT::proto_type; + using proto_type = typename ProtoT::proto_type; SharedSolveContext* ctx; ScopedCallback(ScopedCallback const&) = delete; @@ -101,7 +101,8 @@ class ScopedCallback { if constexpr (std::is_convertible_v) return static_cast(1); } }; - static constexpr proto_type low_level_cb = ExWrapper::low_level_cb; + static constexpr proto_type low_level_cb = + ExWrapper::low_level_cb; public: CbT or_tools_cb; @@ -143,7 +144,8 @@ class ScopedCallback { * CB_RET_TYPE return type of the low-level Xpress callback. * (...ARGS) arguments to the Xpress low-level callback. * code for the low-level Xpress callback - * The effect of the macro is an alias CB_NAME####ScopedCb = ScopedCallback<...>. + * The effect of the macro is an alias CB_NAME####ScopedCb = + * ScopedCallback<...>. */ #define DEFINE_SCOPED_CB(CB_NAME, ORTOOLS_CB, CB_RET_TYPE, ARGS) \ CB_RET_TYPE CB_NAME##GlueFn ARGS; \ From 75e8cd5727114fa87d2b29faeb128d299bb7471e Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 7 Oct 2025 06:57:54 +0200 Subject: [PATCH 16/70] Add missing control handling functions. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 44 +++++++++++++++++++++ ortools/math_opt/solvers/xpress/g_xpress.h | 11 ++++++ 2 files changed, 55 insertions(+) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 8b3af95b56a..f4a22c56b87 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -236,6 +236,11 @@ absl::Status Xpress::MipOptimize() { void Xpress::Terminate() { XPRSinterrupt(xpress_model_, XPRS_STOP_USER); }; +absl::Status Xpress::GetControlInfo(char const* name, int* p_id, + int* p_type) const { + return ToStatus(XPRSgetcontrolinfo(xpress_model_, name, p_id, p_type)); +} + absl::StatusOr Xpress::GetIntControl(int control) const { int result; RETURN_IF_ERROR(ToStatus(XPRSgetintcontrol(xpress_model_, control, &result))) @@ -257,6 +262,45 @@ absl::Status Xpress::ResetIntControl(int control) { ", consider adding it to Xpress::initIntControlDefaults"); } +absl::StatusOr Xpress::GetIntControl64(int control) const { + XPRSint64 result; + RETURN_IF_ERROR( + ToStatus(XPRSgetintcontrol64(xpress_model_, control, &result))) + << "Error getting Xpress int64 control: " << control; + return result; +} + +absl::Status Xpress::SetIntControl64(int control, long long value) { + return ToStatus(XPRSsetintcontrol64(xpress_model_, control, value)); +} + +absl::StatusOr Xpress::GetDblControl(int control) const { + double result; + RETURN_IF_ERROR(ToStatus(XPRSgetdblcontrol(xpress_model_, control, &result))) + << "Error getting Xpress double control: " << control; + return result; +} + +absl::Status Xpress::SetDblControl(int control, double value) { + return ToStatus(XPRSsetdblcontrol(xpress_model_, control, value)); +} + +absl::StatusOr Xpress::GetStrControl(int control) const { + int nbytes; + RETURN_IF_ERROR( + ToStatus(XPRSgetstringcontrol(xpress_model_, control, NULL, 0, &nbytes))); + std::vector result(nbytes, + '\0'); // nbytes CONTAINS the terminating nul! + RETURN_IF_ERROR(ToStatus(XPRSgetstringcontrol( + xpress_model_, control, result.data(), nbytes, &nbytes))) + << "Error getting Xpress string control: " << control; + return std::string(result.data(), nbytes); +} + +absl::Status Xpress::SetStrControl(int control, std::string const& value) { + return ToStatus(XPRSsetstrcontrol(xpress_model_, control, value.c_str())); +} + absl::StatusOr Xpress::GetIntAttr(int attribute) const { int result; RETURN_IF_ERROR(ToStatus(XPRSgetintattrib(xpress_model_, attribute, &result))) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 6d6744a2baa..63328059ace 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -49,10 +49,21 @@ class Xpress { ~Xpress(); + absl::Status GetControlInfo(char const* name, int* p_id, int* p_type) const; + absl::StatusOr GetIntControl(int control) const; absl::Status SetIntControl(int control, int value); absl::Status ResetIntControl(int control); // reset to default value + absl::StatusOr GetIntControl64(int control) const; + absl::Status SetIntControl64(int control, long long value); + + absl::StatusOr GetDblControl(int control) const; + absl::Status SetDblControl(int control, double value); + + absl::StatusOr GetStrControl(int control) const; + absl::Status SetStrControl(int control, std::string const& value); + absl::StatusOr GetIntAttr(int attribute) const; absl::StatusOr GetDoubleAttr(int attribute) const; From ecd3a2d58131495a5f046a4797df2aa6ba04ebc7 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 7 Oct 2025 07:03:03 +0200 Subject: [PATCH 17/70] Support temporary setting of controls. --- ortools/math_opt/solvers/xpress_solver.cc | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index c68da1368d4..2e5385f50a8 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -226,6 +226,16 @@ class ScopedSolverContext { ChecktimeScopedCb checktimeCallback; /** If we installed an interrupter callback then this removes it. */ std::function removeInterrupterCallback; + /** A single control that must be reset in the destructor. */ + struct OneControl { + enum { INT_CONTROL, DBL_CONTROL, STR_CONTROL } type; + int id; + long long l; + double d; + std::string s; + }; + /** Controls to be reset in the destructor. */ + std::vector modifiedControls; public: ScopedSolverContext(Xpress* xpress) : removeInterrupterCallback(nullptr) { @@ -269,11 +279,45 @@ class ScopedSolverContext { */ ~ScopedSolverContext() { + ~SolveContext() { + for (auto it = modifiedControls.rbegin(); it != modifiedControls.rend(); + ++it) { + switch (it->type) { + case OneControl::INT_CONTROL: + CHECK_OK(xpress->SetIntControl64(it->id, it->l)); + break; + case OneControl::DBL_CONTROL: + CHECK_OK(xpress->SetDblControl(it->id, it->d)); + break; + case OneControl::STR_CONTROL: + CHECK_OK(xpress->SetStrControl(it->id, it->s.c_str())); + break; + } + } if (removeInterrupterCallback) removeInterrupterCallback(); // If pending callback exception was not reraised yet then do it now if (shared_ctx.callbackException) std::rethrow_exception(shared_ctx.callbackException); } + + absl::Status set(int id, long long const& value) { + ASSIGN_OR_RETURN(long long old, xpress->GetIntControl64(id)); + modifiedControls.push_back({OneControl::INT_CONTROL, id, old, 0.0, ""}); + RETURN_IF_ERROR(xpress->SetIntControl64(id, value)); + return absl::OkStatus(); + } + absl::Status set(int id, double const& value) { + ASSIGN_OR_RETURN(double old, xpress->GetDblControl(id)); + modifiedControls.push_back({OneControl::DBL_CONTROL, id, 0LL, old, ""}); + RETURN_IF_ERROR(xpress->SetDblControl(id, value)); + return absl::OkStatus(); + } + absl::Status set(int id, std::string const& value) { + ASSIGN_OR_RETURN(std::string old, xpress->GetStrControl(id)); + modifiedControls.push_back({OneControl::STR_CONTROL, id, 0LL, 0.0, old}); + RETURN_IF_ERROR(xpress->SetStrControl(id, value)); + return absl::OkStatus(); + } }; absl::Status CheckParameters(const SolveParametersProto& parameters) { From a08695f059972b80cbc38804c6421c61edf40b2c Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 7 Oct 2025 08:07:46 +0200 Subject: [PATCH 18/70] Switch to `XPRSoptimize()` . --- ortools/math_opt/cpp/parameters.h | 1 + ortools/math_opt/solvers/xpress/g_xpress.cc | 24 +- ortools/math_opt/solvers/xpress/g_xpress.h | 11 +- ortools/math_opt/solvers/xpress_solver.cc | 544 ++++++++++++------ ortools/math_opt/solvers/xpress_solver.h | 11 +- .../third_party_solvers/xpress_environment.cc | 2 + .../third_party_solvers/xpress_environment.h | 33 +- 7 files changed, 449 insertions(+), 177 deletions(-) diff --git a/ortools/math_opt/cpp/parameters.h b/ortools/math_opt/cpp/parameters.h index 190d2ba1080..9cf7d7692b1 100644 --- a/ortools/math_opt/cpp/parameters.h +++ b/ortools/math_opt/cpp/parameters.h @@ -347,6 +347,7 @@ struct SolveParameters { // - Gurobi: [0:GRB_MAXINT] (which as of Gurobi 9.0 is 2x10^9). // - GSCIP: [0:2147483647] (which is MAX_INT or kint32max or 2^31-1). // - GLOP: [0:2147483647] (same as above) + // - Xpress: Any 32bit signed integer is allowed // In all cases, the solver will receive a value equal to: // MAX(0, MIN(MAX_VALID_VALUE_FOR_SOLVER, random_seed)). std::optional random_seed; diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index f4a22c56b87..3c33c53c972 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -234,6 +234,12 @@ absl::Status Xpress::MipOptimize() { return ToStatus(XPRSmipoptimize(xpress_model_, nullptr)); } +absl::Status Xpress::Optimize(std::string const& flags, int* p_solvestatus, + int* p_solstatus) { + return ToStatus( + XPRSoptimize(xpress_model_, flags.c_str(), p_solvestatus, p_solstatus)); +} + void Xpress::Terminate() { XPRSinterrupt(xpress_model_, XPRS_STOP_USER); }; absl::Status Xpress::GetControlInfo(char const* name, int* p_id, @@ -262,7 +268,7 @@ absl::Status Xpress::ResetIntControl(int control) { ", consider adding it to Xpress::initIntControlDefaults"); } -absl::StatusOr Xpress::GetIntControl64(int control) const { +absl::StatusOr Xpress::GetIntControl64(int control) const { XPRSint64 result; RETURN_IF_ERROR( ToStatus(XPRSgetintcontrol64(xpress_model_, control, &result))) @@ -270,7 +276,7 @@ absl::StatusOr Xpress::GetIntControl64(int control) const { return result; } -absl::Status Xpress::SetIntControl64(int control, long long value) { +absl::Status Xpress::SetIntControl64(int control, int64_t value) { return ToStatus(XPRSsetintcontrol64(xpress_model_, control, value)); } @@ -378,4 +384,18 @@ absl::Status Xpress::Interrupt(int reason) { return ToStatus(XPRSinterrupt(xpress_model_, reason)); } +absl::StatusOr Xpress::IsMIP() const { + ASSIGN_OR_RETURN(auto ents, GetIntAttr(XPRS_MIPENTS)); + return ents != 0; /** TODO: Check for preintsol callback? */ +} + +absl::Status Xpress::GetDuals(int* p_status, double* duals, int first, + int last) { + return ToStatus(XPRSgetduals(xpress_model_, p_status, duals, first, last)); +} +absl::Status Xpress::GetSolution(int* p_status, absl::Span x, int first, + int last) { + return ToStatus(XPRSgetsolution(xpress_model_, p_status, x, first, last)); +} + } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 63328059ace..d99a5bd3b2b 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -55,8 +55,8 @@ class Xpress { absl::Status SetIntControl(int control, int value); absl::Status ResetIntControl(int control); // reset to default value - absl::StatusOr GetIntControl64(int control) const; - absl::Status SetIntControl64(int control, long long value); + absl::StatusOr GetIntControl64(int control) const; + absl::Status SetIntControl64(int control, int64_t value); absl::StatusOr GetDblControl(int control) const; absl::Status SetDblControl(int control, double value); @@ -107,6 +107,9 @@ class Xpress { absl::Status GetLpSol(absl::Span primals, absl::Span duals, absl::Span reducedCosts); absl::Status MipOptimize(); + absl::Status Optimize(std::string const& flags = "", + int* p_solvestatus = nullptr, + int* p_solstatus = nullptr); absl::Status PostSolve(); void Terminate(); @@ -136,6 +139,10 @@ class Xpress { absl::Status Interrupt(int reason); + absl::StatusOr IsMIP() const; + absl::Status GetDuals(int* p_status, double* duals, int first, int last); + absl::Status GetSolution(int* p_status, double* x, int first, int last); + private: XPRSprob xpress_model_; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 2e5385f50a8..74c907f67dc 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -211,6 +211,11 @@ DEFINE_SCOPED_CB(Checktime, SolveInterrupter const*, int, return cb->or_tools_cb->IsInterrupted() ? 1 : 0; } +/** An ortools message callback that prints everything to stdout. */ +static void stdoutMessageCallback(std::vector const& lines) { + for (auto& l : lines) std::cout << l << std::endl; +} + /** Temporary settings for a solve. * Instances of this class capture settings in the XPRSprob instance that are * made only temporarily for a solve. @@ -230,7 +235,7 @@ class ScopedSolverContext { struct OneControl { enum { INT_CONTROL, DBL_CONTROL, STR_CONTROL } type; int id; - long long l; + int64_t l; double d; std::string s; }; @@ -269,9 +274,145 @@ class ScopedSolverContext { } return absl::OkStatus(); } - /** TODO: Implement this. - * absl::Status ApplyParameters(const SolveParametersProto& parameters); - */ + /** Setup model specific parameters. */ + absl::Status ApplyParameters(const SolveParametersProto& parameters, + MessageCallback message_callback) { + std::vector warnings; + ASSIGN_OR_RETURN(bool const isMIP, xpress->IsMIP()); + if (parameters.enable_output()) { + // This is considered only if no message callback is set. + if (!message_callback) { + RETURN_IF_ERROR(messageCallback.Add(this, stdoutMessageCallback)); + } + } + absl::Duration time_limit = absl::InfiniteDuration(); + if (parameters.has_time_limit()) { + ASSIGN_OR_RETURN( + time_limit, util_time::DecodeGoogleApiProto(parameters.time_limit())); + } + if (time_limit < absl::InfiniteDuration()) { + RETURN_IF_ERROR(Set(XPRS_TIMELIMIT, absl::ToDoubleSeconds(time_limit))); + } + if (parameters.has_iteration_limit()) { + RETURN_IF_ERROR(Set(XPRS_LPITERLIMIT, parameters.iteration_limit())); + RETURN_IF_ERROR(Set(XPRS_BARITERLIMIT, parameters.iteration_limit())); + } + if (parameters.has_node_limit()) { + RETURN_IF_ERROR(Set(XPRS_MAXNODE, parameters.node_limit())); + } + if (parameters.has_cutoff_limit()) { + RETURN_IF_ERROR(Set(XPRS_MIPABSCUTOFF, parameters.cutoff_limit())); + } + if (parameters.has_objective_limit()) { + /** TODO */ + } + if (parameters.has_best_bound_limit()) { + /** TODO */ + } + if (parameters.has_solution_limit()) { + RETURN_IF_ERROR(Set(XPRS_MAXMIPSOL, parameters.solution_limit())); + } + if (parameters.has_threads() && parameters.threads() > 1) + RETURN_IF_ERROR(Set(XPRS_THREADS, parameters.threads())); + if (parameters.has_random_seed()) { + RETURN_IF_ERROR(Set(XPRS_RANDOMSEED, parameters.random_seed())); + } + if (parameters.has_absolute_gap_tolerance()) + RETURN_IF_ERROR( + Set(XPRS_MIPABSSTOP, parameters.absolute_gap_tolerance())); + if (parameters.has_relative_gap_tolerance()) + RETURN_IF_ERROR( + Set(XPRS_MIPRELSTOP, parameters.relative_gap_tolerance())); + if (parameters.has_solution_pool_size()) { + warnings.emplace_back("XpressSolver does not support solution_pool_size"); + } + // According to the documentation, LP algorithm is only for LPs + if (!isMIP && parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) { + switch (parameters.lp_algorithm()) { + case LP_ALGORITHM_PRIMAL_SIMPLEX: + RETURN_IF_ERROR(Set(XPRS_LPFLAGS, 1 << 1)); + break; + case LP_ALGORITHM_DUAL_SIMPLEX: + RETURN_IF_ERROR(Set(XPRS_LPFLAGS, 1 << 0)); + break; + case LP_ALGORITHM_BARRIER: + RETURN_IF_ERROR(Set(XPRS_LPFLAGS, 1 << 2)); + break; + case LP_ALGORITHM_FIRST_ORDER: + RETURN_IF_ERROR(Set(XPRS_LPFLAGS, 1 << 2)); + RETURN_IF_ERROR(Set(XPRS_BARALG, 4)); + break; + // Note: Xpress also supports network simplex, but that is not + // supported by ortools. + } + } + if (parameters.presolve() != EMPHASIS_UNSPECIFIED) { + // default value for XPRS_PRESOLVEPASSES is 1 + int presolve = -1; + switch (parameters.presolve()) { + case EMPHASIS_OFF: + break; + case EMPHASIS_UNSPECIFIED: + presolve = 2; + break; + case EMPHASIS_LOW: + presolve = 3; + break; + case EMPHASIS_MEDIUM: + presolve = 4; + break; + case EMPHASIS_HIGH: + presolve = 5; + break; + case EMPHASIS_VERY_HIGH: + presolve = 6; + break; + } + if (presolve > 0) RETURN_IF_ERROR(Set(XPRS_PRESOLVEPASSES, presolve)); + } + if (parameters.cuts() != EMPHASIS_UNSPECIFIED) { + switch (parameters.cuts()) { + case EMPHASIS_OFF: + RETURN_IF_ERROR(Set(XPRS_GOMCUTS, 0)); + RETURN_IF_ERROR(Set(XPRS_TREEGOMCUTS, 0)); + RETURN_IF_ERROR(Set(XPRS_COVERCUTS, 0)); + RETURN_IF_ERROR(Set(XPRS_TREECOVERCUTS, 0)); + break; + case EMPHASIS_UNSPECIFIED: /** TODO */ + break; + case EMPHASIS_LOW: /** TODO */ + break; + case EMPHASIS_MEDIUM: /** TODO */ + break; + case EMPHASIS_HIGH: /** TODO */ + break; + case EMPHASIS_VERY_HIGH: /** TODO */ + break; + } + } + if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) { + switch (parameters.heuristics()) { + case EMPHASIS_OFF: + break; + case EMPHASIS_UNSPECIFIED: + break; + case EMPHASIS_LOW: // fallthrough + case EMPHASIS_MEDIUM: + RETURN_IF_ERROR(Set(XPRS_HEUREMPHASIS, 1)); + break; + case EMPHASIS_HIGH: // fallthrough + case EMPHASIS_VERY_HIGH: + RETURN_IF_ERROR(Set(XPRS_HEUREMPHASIS, 2)); + break; + } + } + /** TODO: Add XpressParameters to structure and apply settings. */ + + if (!warnings.empty()) { + return absl::InvalidArgumentError(absl::StrJoin(warnings, "; ")); + } + return absl::OkStatus(); + } /** TODO: Implement this (only for Solve(), not for * ComputeInfeasibleSubsystem()) * absl::Status ApplyParameters(const ModelSolveParametersProto& @@ -300,19 +441,22 @@ class ScopedSolverContext { std::rethrow_exception(shared_ctx.callbackException); } - absl::Status set(int id, long long const& value) { - ASSIGN_OR_RETURN(long long old, xpress->GetIntControl64(id)); + absl::Status Set(int id, int32_t const& value) { + return Set(id, int64_t(value)); + } + absl::Status Set(int id, int64_t const& value) { + ASSIGN_OR_RETURN(int64_t old, xpress->GetIntControl64(id)); modifiedControls.push_back({OneControl::INT_CONTROL, id, old, 0.0, ""}); RETURN_IF_ERROR(xpress->SetIntControl64(id, value)); return absl::OkStatus(); } - absl::Status set(int id, double const& value) { + absl::Status Set(int id, double const& value) { ASSIGN_OR_RETURN(double old, xpress->GetDblControl(id)); modifiedControls.push_back({OneControl::DBL_CONTROL, id, 0LL, old, ""}); RETURN_IF_ERROR(xpress->SetDblControl(id, value)); return absl::OkStatus(); } - absl::Status set(int id, std::string const& value) { + absl::Status Set(int id, std::string const& value) { ASSIGN_OR_RETURN(std::string old, xpress->GetStrControl(id)); modifiedControls.push_back({OneControl::STR_CONTROL, id, 0LL, 0.0, old}); RETURN_IF_ERROR(xpress->SetStrControl(id, value)); @@ -320,35 +464,27 @@ class ScopedSolverContext { } }; -absl::Status CheckParameters(const SolveParametersProto& parameters) { - std::vector warnings; - if (parameters.has_threads() && parameters.threads() > 1) { - warnings.push_back(absl::StrCat( - "XpressSolver only supports parameters.threads = 1; value ", - parameters.threads(), " is not supported")); - } - if (parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED && - parameters.lp_algorithm() != LP_ALGORITHM_PRIMAL_SIMPLEX && - parameters.lp_algorithm() != LP_ALGORITHM_DUAL_SIMPLEX && - parameters.lp_algorithm() != LP_ALGORITHM_BARRIER) { - warnings.emplace_back(absl::StrCat( - "XpressSolver does not support the 'lp_algorithm' parameter value: ", - ProtoEnumToString(parameters.lp_algorithm()))); - } - if (parameters.has_objective_limit()) { - warnings.emplace_back("XpressSolver does not support objective_limit yet"); - } - if (parameters.has_best_bound_limit()) { - warnings.emplace_back("XpressSolver does not support best_bound_limit yet"); - } - if (parameters.has_cutoff_limit()) { - warnings.emplace_back("XpressSolver does not support cutoff_limit yet"); - } - if (!warnings.empty()) { - return absl::InvalidArgumentError(absl::StrJoin(warnings, "; ")); - } +template +absl::Status ScopedCallback::Add(SolveContext* ctx, + typename T::DataType data) { + RETURN_IF_ERROR(T::Add(ctx->xpress, reinterpret_cast(this))); + callbackData = data; + context = ctx; return absl::OkStatus(); } +template +void ScopedCallback::Interrupt(int reason) { + context->Interrupt(reason); +} +template +void ScopedCallback::SetCallbackException(std::exception_ptr ex) { + context->SetCallbackException(ex); +} + +template +ScopedCallback::~ScopedCallback() { + if (context) T::Remove(context->xpress, reinterpret_cast(this)); +} } // namespace constexpr SupportedProblemStructures kXpressSupportedStructures = { @@ -465,7 +601,6 @@ absl::Status XpressSolver::AddNewLinearConstraints( absl::Status XpressSolver::AddSingleObjective(const ObjectiveProto& objective) { // Sense RETURN_IF_ERROR(xpress_->SetObjectiveSense(objective.maximize())); - is_maximize_ = objective.maximize(); // Linear terms std::vector index; index.reserve(objective.linear_coefficients().ids_size()); @@ -518,15 +653,18 @@ absl::StatusOr XpressSolver::Solve( MessageCallback message_callback, const CallbackRegistrationProto& callback_registration, Callback, const SolveInterrupter* interrupter) { + primal_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; + dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; + solvestatus_ = XPRS_SOLVESTATUS_UNSTARTED; + solstatus_ = XPRS_SOLSTATUS_NOTFOUND; RETURN_IF_ERROR(ModelSolveParametersAreSupported( model_parameters, kXpressSupportedStructures, "XPRESS")); + ASSIGN_OR_RETURN(is_mip_, xpress_->IsMIP()); const absl::Time start = absl::Now(); RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration, /*supported_events=*/{})); - RETURN_IF_ERROR(CheckParameters(parameters)); - // Check that bounds are not inverted just before solve // XPRESS returns "infeasible" when bounds are inverted { @@ -545,8 +683,25 @@ absl::StatusOr XpressSolver::Solve( { ScopedSolverContext solveContext(xpress_.get()); RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); - RETURN_IF_ERROR(CallXpressSolve(parameters)) << "Error during XPRESS solve"; + RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback)); + // Solve. We use the generic XPRSoptimize() and let Xpress decide what is + // the best algorithm. Note that we do not pass flags to the function either. + // We assume that algorithms are configured via controls like LPFLAGS. + + RETURN_IF_ERROR(xpress_->Optimize("", &solvestatus_, &solstatus_)); + } + RETURN_IF_ERROR(xpress_->GetSolution(&primal_sol_avail_, nullptr, 0, -1)); + RETURN_IF_ERROR(xpress_->GetDuals(&dual_sol_avail_, nullptr, 0, -1)); + + if (!is_mip_) { + ASSIGN_OR_RETURN(int primal_status, xpress_->GetIntAttr(XPRS_LPSTATUS)); + // Note: dual_status will be one of XPRS_SOLAVAILABLE_NOTFOUND, + // XPRS_SOLAVAILABLE_OPTIMAL, XPRS_SOLAVAILABLE_FEASIBLE + int dual_status; + RETURN_IF_ERROR(xpress_->GetDuals(&dual_status, nullptr, 0, -1)); + xpress_lp_status_ = {primal_status, dual_status}; } + RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; ASSIGN_OR_RETURN( SolveResultProto solve_result, @@ -587,58 +742,6 @@ std::string XpressSolver::GetLpOptimizationFlags( return ""; } } -absl::Status XpressSolver::CallXpressSolve( - const SolveParametersProto& parameters) { - // Enable screen output right before solve - if (parameters.enable_output()) { - RETURN_IF_ERROR(xpress_->SetIntControl(XPRS_OUTPUTLOG, 1)) - << "Unable to enable XPRESS logs"; - } - // Solve - if (is_mip_) { - RETURN_IF_ERROR(xpress_->MipOptimize()); - ASSIGN_OR_RETURN(xpress_mip_status_, xpress_->GetIntAttr(XPRS_MIPSTATUS)); - } else { - RETURN_IF_ERROR(SetLpIterLimits(parameters)) - << "Could not set iteration limits."; - RETURN_IF_ERROR(xpress_->LpOptimize(GetLpOptimizationFlags(parameters))); - ASSIGN_OR_RETURN(int primal_status, xpress_->GetIntAttr(XPRS_LPSTATUS)); - ASSIGN_OR_RETURN(int dual_status, xpress_->GetDualStatus()); - xpress_lp_status_ = {primal_status, dual_status}; - } - // Post-solve - if (!(is_mip_ ? (xpress_mip_status_ == XPRS_MIP_OPTIMAL) - : (xpress_lp_status_.primal_status == XPRS_LP_OPTIMAL))) { - RETURN_IF_ERROR(xpress_->PostSolve()) << "Post-solve failed in XPRESS"; - } - // Disable screen output right after solve - if (parameters.enable_output()) { - RETURN_IF_ERROR(xpress_->SetIntControl(XPRS_OUTPUTLOG, 0)) - << "Unable to disable XPRESS logs"; - } - return absl::OkStatus(); -} - -absl::Status XpressSolver::SetLpIterLimits( - const SolveParametersProto& parameters) { - // If the user has set no limits, we still have to reset the limits - // explicitly to their default values, else the parameters could be kept - // between solves. - if (parameters.has_iteration_limit()) { - RETURN_IF_ERROR(xpress_->SetIntControl( - XPRS_LPITERLIMIT, static_cast(parameters.iteration_limit()))) - << "Could not set XPRS_LPITERLIMIT"; - RETURN_IF_ERROR(xpress_->SetIntControl( - XPRS_BARITERLIMIT, static_cast(parameters.iteration_limit()))) - << "Could not set XPRS_BARITERLIMIT"; - } else { - RETURN_IF_ERROR(xpress_->ResetIntControl(XPRS_LPITERLIMIT)) - << "Could not reset XPRS_LPITERLIMIT to its default value"; - RETURN_IF_ERROR(xpress_->ResetIntControl(XPRS_BARITERLIMIT)) - << "Could not reset XPRS_BARITERLIMIT to its default value"; - } - return absl::OkStatus(); -} absl::StatusOr XpressSolver::ExtractSolveResultProto( absl::Time start, const ModelSolveParametersProto& model_parameters, @@ -657,32 +760,56 @@ absl::StatusOr XpressSolver::ExtractSolveResultProto( } absl::StatusOr XpressSolver::GetBestPrimalBound() const { - if ((lp_algorithm_ == LP_ALGORITHM_PRIMAL_SIMPLEX) && - (isPrimalFeasible() || - xpress_lp_status_.primal_status == XPRS_LP_OPTIMAL)) { - // When primal simplex algorithm is used, XPRESS uses LPOBJVAL to store the - // primal problem's objective value - return xpress_->GetDoubleAttr(XPRS_LPOBJVAL); + if (is_mip_) { + return xpress_->GetDoubleAttr(XPRS_OBJVAL); + } else if (primal_sol_avail_ == XPRS_SOLAVAILABLE_OPTIMAL || + primal_sol_avail_ == XPRS_SOLAVAILABLE_FEASIBLE) { + return xpress_->GetDoubleAttr(XPRS_OBJVAL); } - return is_maximize_ ? kMinusInf : kPlusInf; + // No primal bound available, return infinity. + ASSIGN_OR_RETURN(double const objsen, xpress_->GetDoubleAttr(XPRS_OBJSENSE)); + return objsen * kPlusInf; } absl::StatusOr XpressSolver::GetBestDualBound() const { - if ((lp_algorithm_ == LP_ALGORITHM_DUAL_SIMPLEX) && - (isPrimalFeasible() || - xpress_lp_status_.primal_status == XPRS_LP_OPTIMAL)) { - // When dual simplex algorithm is used, XPRESS uses LPOBJVAL to store the - // dual problem's objective value - return xpress_->GetDoubleAttr(XPRS_LPOBJVAL); + if (is_mip_) { + return xpress_->GetDoubleAttr(XPRS_BESTBOUND); + } + // Xpress does not have an attribute to report the best dual bound from + // simplex + else { + ASSIGN_OR_RETURN(int const alg, xpress_->GetIntAttr(XPRS_ALGORITHM)); + if (alg == XPRS_ALG_BARRIER) + return xpress_->GetDoubleAttr(XPRS_BARDUALOBJ); + else if (primal_sol_avail_ == XPRS_SOLAVAILABLE_OPTIMAL) + return xpress_->GetDoubleAttr(XPRS_OBJVAL); } - return is_maximize_ ? kPlusInf : kMinusInf; + // No dual bound available, return infinity. + ASSIGN_OR_RETURN(double const objsen, xpress_->GetDoubleAttr(XPRS_OBJSENSE)); + return objsen * kMinusInf; } absl::StatusOr XpressSolver::GetSolution( const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters) { if (is_mip_) { - return absl::UnimplementedError("XpressSolver does not handle MIPs yet"); + int nVars = xpress_->GetNumberOfVariables(); + std::vector x(nVars); + int avail; + RETURN_IF_ERROR(xpress_->GetSolution(&avail, x.data(), 0, nVars - 1)); + SolutionProto solution{}; + if (avail != XPRS_SOLAVAILABLE_NOTFOUND) { + solution.mutable_primal_solution()->set_feasibility_status( + getPrimalSolutionStatus()); + ASSIGN_OR_RETURN(const double objval, + xpress_->GetDoubleAttr(XPRS_OBJVAL)); + solution.mutable_primal_solution()->set_objective_value(objval); + XpressVectorToSparseDoubleVector( + x, variables_map_, + *solution.mutable_primal_solution()->mutable_variable_values(), + model_parameters.variable_values_filter()); + } + return solution; } else { return GetLpSolution(model_parameters, solve_parameters); } @@ -709,7 +836,7 @@ absl::StatusOr XpressSolver::GetLpSolution( if (isPrimalFeasible()) { // Handle primal solution solution.mutable_primal_solution()->set_feasibility_status( - getLpSolutionStatus()); + getPrimalSolutionStatus()); ASSIGN_OR_RETURN(const double primalBound, GetBestPrimalBound()); solution.mutable_primal_solution()->set_objective_value(primalBound); XpressVectorToSparseDoubleVector( @@ -743,44 +870,37 @@ absl::StatusOr XpressSolver::GetLpSolution( } bool XpressSolver::isPrimalFeasible() const { - if (is_mip_) { - return xpress_mip_status_ == XPRS_MIP_OPTIMAL || - xpress_mip_status_ == XPRS_MIP_SOLUTION; - } else { - return xpress_lp_status_.primal_status == XPRS_LP_OPTIMAL || - xpress_lp_status_.primal_status == XPRS_LP_UNFINISHED; - } + return primal_sol_avail_ == XPRS_SOLAVAILABLE_FEASIBLE || + primal_sol_avail_ == XPRS_SOLAVAILABLE_OPTIMAL; } bool XpressSolver::isDualFeasible() const { - if (is_mip_) { - return isPrimalFeasible(); - } - return xpress_lp_status_.dual_status == XPRS_SOLSTATUS_OPTIMAL || - xpress_lp_status_.dual_status == XPRS_SOLSTATUS_FEASIBLE || - // When using dual simplex algorithm, if we interrupt it, dual_status - // is "not found" even if there is a solution. Using the following - // as a workaround for now - (lp_algorithm_ == LP_ALGORITHM_DUAL_SIMPLEX && isPrimalFeasible()); + /** TODO: For MIP, should we return true if we are optimal? */ + return dual_sol_avail_ == XPRS_SOLAVAILABLE_FEASIBLE || + dual_sol_avail_ == XPRS_SOLAVAILABLE_OPTIMAL; } -SolutionStatusProto XpressSolver::getLpSolutionStatus() const { - switch (xpress_lp_status_.primal_status) { - case XPRS_LP_OPTIMAL: - case XPRS_LP_UNFINISHED: +SolutionStatusProto XpressSolver::getPrimalSolutionStatus() const { + switch (solvestatus_) { + case XPRS_SOLVESTATUS_UNSTARTED: + return SOLUTION_STATUS_UNDETERMINED; + case XPRS_SOLVESTATUS_STOPPED: // fallthrough + case XPRS_SOLVESTATUS_FAILED: // fallthrough + case XPRS_SOLVESTATUS_COMPLETED: // fallthrough + break; + } + switch (solstatus_) { + case XPRS_SOLSTATUS_NOTFOUND: + return SOLUTION_STATUS_UNDETERMINED; + case XPRS_SOLSTATUS_OPTIMAL: // fallthrough + case XPRS_SOLSTATUS_FEASIBLE: return SOLUTION_STATUS_FEASIBLE; - case XPRS_LP_INFEAS: - case XPRS_LP_CUTOFF: - case XPRS_LP_CUTOFF_IN_DUAL: - case XPRS_LP_NONCONVEX: + case XPRS_SOLSTATUS_INFEASIBLE: return SOLUTION_STATUS_INFEASIBLE; - case XPRS_LP_UNSTARTED: - case XPRS_LP_UNBOUNDED: - case XPRS_LP_UNSOLVED: + case XPRS_SOLSTATUS_UNBOUNDED: return SOLUTION_STATUS_UNDETERMINED; - default: - return SOLUTION_STATUS_UNSPECIFIED; } + return SOLUTION_STATUS_UNSPECIFIED; } SolutionStatusProto XpressSolver::getDualSolutionStatus() const { @@ -945,50 +1065,141 @@ void XpressSolver::XpressVectorToSparseDoubleVector( absl::StatusOr XpressSolver::ConvertTerminationReason( double best_primal_bound, double best_dual_bound) const { + ASSIGN_OR_RETURN(double const objsen, xpress_->GetDoubleAttr(XPRS_OBJSENSE)); + ASSIGN_OR_RETURN(int const stopStatus, xpress_->GetIntAttr(XPRS_STOPSTATUS)); + bool const isMax = objsen < 0.0; + bool checkSolStatus = false; + if (!is_mip_) { - switch (xpress_lp_status_.primal_status) { + // Handle some special LP termination reasons. + ASSIGN_OR_RETURN(int const lpstatus, xpress_->GetIntAttr(XPRS_LPSTATUS)); + switch (lpstatus) { case XPRS_LP_UNSTARTED: - return TerminateForReason( - is_maximize_, TERMINATION_REASON_OTHER_ERROR, - "Problem solve has not started (XPRS_LP_UNSTARTED)"); + break; case XPRS_LP_OPTIMAL: - return OptimalTerminationProto(best_primal_bound, best_dual_bound); + break; case XPRS_LP_INFEAS: - return InfeasibleTerminationProto( - is_maximize_, isDualFeasible() ? FEASIBILITY_STATUS_FEASIBLE - : FEASIBILITY_STATUS_UNDETERMINED); + break; case XPRS_LP_CUTOFF: - return CutoffTerminationProto( - is_maximize_, "Objective worse than cutoff (XPRS_LP_CUTOFF)"); + break; case XPRS_LP_UNFINISHED: - // TODO: add support for more limit types here (this only works for LP - // iterations limit for now) - return FeasibleTerminationProto( - is_maximize_, LIMIT_ITERATION, best_primal_bound, best_dual_bound, - "Solve did not finish (XPRS_LP_UNFINISHED)"); + break; case XPRS_LP_UNBOUNDED: - return UnboundedTerminationProto(is_maximize_, - "Xpress status XPRS_LP_UNBOUNDED"); + break; case XPRS_LP_CUTOFF_IN_DUAL: return CutoffTerminationProto( - is_maximize_, "Cutoff in dual (XPRS_LP_CUTOFF_IN_DUAL)"); + isMax, "Cutoff in dual (XPRS_LP_CUTOFF_IN_DUAL)"); case XPRS_LP_UNSOLVED: - return TerminateForReason( - is_maximize_, TERMINATION_REASON_NUMERICAL_ERROR, - "Problem could not be solved due to numerical issues " - "(XPRS_LP_UNSOLVED)"); + break; case XPRS_LP_NONCONVEX: - return TerminateForReason(is_maximize_, TERMINATION_REASON_OTHER_ERROR, + return TerminateForReason(isMax, TERMINATION_REASON_OTHER_ERROR, "Problem contains quadratic data, which is " "not convex (XPRS_LP_NONCONVEX)"); - default: - return absl::InternalError( - absl::StrCat("Missing Xpress LP status code case: ", - xpress_lp_status_.primal_status)); } - } else { - return absl::UnimplementedError("XpressSolver does not handle MIPs yet"); } + + // First check how far the solve actually got. + switch (solvestatus_) { + case XPRS_SOLVESTATUS_UNSTARTED: + return TerminateForReason(isMax, TERMINATION_REASON_OTHER_ERROR, + "Problem solve has not started"); + break; + case XPRS_SOLVESTATUS_STOPPED: + checkSolStatus = true; + break; + case XPRS_SOLVESTATUS_FAILED: + switch (stopStatus) { + case XPRS_STOP_GENERICERROR: + return TerminateForReason(isMax, TERMINATION_REASON_OTHER_ERROR, + "Generic error"); + case XPRS_STOP_MEMORYERROR: + return TerminateForReason(isMax, TERMINATION_REASON_OTHER_ERROR, + "Memory error"); + case XPRS_STOP_LICENSELOST: + return TerminateForReason(isMax, TERMINATION_REASON_OTHER_ERROR, + "License lost"); + case XPRS_STOP_NUMERICALERROR: + return TerminateForReason(isMax, TERMINATION_REASON_NUMERICAL_ERROR, + "Numerical issues"); + default: + return TerminateForReason(isMax, TERMINATION_REASON_OTHER_ERROR, + "Problem solve failed"); + } + break; + case XPRS_SOLVESTATUS_COMPLETED: + checkSolStatus = true; + break; + } + if (checkSolStatus) { + // Algorithm finished or was stopped on purpose + switch (solstatus_) { + case XPRS_SOLSTATUS_NOTFOUND: + switch (stopStatus) { + case XPRS_STOP_TIMELIMIT: + return NoSolutionFoundTerminationProto( + isMax, LIMIT_TIME, std::nullopt, /** TODO: bound? */ + "Time limit hit"); + case XPRS_STOP_CTRLC: // fallthrough + case XPRS_STOP_USER: + return NoSolutionFoundTerminationProto( + isMax, LIMIT_INTERRUPTED, std::nullopt, /** TODO: bound? */ + "Interrupted"); + case XPRS_STOP_NODELIMIT: + return NoSolutionFoundTerminationProto( + isMax, LIMIT_NODE, std::nullopt, /** TODO: bound? */ + "Node limit hit"); + case XPRS_STOP_ITERLIMIT: + return NoSolutionFoundTerminationProto( + isMax, LIMIT_ITERATION, std::nullopt, /** TODO: bound? */ + "Node limit hit"); + case XPRS_STOP_WORKLIMIT: + return NoSolutionFoundTerminationProto( + isMax, LIMIT_OTHER, std::nullopt, /** TODO: bound? */ + "Work limit hit"); + default: + return TerminateForReason(isMax, + TERMINATION_REASON_NO_SOLUTION_FOUND); + } + break; + case XPRS_SOLSTATUS_OPTIMAL: + return OptimalTerminationProto(best_primal_bound, best_dual_bound); + break; + case XPRS_SOLSTATUS_FEASIBLE: + switch (stopStatus) { + case XPRS_STOP_TIMELIMIT: + return FeasibleTerminationProto(isMax, LIMIT_TIME, + best_primal_bound, best_dual_bound, + "Time limit hit"); + case XPRS_STOP_CTRLC: // fallthrough + case XPRS_STOP_USER: + return FeasibleTerminationProto(isMax, LIMIT_INTERRUPTED, + best_primal_bound, best_dual_bound, + "Interrupted"); + case XPRS_STOP_NODELIMIT: + return FeasibleTerminationProto(isMax, LIMIT_NODE, + best_primal_bound, best_dual_bound, + "Node limit hit"); + case XPRS_STOP_ITERLIMIT: + return FeasibleTerminationProto(isMax, LIMIT_ITERATION, + best_primal_bound, best_dual_bound, + "Node limit hit"); + case XPRS_STOP_WORKLIMIT: + return FeasibleTerminationProto(isMax, LIMIT_OTHER, + best_primal_bound, best_dual_bound, + "Work limit hit"); + default: + return FeasibleTerminationProto(isMax, LIMIT_UNDETERMINED, + best_primal_bound, best_dual_bound); + } + break; + case XPRS_SOLSTATUS_INFEASIBLE: + return TerminateForReason(isMax, TERMINATION_REASON_INFEASIBLE); + case XPRS_SOLSTATUS_UNBOUNDED: + return UnboundedTerminationProto(isMax); + } + } + return TerminateForReason(isMax, TERMINATION_REASON_OTHER_ERROR, + "Unknown error"); } absl::StatusOr XpressSolver::Update(const ModelUpdateProto&) { @@ -997,11 +1208,12 @@ absl::StatusOr XpressSolver::Update(const ModelUpdateProto&) { } absl::StatusOr -XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto&, +XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, MessageCallback message_callback, const SolveInterrupter* interrupter) { ScopedSolverContext solveContext(xpress_.get()); RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); + RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback)); return absl::UnimplementedError( "XPRESS does not provide a method to compute an infeasible subsystem"); diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 65c98b456fe..d1f0abe12e2 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -136,7 +136,6 @@ class XpressSolver : public SolverInterface { absl::Status LoadModel(const ModelProto& input_model); std::string GetLpOptimizationFlags(const SolveParametersProto& parameters); - absl::Status CallXpressSolve(const SolveParametersProto& parameters); // Fills in result with the values in xpress_values aided by the index // conversion from map which should be either variables_map_ or @@ -162,14 +161,16 @@ class XpressSolver : public SolverInterface { int get_model_index(const LinearConstraintData& index) const { return index.constraint_index; } - SolutionStatusProto getLpSolutionStatus() const; + SolutionStatusProto getPrimalSolutionStatus() const; SolutionStatusProto getDualSolutionStatus() const; absl::StatusOr ListInvertedBounds() const; absl::Status SetXpressStartingBasis(const BasisProto& basis); - absl::Status SetLpIterLimits(const SolveParametersProto& parameters); bool is_mip_ = false; - bool is_maximize_ = false; + int primal_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; + int dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; + int solvestatus_ = XPRS_SOLVESTATUS_UNSTARTED; + int solstatus_ = XPRS_SOLSTATUS_NOTFOUND; struct LpStatus { int primal_status = 0; @@ -177,8 +178,6 @@ class XpressSolver : public SolverInterface { }; LpStatus xpress_lp_status_; LPAlgorithmProto lp_algorithm_ = LP_ALGORITHM_UNSPECIFIED; - - int xpress_mip_status_ = 0; }; } // namespace operations_research::math_opt diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index 5fac4ce9834..a7f2508bf00 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -73,6 +73,7 @@ std::function XPRSgetrhsr std::function XPRSgetlb = nullptr; std::function XPRSgetub = nullptr; std::function XPRSgetcoef = nullptr; +std::function XPRSgetsolution = nullptr; std::function XPRSgetduals = nullptr; std::function XPRSgetredcosts = nullptr; std::function XPRSaddrows = nullptr; @@ -151,6 +152,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSgetlb, "XPRSgetlb"); xpress_dynamic_library->GetFunction(&XPRSgetub, "XPRSgetub"); xpress_dynamic_library->GetFunction(&XPRSgetcoef, "XPRSgetcoef"); + xpress_dynamic_library->GetFunction(&XPRSgetsolution, "XPRSgetsolution"); xpress_dynamic_library->GetFunction(&XPRSgetduals, "XPRSgetduals"); xpress_dynamic_library->GetFunction(&XPRSgetredcosts, "XPRSgetredcosts"); xpress_dynamic_library->GetFunction(&XPRSaddrows, "XPRSaddrows"); diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index 501c55cb703..61a17c95e5a 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -77,6 +77,7 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); #define XPRS_STOP_SOLVECOMPLETE 10 #define XPRS_STOP_LICENSELOST 11 #define XPRS_STOP_NUMERICALERROR 13 +#define XPRS_STOP_WORKLIMIT 14 // *************************************************************************** // * values related to Set/GetControl/Attribinfo * // *************************************************************************** @@ -90,11 +91,39 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); // *************************************************************************** #define XPRS_NAMES_ROW 1 #define XPRS_NAMES_COLUMN 2 +// *************************************************************************** +// * values related to SOLAVAILABLE * +// *************************************************************************** +#define XPRS_SOLAVAILABLE_NOTFOUND 0 +#define XPRS_SOLAVAILABLE_OPTIMAL 1 +#define XPRS_SOLAVAILABLE_FEASIBLE 2 +// *************************************************************************** +// * values related to SOLVESTATUS * +// *************************************************************************** +#define XPRS_SOLVESTATUS_UNSTARTED 0 +#define XPRS_SOLVESTATUS_STOPPED 1 +#define XPRS_SOLVESTATUS_FAILED 2 +#define XPRS_SOLVESTATUS_COMPLETED 3 +// *************************************************************************** +// * values related to DEFAULTALG and ALGORITHM * +// *************************************************************************** +#define XPRS_ALG_DEFAULT 1 +#define XPRS_ALG_DUAL 2 +#define XPRS_ALG_PRIMAL 3 +#define XPRS_ALG_BARRIER 4 +#define XPRS_ALG_NETWORK 5 #define XPRS_PLUSINFINITY 1.0e+20 #define XPRS_MINUSINFINITY -1.0e+20 #define XPRS_MAXBANNERLENGTH 512 -#define XPVERSION 41 +#define XPVERSION 45 // >= 45 for XPRS_SOLAVAILABLE flags, XPRSgetduals(), etc. +#define XPRS_MIPENTS 1032 +#define XPRS_ALGORITHM 1049 +#define XPRS_STOPSTATUS 1179 +#define XPRS_SOLVESTATUS 1394 +#define XPRS_OBJVAL 2118 +#define XPRS_BARPRIMALOBJ 4001 +#define XPRS_BARDUALOBJ 4002 #define XPRS_MPSRHSNAME 6001 #define XPRS_MPSOBJNAME 6002 #define XPRS_MPSRANGENAME 6003 @@ -429,6 +458,7 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); #define XPRS_SOLSTATUS_FEASIBLE 2 #define XPRS_SOLSTATUS_INFEASIBLE 3 #define XPRS_SOLSTATUS_UNBOUNDED 4 +#define XPRS_SOLSTATUS 1053 #define XPRS_LPSTATUS 1010 #define XPRS_MIPSTATUS 1011 #define XPRS_NODES 1013 @@ -506,6 +536,7 @@ OR_DLL extern std::function XPRSgetlb; OR_DLL extern std::function XPRSgetub; OR_DLL extern std::function XPRSgetcoef; +extern std::function XPRSsolution; extern std::function XPRSgetduals; extern std::function XPRSgetredcosts; extern std::function XPRSaddrows; From 79b84c2cfcb33ccdd5b2009283a457320eb6192b Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 7 Oct 2025 10:30:00 +0200 Subject: [PATCH 19/70] Fix setting of cut strategy. --- ortools/math_opt/solvers/xpress_solver.cc | 30 ++++++++++------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 74c907f67dc..f128ce629d3 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -352,20 +352,17 @@ class ScopedSolverContext { switch (parameters.presolve()) { case EMPHASIS_OFF: break; - case EMPHASIS_UNSPECIFIED: - presolve = 2; - break; case EMPHASIS_LOW: - presolve = 3; + presolve = 2; break; case EMPHASIS_MEDIUM: - presolve = 4; + presolve = 3; break; case EMPHASIS_HIGH: - presolve = 5; + presolve = 4; break; case EMPHASIS_VERY_HIGH: - presolve = 6; + presolve = 5; break; } if (presolve > 0) RETURN_IF_ERROR(Set(XPRS_PRESOLVEPASSES, presolve)); @@ -373,20 +370,19 @@ class ScopedSolverContext { if (parameters.cuts() != EMPHASIS_UNSPECIFIED) { switch (parameters.cuts()) { case EMPHASIS_OFF: - RETURN_IF_ERROR(Set(XPRS_GOMCUTS, 0)); - RETURN_IF_ERROR(Set(XPRS_TREEGOMCUTS, 0)); - RETURN_IF_ERROR(Set(XPRS_COVERCUTS, 0)); - RETURN_IF_ERROR(Set(XPRS_TREECOVERCUTS, 0)); + RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 0)); break; - case EMPHASIS_UNSPECIFIED: /** TODO */ - break; - case EMPHASIS_LOW: /** TODO */ + case EMPHASIS_LOW: + RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 1)); break; - case EMPHASIS_MEDIUM: /** TODO */ + case EMPHASIS_MEDIUM: + RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 2)); break; - case EMPHASIS_HIGH: /** TODO */ + case EMPHASIS_HIGH: + RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 3)); break; - case EMPHASIS_VERY_HIGH: /** TODO */ + case EMPHASIS_VERY_HIGH: + RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 3)); // Same as high break; } } From 797f515a8c28d9ff129be2dfb7a9c152cd4306d9 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 7 Oct 2025 10:34:20 +0200 Subject: [PATCH 20/70] Finish conversion to `XPRSoptimize()`. --- ortools/math_opt/solvers/xpress_solver.cc | 70 ++--------------------- ortools/math_opt/solvers/xpress_solver.h | 10 +--- 2 files changed, 7 insertions(+), 73 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index f128ce629d3..f2a69b1b0b2 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -688,15 +688,6 @@ absl::StatusOr XpressSolver::Solve( } RETURN_IF_ERROR(xpress_->GetSolution(&primal_sol_avail_, nullptr, 0, -1)); RETURN_IF_ERROR(xpress_->GetDuals(&dual_sol_avail_, nullptr, 0, -1)); - - if (!is_mip_) { - ASSIGN_OR_RETURN(int primal_status, xpress_->GetIntAttr(XPRS_LPSTATUS)); - // Note: dual_status will be one of XPRS_SOLAVAILABLE_NOTFOUND, - // XPRS_SOLAVAILABLE_OPTIMAL, XPRS_SOLAVAILABLE_FEASIBLE - int dual_status; - RETURN_IF_ERROR(xpress_->GetDuals(&dual_status, nullptr, 0, -1)); - xpress_lp_status_ = {primal_status, dual_status}; - } RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; ASSIGN_OR_RETURN( @@ -706,39 +697,6 @@ absl::StatusOr XpressSolver::Solve( return solve_result; } -std::string XpressSolver::GetLpOptimizationFlags( - const SolveParametersProto& parameters) { - switch (parameters.lp_algorithm()) { - case LP_ALGORITHM_PRIMAL_SIMPLEX: - lp_algorithm_ = LP_ALGORITHM_PRIMAL_SIMPLEX; - return "p"; - case LP_ALGORITHM_DUAL_SIMPLEX: - lp_algorithm_ = LP_ALGORITHM_DUAL_SIMPLEX; - return "d"; - case LP_ALGORITHM_BARRIER: - lp_algorithm_ = LP_ALGORITHM_BARRIER; - return "b"; - default: - // this makes XPRESS use default algorithm (XPRS_DEFAULTALG) - // but we have to figure out what it is for solution processing - auto default_alg = xpress_->GetIntControl(XPRS_DEFAULTALG); - switch (default_alg.value_or(-1)) { - case XPRS_ALG_PRIMAL: - lp_algorithm_ = LP_ALGORITHM_PRIMAL_SIMPLEX; - break; - case XPRS_ALG_DUAL: - lp_algorithm_ = LP_ALGORITHM_DUAL_SIMPLEX; - break; - case XPRS_ALG_BARRIER: - lp_algorithm_ = LP_ALGORITHM_BARRIER; - break; - default: - lp_algorithm_ = LP_ALGORITHM_UNSPECIFIED; - } - return ""; - } -} - absl::StatusOr XpressSolver::ExtractSolveResultProto( absl::Time start, const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters) { @@ -900,29 +858,13 @@ SolutionStatusProto XpressSolver::getPrimalSolutionStatus() const { } SolutionStatusProto XpressSolver::getDualSolutionStatus() const { - // When using dual simplex algorithm, if we interrupt it, dual_status - // is "not found" even if there is a solution. Using the following - // as a workaround for now - if (isDualFeasible()) { + if (dual_sol_avail_ == XPRS_SOLAVAILABLE_OPTIMAL || + dual_sol_avail_ == XPRS_SOLAVAILABLE_FEASIBLE) return SOLUTION_STATUS_FEASIBLE; - } - switch (xpress_lp_status_.dual_status) { - case XPRS_SOLSTATUS_OPTIMAL: - case XPRS_SOLSTATUS_FEASIBLE: - return SOLUTION_STATUS_FEASIBLE; - case XPRS_SOLSTATUS_INFEASIBLE: - return SOLUTION_STATUS_INFEASIBLE; - case XPRS_SOLSTATUS_UNBOUNDED: - // when primal is unbounded, XPRESS returns unbounded for dual also (known - // issue). this is a temporary workaround - return (xpress_lp_status_.primal_status == XPRS_LP_UNBOUNDED) - ? SOLUTION_STATUS_INFEASIBLE - : SOLUTION_STATUS_UNDETERMINED; - case XPRS_SOLSTATUS_NOTFOUND: - return SOLUTION_STATUS_UNDETERMINED; - default: - return SOLUTION_STATUS_UNSPECIFIED; - } + /** TODO: Should we be more specific here? If primal is unbounded we + * know that dual is infeasible. + */ + return SOLUTION_STATUS_UNDETERMINED; } inline BasisStatusProto XpressToMathOptBasisStatus(const int status, diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index d1f0abe12e2..339686de5d3 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -135,8 +135,6 @@ class XpressSolver : public SolverInterface { absl::Status LoadModel(const ModelProto& input_model); - std::string GetLpOptimizationFlags(const SolveParametersProto& parameters); - // Fills in result with the values in xpress_values aided by the index // conversion from map which should be either variables_map_ or // linear_constraints_map_ as appropriate. Only key/value pairs that passes @@ -167,17 +165,11 @@ class XpressSolver : public SolverInterface { absl::Status SetXpressStartingBasis(const BasisProto& basis); bool is_mip_ = false; + // Results of the last solve int primal_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; int dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; int solvestatus_ = XPRS_SOLVESTATUS_UNSTARTED; int solstatus_ = XPRS_SOLSTATUS_NOTFOUND; - - struct LpStatus { - int primal_status = 0; - int dual_status = 0; - }; - LpStatus xpress_lp_status_; - LPAlgorithmProto lp_algorithm_ = LP_ALGORITHM_UNSPECIFIED; }; } // namespace operations_research::math_opt From f63af0306ad05e350964707d9805c54a20d84675 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 7 Oct 2025 10:36:33 +0200 Subject: [PATCH 21/70] Enable support for integer variables. Now that we go through `XPRSoptimize()` this should work out of the box. --- ortools/math_opt/solvers/xpress_solver.cc | 14 +++++--------- ortools/math_opt/solvers/xpress_solver_test.cc | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index f2a69b1b0b2..783eca86906 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -373,16 +373,16 @@ class ScopedSolverContext { RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 0)); break; case EMPHASIS_LOW: - RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 1)); + RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 1)); break; case EMPHASIS_MEDIUM: - RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 2)); + RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 2)); break; case EMPHASIS_HIGH: - RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 3)); + RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 3)); break; case EMPHASIS_VERY_HIGH: - RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 3)); // Same as high + RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 3)); // Same as high break; } } @@ -484,7 +484,7 @@ ScopedCallback::~ScopedCallback() { } // namespace constexpr SupportedProblemStructures kXpressSupportedStructures = { - .integer_variables = SupportType::kNotSupported, + .integer_variables = SupportType::kSupported, .multi_objectives = SupportType::kNotSupported, .quadratic_objectives = SupportType::kSupported, .quadratic_constraints = SupportType::kNotSupported, @@ -529,10 +529,6 @@ absl::Status XpressSolver::AddNewVariables( gtl::InsertOrDie(&variables_map_, id, j + n_variables); variable_type[j] = new_variables.integers(j) ? XPRS_INTEGER : XPRS_CONTINUOUS; - if (new_variables.integers(j)) { - is_mip_ = true; - return absl::UnimplementedError("XpressSolver does not handle MIPs yet"); - } } RETURN_IF_ERROR(xpress_->AddVars({}, new_variables.lower_bounds(), new_variables.upper_bounds(), diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 2b5c4da7705..1ea81fb5d7a 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -98,12 +98,12 @@ INSTANTIATE_TEST_SUITE_P(XpressMessageCallbackTest, MessageCallbackTest, SolverType::kXpress, /*support_message_callback=*/true, /*support_interrupter=*/true, - /*integer_variables=*/false, ""))); + /*integer_variables=*/true, ""))); INSTANTIATE_TEST_SUITE_P( XpressCallbackTest, CallbackTest, testing::Values(CallbackTestParams(SolverType::kXpress, - /*integer_variables=*/false, + /*integer_variables=*/true, /*add_lazy_constraints=*/false, /*add_cuts=*/false, /*supported_events=*/{}, From 720602f268661755475ed6f024657eefe0e06ee1 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 7 Oct 2025 13:26:22 +0200 Subject: [PATCH 22/70] Fix compilation issues. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 2 +- ortools/third_party_solvers/xpress_environment.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 3c33c53c972..6518b1e66d8 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -393,7 +393,7 @@ absl::Status Xpress::GetDuals(int* p_status, double* duals, int first, int last) { return ToStatus(XPRSgetduals(xpress_model_, p_status, duals, first, last)); } -absl::Status Xpress::GetSolution(int* p_status, absl::Span x, int first, +absl::Status Xpress::GetSolution(int* p_status, double *x, int first, int last) { return ToStatus(XPRSgetsolution(xpress_model_, p_status, x, first, last)); } diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index 61a17c95e5a..0eaa68ec97b 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -536,7 +536,7 @@ OR_DLL extern std::function XPRSgetlb; OR_DLL extern std::function XPRSgetub; OR_DLL extern std::function XPRSgetcoef; -extern std::function XPRSsolution; +extern std::function XPRSgetsolution; extern std::function XPRSgetduals; extern std::function XPRSgetredcosts; extern std::function XPRSaddrows; From acbf3984d3f63d4384bb36671d4c65a985be8f03 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 07:00:11 +0200 Subject: [PATCH 23/70] Fix extraction of free ranged rows. --- ortools/math_opt/solvers/xpress_solver.cc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 783eca86906..e6c7a68f7ee 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -568,7 +568,18 @@ absl::Status XpressSolver::AddNewLinearConstraints( double rng = 0.0; const bool lb_is_xprs_neg_inf = lb <= kMinusInf; const bool ub_is_xprs_pos_inf = ub >= kPlusInf; - if (lb_is_xprs_neg_inf && !ub_is_xprs_pos_inf) { + if (lb_is_xprs_neg_inf && ub_is_xprs_pos_inf) { + // We have a row + // -inf <= expression <= inf + // Xpress has no way to submit this as a ranged constraint. For Xpress + // the upper bound of the constraint is just the ub and the lower bound + // is computed as ub-abs(lb). This would result in inf-inf=nan if you + // use IEEE infinity or XPRS_INFINITY - XPRS_INFINITY = 0. Both are wrong. + // So we explicitly register this as free row. + sense = XPRS_NONBINDING; + rhs = 0.0; + rng = 0.0; + } else if (lb_is_xprs_neg_inf && !ub_is_xprs_pos_inf) { sense = XPRS_LESS_EQUAL; rhs = ub; } else if (!lb_is_xprs_neg_inf && ub_is_xprs_pos_inf) { From dd4842347e1f5b9c38d3d69288687ad09961d7bb Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 07:49:34 +0200 Subject: [PATCH 24/70] Enable more tests. --- .../math_opt/solvers/xpress_solver_test.cc | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 1ea81fb5d7a..fd20d24f7a7 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -62,10 +62,10 @@ INSTANTIATE_TEST_SUITE_P( testing::Values(LpParameterTestParams(SolverType::kXpress, /*supports_simplex=*/true, /*supports_barrier=*/true, - /*supports_first_order=*/false, - /*supports_random_seed=*/false, - /*supports_presolve=*/false, - /*supports_cutoff=*/false, + /*supports_first_order=*/true, + /*supports_random_seed=*/true, + /*supports_presolve=*/true, + /*supports_cutoff=*/true, /*supports_objective_limit=*/false, /*supports_best_bound_limit=*/false, /*reports_limits=*/false))); @@ -77,7 +77,7 @@ INSTANTIATE_TEST_SUITE_P( /*lp_algorithm=*/LPAlgorithm::kPrimalSimplex, /*supports_iteration_limit=*/true, /*supports_initial_basis=*/false, /*supports_incremental_solve=*/false, /*supports_basis=*/true, - /*supports_presolve=*/false, /*check_primal_objective=*/true, + /*supports_presolve=*/true, /*check_primal_objective=*/true, /*primal_solution_status_always_set=*/true, /*dual_solution_status_always_set=*/true))); INSTANTIATE_TEST_SUITE_P( @@ -87,18 +87,18 @@ INSTANTIATE_TEST_SUITE_P( /*lp_algorithm=*/LPAlgorithm::kDualSimplex, /*supports_iteration_limit=*/true, /*supports_initial_basis=*/false, /*supports_incremental_solve=*/false, /*supports_basis=*/true, - /*supports_presolve=*/false, /*check_primal_objective=*/true, + /*supports_presolve=*/true, /*check_primal_objective=*/true, /*primal_solution_status_always_set=*/true, /*dual_solution_status_always_set=*/true))); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(IncrementalLpTest); -INSTANTIATE_TEST_SUITE_P(XpressMessageCallbackTest, MessageCallbackTest, - testing::Values(MessageCallbackTestParams( - SolverType::kXpress, - /*support_message_callback=*/true, - /*support_interrupter=*/true, - /*integer_variables=*/true, ""))); +INSTANTIATE_TEST_SUITE_P( + XpressMessageCallbackTest, MessageCallbackTest, + testing::Values(MessageCallbackTestParams(SolverType::kXpress, + /*support_message_callback=*/true, + /*support_interrupter=*/true, + /*integer_variables=*/true, ""))); INSTANTIATE_TEST_SUITE_P( XpressCallbackTest, CallbackTest, @@ -115,20 +115,10 @@ INSTANTIATE_TEST_SUITE_P(XpressInvalidInputTest, InvalidInputTest, SolverType::kXpress, /*use_integer_variables=*/false))); -InvalidParameterTestParams InvalidThreadsParameters() { - SolveParameters params; - params.threads = 2; - return InvalidParameterTestParams(SolverType::kXpress, std::move(params), - {"only supports parameters.threads = 1"}); -} - -INSTANTIATE_TEST_SUITE_P(XpressInvalidParameterTest, InvalidParameterTest, - ValuesIn({InvalidThreadsParameters()})); - INSTANTIATE_TEST_SUITE_P(XpressGenericTest, GenericTest, testing::Values(GenericTestParameters( - SolverType::kXpress, /*support_interrupter=*/false, - /*integer_variables=*/false, + SolverType::kXpress, /*support_interrupter=*/true, + /*integer_variables=*/true, /*expected_log=*/"Optimal solution found"))); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TimeLimitTest); @@ -151,7 +141,7 @@ GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(LazyConstraintsTest); LogicalConstraintTestParameters GetXpressLogicalConstraintTestParameters() { return LogicalConstraintTestParameters( SolverType::kXpress, SolveParameters(), - /*supports_integer_variables=*/false, + /*supports_integer_variables=*/true, /*supports_sos1=*/false, /*supports_sos2=*/false, /*supports_indicator_constraints=*/false, @@ -174,7 +164,7 @@ MultiObjectiveTestParameters GetXpressMultiObjectiveTestParameters() { /*supports_auxiliary_objectives=*/false, /*supports_incremental_objective_add_and_delete=*/false, /*supports_incremental_objective_modification=*/false, - /*supports_integer_variables=*/false); + /*supports_integer_variables=*/true); } INSTANTIATE_TEST_SUITE_P( @@ -234,10 +224,10 @@ std::vector MakeStatusTestConfigs() { test_parameters.push_back(StatusTestParameters( SolverType::kXpress, solve_parameters, /*disallow_primal_or_dual_infeasible=*/false, - /*supports_iteration_limit=*/false, + /*supports_iteration_limit=*/true, /*use_integer_variables=*/false, - /*supports_node_limit=*/false, - /*support_interrupter=*/false, /*supports_one_thread=*/true)); + /*supports_node_limit=*/true, + /*support_interrupter=*/true, /*supports_one_thread=*/true)); } return test_parameters; } From 5d89668a2d2aa04083f4079d4336cb11763d4f48 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 07:49:40 +0200 Subject: [PATCH 25/70] Fix infeasibility status. --- ortools/math_opt/solvers/xpress_solver.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index e6c7a68f7ee..3ff0a016fca 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -1138,7 +1138,9 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( } break; case XPRS_SOLSTATUS_INFEASIBLE: - return TerminateForReason(isMax, TERMINATION_REASON_INFEASIBLE); + return InfeasibleTerminationProto( + isMax, isDualFeasible() ? FEASIBILITY_STATUS_FEASIBLE + : FEASIBILITY_STATUS_UNDETERMINED); case XPRS_SOLSTATUS_UNBOUNDED: return UnboundedTerminationProto(isMax); } From 83f51815d25ed9d16b390a91cf7c7025b5298090 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 11:25:31 +0200 Subject: [PATCH 26/70] Complete and fix `SolveStats` reporting. --- ortools/math_opt/solvers/xpress_solver.cc | 37 +++++++++++++++++------ ortools/math_opt/solvers/xpress_solver.h | 1 + 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 3ff0a016fca..00a3664ff32 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -660,6 +660,7 @@ absl::StatusOr XpressSolver::Solve( dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; solvestatus_ = XPRS_SOLVESTATUS_UNSTARTED; solstatus_ = XPRS_SOLSTATUS_NOTFOUND; + algorithm_ = XPRS_ALG_DEFAULT; RETURN_IF_ERROR(ModelSolveParametersAreSupported( model_parameters, kXpressSupportedStructures, "XPRESS")); ASSIGN_OR_RETURN(is_mip_, xpress_->IsMIP()); @@ -695,6 +696,7 @@ absl::StatusOr XpressSolver::Solve( } RETURN_IF_ERROR(xpress_->GetSolution(&primal_sol_avail_, nullptr, 0, -1)); RETURN_IF_ERROR(xpress_->GetDuals(&dual_sol_avail_, nullptr, 0, -1)); + ASSIGN_OR_RETURN(algorithm_, xpress_->GetIntAttr(XPRS_ALGORITHM)); RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; ASSIGN_OR_RETURN( @@ -978,18 +980,33 @@ absl::StatusOr XpressSolver::GetSolveStats( CHECK_OK(util_time::EncodeGoogleApiProto(absl::Now() - start, solve_stats.mutable_solve_time())); - // LP simplex iterations - { - ASSIGN_OR_RETURN(const int iters, xpress_->GetIntAttr(XPRS_SIMPLEXITER)); - solve_stats.set_simplex_iterations(iters); + int simplex_iters = 0; + int barrier_iters = 0; + int first_order_iters = 0; + if (algorithm_ == XPRS_ALG_DEFAULT) { + // Could be concurrent, so capture simplex and barrier iterations + ASSIGN_OR_RETURN(simplex_iters, xpress_->GetIntAttr(XPRS_SIMPLEXITER)); + ASSIGN_OR_RETURN(barrier_iters, xpress_->GetIntAttr(XPRS_BARITER)); + } else if (algorithm_ == XPRS_ALG_DUAL || algorithm_ == XPRS_ALG_PRIMAL || + algorithm_ == XPRS_ALG_NETWORK) { + // Definitely simplex + ASSIGN_OR_RETURN(simplex_iters, xpress_->GetIntAttr(XPRS_SIMPLEXITER)); + } else if (algorithm_ == XPRS_ALG_BARRIER) { + // Barrier or first order + ASSIGN_OR_RETURN(const int baralg, xpress_->GetIntControl(XPRS_BARALG)); + if (baralg == 4) { + ASSIGN_OR_RETURN(first_order_iters, xpress_->GetIntAttr(XPRS_BARITER)); + } else { + ASSIGN_OR_RETURN(barrier_iters, xpress_->GetIntAttr(XPRS_BARITER)); + } } - // LP barrier iterations - { - ASSIGN_OR_RETURN(const int iters, xpress_->GetIntAttr(XPRS_BARITER)); - solve_stats.set_barrier_iterations(iters); + solve_stats.set_simplex_iterations(simplex_iters); + solve_stats.set_barrier_iterations(barrier_iters); + solve_stats.set_first_order_iterations(first_order_iters); + if (is_mip_) { + ASSIGN_OR_RETURN(const int nodes, xpress_->GetIntAttr(XPRS_NODES)); + solve_stats.set_node_count(nodes); } - - // TODO: complete these stats return solve_stats; } diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 339686de5d3..a882df35738 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -170,6 +170,7 @@ class XpressSolver : public SolverInterface { int dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; int solvestatus_ = XPRS_SOLVESTATUS_UNSTARTED; int solstatus_ = XPRS_SOLSTATUS_NOTFOUND; + int algorithm_ = XPRS_ALG_DEFAULT; }; } // namespace operations_research::math_opt From e037d0b6073baffce0937bfe55bf62900bd9ab56 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 12:20:16 +0200 Subject: [PATCH 27/70] Skip random seed tests. --- .../math_opt/solvers/xpress_solver_test.cc | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index fd20d24f7a7..40dc5123464 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -59,16 +59,18 @@ INSTANTIATE_TEST_SUITE_P(XpressLpModelSolveParametersTest, INSTANTIATE_TEST_SUITE_P( XpressLpParameterTest, LpParameterTest, - testing::Values(LpParameterTestParams(SolverType::kXpress, - /*supports_simplex=*/true, - /*supports_barrier=*/true, - /*supports_first_order=*/true, - /*supports_random_seed=*/true, - /*supports_presolve=*/true, - /*supports_cutoff=*/true, - /*supports_objective_limit=*/false, - /*supports_best_bound_limit=*/false, - /*reports_limits=*/false))); + testing::Values(LpParameterTestParams( + SolverType::kXpress, + /*supports_simplex=*/true, + /*supports_barrier=*/true, + /*supports_first_order=*/true, + /*supports_random_seed=*/false, // Xpress supports this but it does not + // generate enough variability for this + /*supports_presolve=*/true, + /*supports_cutoff=*/true, + /*supports_objective_limit=*/false, + /*supports_best_bound_limit=*/false, + /*reports_limits=*/false))); INSTANTIATE_TEST_SUITE_P( XpressPrimalSimplexLpIncompleteSolveTest, LpIncompleteSolveTest, From 6ca60151df3a5558ed5bdec1948cea6f17bcfb87 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 12:30:18 +0200 Subject: [PATCH 28/70] Fix handling of presolve emphasis. --- ortools/math_opt/solvers/xpress_solver.cc | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 00a3664ff32..b3be436d873 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -348,24 +348,26 @@ class ScopedSolverContext { } if (parameters.presolve() != EMPHASIS_UNSPECIFIED) { // default value for XPRS_PRESOLVEPASSES is 1 - int presolve = -1; + int presolvePasses = -1; switch (parameters.presolve()) { case EMPHASIS_OFF: + RETURN_IF_ERROR(Set(XPRS_PRESOLVE, 0)); // Turn presolve off break; case EMPHASIS_LOW: - presolve = 2; + presolvePasses = 2; break; case EMPHASIS_MEDIUM: - presolve = 3; + presolvePasses = 3; break; case EMPHASIS_HIGH: - presolve = 4; + presolvePasses = 4; break; case EMPHASIS_VERY_HIGH: - presolve = 5; + presolvePasses = 5; break; } - if (presolve > 0) RETURN_IF_ERROR(Set(XPRS_PRESOLVEPASSES, presolve)); + if (presolvePasses > 0) + RETURN_IF_ERROR(Set(XPRS_PRESOLVEPASSES, presolvePasses)); } if (parameters.cuts() != EMPHASIS_UNSPECIFIED) { switch (parameters.cuts()) { From 0a438642c4eb52559a657fc1072d52ff7ab2a181 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 12:45:06 +0200 Subject: [PATCH 29/70] Fix some parameter handling. --- ortools/math_opt/solvers/xpress_solver.cc | 7 +++++-- ortools/math_opt/solvers/xpress_solver_test.cc | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index b3be436d873..0af2251a1e3 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -304,10 +304,11 @@ class ScopedSolverContext { RETURN_IF_ERROR(Set(XPRS_MIPABSCUTOFF, parameters.cutoff_limit())); } if (parameters.has_objective_limit()) { - /** TODO */ + // MIPABSCUTOFF also applies to LPs. + RETURN_IF_ERROR(Set(XPRS_MIPABSCUTOFF, parameters.cutoff_limit())); } if (parameters.has_best_bound_limit()) { - /** TODO */ + warnings.emplace_back("XpressSolver does not support best_bound_limit"); } if (parameters.has_solution_limit()) { RETURN_IF_ERROR(Set(XPRS_MAXMIPSOL, parameters.solution_limit())); @@ -1045,6 +1046,8 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( case XPRS_LP_INFEAS: break; case XPRS_LP_CUTOFF: + return CutoffTerminationProto( + isMax, "Cutoff in dual (XPRS_LP_CUTOFF_IN_DUAL)"); break; case XPRS_LP_UNFINISHED: break; diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 40dc5123464..6b31e9a9246 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -68,7 +68,7 @@ INSTANTIATE_TEST_SUITE_P( // generate enough variability for this /*supports_presolve=*/true, /*supports_cutoff=*/true, - /*supports_objective_limit=*/false, + /*supports_objective_limit=*/true, /*supports_best_bound_limit=*/false, /*reports_limits=*/false))); From 7e8b5f940824b6bfe608b2535e308336ac192ad5 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 12:51:58 +0200 Subject: [PATCH 30/70] Do not return non-zero from checktime() callback. --- ortools/math_opt/solvers/xpress_solver.cc | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 0af2251a1e3..960234fda4a 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -208,7 +208,14 @@ DEFINE_SCOPED_CB(Message, MessageCallback, void, DEFINE_SCOPED_CB(Checktime, SolveInterrupter const*, int, (XPRSprob prob, void* cbdata)) { auto cb = reinterpret_cast(cbdata); - return cb->or_tools_cb->IsInterrupted() ? 1 : 0; + // Note: we do NOT return non-zero from the callback if the solve was + // interrupted. Returning non-zero from the callback is interpreted + // as hitting a time limit and we would therefore not map correctly + // the resulting stop status to or tools' termination status. + if (cb->or_tools_cb->IsInterrupted()) { + cb->Interrupt(XPRS_STOP_USER); + } + return 0; } /** An ortools message callback that prints everything to stdout. */ From 810088512e2eb328e90b11bce65fa025c9579b07 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 13:09:38 +0200 Subject: [PATCH 31/70] Fix handling of cutoff return status. --- ortools/math_opt/solvers/xpress_solver.cc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 960234fda4a..3696ed94910 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -1053,16 +1053,20 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( case XPRS_LP_INFEAS: break; case XPRS_LP_CUTOFF: - return CutoffTerminationProto( - isMax, "Cutoff in dual (XPRS_LP_CUTOFF_IN_DUAL)"); + // This can happen if you set MIPABSCUTOFF for an LP + return LimitTerminationProto(isMax, LIMIT_OBJECTIVE, best_primal_bound, + best_dual_bound, + "Objective limit (LP_CUTOFF)"); break; case XPRS_LP_UNFINISHED: break; case XPRS_LP_UNBOUNDED: break; case XPRS_LP_CUTOFF_IN_DUAL: - return CutoffTerminationProto( - isMax, "Cutoff in dual (XPRS_LP_CUTOFF_IN_DUAL)"); + // This can happen if you set MIPABSCUTOFF for an LP + return LimitTerminationProto(isMax, LIMIT_OBJECTIVE, best_primal_bound, + best_dual_bound, + "Objective limit (LP_CUTOFF_IN_DUAL)"); case XPRS_LP_UNSOLVED: break; case XPRS_LP_NONCONVEX: From b0744a06ef7586a5f48253e2104d6b89da3fa9c2 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 13:52:35 +0200 Subject: [PATCH 32/70] Fix copy/paste error. --- ortools/math_opt/solvers/xpress_solver.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 3696ed94910..976a57450c0 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -312,7 +312,7 @@ class ScopedSolverContext { } if (parameters.has_objective_limit()) { // MIPABSCUTOFF also applies to LPs. - RETURN_IF_ERROR(Set(XPRS_MIPABSCUTOFF, parameters.cutoff_limit())); + RETURN_IF_ERROR(Set(XPRS_MIPABSCUTOFF, parameters.objective_limit())); } if (parameters.has_best_bound_limit()) { warnings.emplace_back("XpressSolver does not support best_bound_limit"); From 94ad8429ac8028a2e81a20479f16d3341ecde335 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 13:58:08 +0200 Subject: [PATCH 33/70] Do not support `objective_limit`. --- ortools/math_opt/solvers/xpress_solver.cc | 13 +++++++++---- ortools/math_opt/solvers/xpress_solver_test.cc | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 976a57450c0..5dca69af4ac 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -311,8 +311,13 @@ class ScopedSolverContext { RETURN_IF_ERROR(Set(XPRS_MIPABSCUTOFF, parameters.cutoff_limit())); } if (parameters.has_objective_limit()) { - // MIPABSCUTOFF also applies to LPs. - RETURN_IF_ERROR(Set(XPRS_MIPABSCUTOFF, parameters.objective_limit())); + // In Xpress you can apply MIPABSCUTOFF also to LPs. + // However, or tools applies both cutoff_limit and objective_limit + // to LPs and distinguishes the two, i.e., expect different return + // values depending on what is set. Since we cannot easily make this + // distinction, we do not support objective_limit. Users should just + // use cutoff_limit with LPs as well. + warnings.emplace_back("XpressSolver does not support objective_limit"); } if (parameters.has_best_bound_limit()) { warnings.emplace_back("XpressSolver does not support best_bound_limit"); @@ -1054,7 +1059,7 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( break; case XPRS_LP_CUTOFF: // This can happen if you set MIPABSCUTOFF for an LP - return LimitTerminationProto(isMax, LIMIT_OBJECTIVE, best_primal_bound, + return LimitTerminationProto(isMax, LIMIT_CUTOFF, best_primal_bound, best_dual_bound, "Objective limit (LP_CUTOFF)"); break; @@ -1064,7 +1069,7 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( break; case XPRS_LP_CUTOFF_IN_DUAL: // This can happen if you set MIPABSCUTOFF for an LP - return LimitTerminationProto(isMax, LIMIT_OBJECTIVE, best_primal_bound, + return LimitTerminationProto(isMax, LIMIT_CUTOFF, best_primal_bound, best_dual_bound, "Objective limit (LP_CUTOFF_IN_DUAL)"); case XPRS_LP_UNSOLVED: diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 6b31e9a9246..5162ba4702f 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -68,7 +68,7 @@ INSTANTIATE_TEST_SUITE_P( // generate enough variability for this /*supports_presolve=*/true, /*supports_cutoff=*/true, - /*supports_objective_limit=*/true, + /*supports_objective_limit=*/false, // See comments in xpress_solver.cc /*supports_best_bound_limit=*/false, /*reports_limits=*/false))); From 15d4ca8b29352a1ca7c446d9e6f4b8ae84d854eb Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 16:56:39 +0200 Subject: [PATCH 34/70] Fix some test failures. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 6 ++- ortools/math_opt/solvers/xpress/g_xpress.h | 1 + ortools/math_opt/solvers/xpress_solver.cc | 54 +++++++++++++++++---- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 6518b1e66d8..57376b16839 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -393,9 +393,13 @@ absl::Status Xpress::GetDuals(int* p_status, double* duals, int first, int last) { return ToStatus(XPRSgetduals(xpress_model_, p_status, duals, first, last)); } -absl::Status Xpress::GetSolution(int* p_status, double *x, int first, +absl::Status Xpress::GetSolution(int* p_status, double* x, int first, int last) { return ToStatus(XPRSgetsolution(xpress_model_, p_status, x, first, last)); } +absl::Status Xpress::GetRedCosts(int* p_status, double* dj, int first, + int last) { + return ToStatus(XPRSgetredcosts(xpress_model_, p_status, dj, first, last)); +} } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index d99a5bd3b2b..4f017b2ab14 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -142,6 +142,7 @@ class Xpress { absl::StatusOr IsMIP() const; absl::Status GetDuals(int* p_status, double* duals, int first, int last); absl::Status GetSolution(int* p_status, double* x, int first, int last); + absl::Status GetRedCosts(int* p_status, double* dj, int first, int last); private: XPRSprob xpress_model_; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 5dca69af4ac..c8743e3cc9f 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -810,9 +810,14 @@ absl::StatusOr XpressSolver::GetLpSolution( .ok(); SolutionProto solution{}; + bool storeSolutions = (solvestatus_ == XPRS_SOLVESTATUS_STOPPED || + solvestatus_ == XPRS_SOLVESTATUS_COMPLETED); if (isPrimalFeasible()) { - // Handle primal solution + // The preferred methods for obtaining primal information are + // XPRSgetsolution() and XPRSgetslacks() (not used here) + RETURN_IF_ERROR( + xpress_->GetSolution(nullptr, primals.data(), 0, nVars - 1)); solution.mutable_primal_solution()->set_feasibility_status( getPrimalSolutionStatus()); ASSIGN_OR_RETURN(const double primalBound, GetBestPrimalBound()); @@ -821,10 +826,26 @@ absl::StatusOr XpressSolver::GetLpSolution( primals, variables_map_, *solution.mutable_primal_solution()->mutable_variable_values(), model_parameters.variable_values_filter()); + } else if (storeSolutions) { + // Even if we are not primal feasible, store the results we obtained + // from XPRSgetlpsolution(). The feasibility status of this vector + // is undetermined, though. + solution.mutable_primal_solution()->set_feasibility_status( + SOLUTION_STATUS_UNDETERMINED); + ASSIGN_OR_RETURN(const double primalBound, GetBestPrimalBound()); + solution.mutable_primal_solution()->set_objective_value(primalBound); + XpressVectorToSparseDoubleVector( + primals, variables_map_, + *solution.mutable_primal_solution()->mutable_variable_values(), + model_parameters.variable_values_filter()); } - if (hasSolution) { - // Add dual solution even if not feasible + if (isDualFeasible()) { + // The preferred methods for obtain dual information are XPRSgetduals() + // and XPRSgetredcosts(). + RETURN_IF_ERROR(xpress_->GetDuals(nullptr, duals.data(), 0, nCons - 1)); + RETURN_IF_ERROR( + xpress_->GetRedCosts(nullptr, reducedCosts.data(), 0, nVars - 1)); solution.mutable_dual_solution()->set_feasibility_status( getDualSolutionStatus()); ASSIGN_OR_RETURN(const double dualBound, GetBestDualBound()); @@ -837,6 +858,22 @@ absl::StatusOr XpressSolver::GetLpSolution( reducedCosts, variables_map_, *solution.mutable_dual_solution()->mutable_reduced_costs(), model_parameters.reduced_costs_filter()); + } else if (storeSolutions) { + // Even if we are not dual feasible, store the results we obtained from + // XPRSgetlpsolution(). The feasibility status of this vector + // is undetermined, though. + solution.mutable_dual_solution()->set_feasibility_status( + SOLUTION_STATUS_UNDETERMINED); + ASSIGN_OR_RETURN(const double dualBound, GetBestDualBound()); + solution.mutable_dual_solution()->set_objective_value(dualBound); + XpressVectorToSparseDoubleVector( + duals, linear_constraints_map_, + *solution.mutable_dual_solution()->mutable_dual_values(), + model_parameters.dual_values_filter()); + XpressVectorToSparseDoubleVector( + reducedCosts, variables_map_, + *solution.mutable_dual_solution()->mutable_reduced_costs(), + model_parameters.reduced_costs_filter()); } // Get basis @@ -1059,9 +1096,8 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( break; case XPRS_LP_CUTOFF: // This can happen if you set MIPABSCUTOFF for an LP - return LimitTerminationProto(isMax, LIMIT_CUTOFF, best_primal_bound, - best_dual_bound, - "Objective limit (LP_CUTOFF)"); + return NoSolutionFoundTerminationProto( + isMax, LIMIT_CUTOFF, std::nullopt, "Objective limit (LP_CUTOFF)"); break; case XPRS_LP_UNFINISHED: break; @@ -1069,9 +1105,9 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( break; case XPRS_LP_CUTOFF_IN_DUAL: // This can happen if you set MIPABSCUTOFF for an LP - return LimitTerminationProto(isMax, LIMIT_CUTOFF, best_primal_bound, - best_dual_bound, - "Objective limit (LP_CUTOFF_IN_DUAL)"); + return NoSolutionFoundTerminationProto( + isMax, LIMIT_CUTOFF, std::nullopt, + "Objective limit (LP_CUTOFF_IN_DUAL)"); case XPRS_LP_UNSOLVED: break; case XPRS_LP_NONCONVEX: From d4029bacd9a3b80479f66e8a9ddaea41f88154fd Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 17:06:15 +0200 Subject: [PATCH 35/70] Add tests for unsupported parameters. --- .../math_opt/solvers/xpress_solver_test.cc | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 5162ba4702f..436c4946d7d 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -117,6 +117,35 @@ INSTANTIATE_TEST_SUITE_P(XpressInvalidInputTest, InvalidInputTest, SolverType::kXpress, /*use_integer_variables=*/false))); +InvalidParameterTestParams InvalidObjectiveLimitParameters() { + SolveParameters params; + params.objective_limit = 1.5; + return InvalidParameterTestParams( + SolverType::kXpress, std::move(params), + {"XpressSolver does not support objective_limit"}); +} + +InvalidParameterTestParams InvalidBestBoundLimitParameters() { + SolveParameters params; + params.best_bound_limit = 1.5; + return InvalidParameterTestParams( + SolverType::kXpress, std::move(params), + {"XpressSolver does not support best_bound_limit"}); +} + +InvalidParameterTestParams InvalidSolutionPoolSizeParameters() { + SolveParameters params; + params.solution_pool_size = 2; + return InvalidParameterTestParams( + SolverType::kXpress, std::move(params), + {"XpressSolver does not support solution_pool_size"}); +} + +INSTANTIATE_TEST_SUITE_P(XpressInvalidParameterTest, InvalidParameterTest, + ValuesIn({InvalidObjectiveLimitParameters(), + InvalidBestBoundLimitParameters(), + InvalidSolutionPoolSizeParameters()})); + INSTANTIATE_TEST_SUITE_P(XpressGenericTest, GenericTest, testing::Values(GenericTestParameters( SolverType::kXpress, /*support_interrupter=*/true, From 83e9e030fca72774beb20c17b8c919312bb3a798 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 8 Oct 2025 17:14:11 +0200 Subject: [PATCH 36/70] Fix expected value for test. --- ortools/math_opt/solvers/xpress_solver_test.cc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 436c4946d7d..9b03d2a570d 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -95,12 +95,13 @@ INSTANTIATE_TEST_SUITE_P( GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(IncrementalLpTest); -INSTANTIATE_TEST_SUITE_P( - XpressMessageCallbackTest, MessageCallbackTest, - testing::Values(MessageCallbackTestParams(SolverType::kXpress, - /*support_message_callback=*/true, - /*support_interrupter=*/true, - /*integer_variables=*/true, ""))); +INSTANTIATE_TEST_SUITE_P(XpressMessageCallbackTest, MessageCallbackTest, + testing::Values(MessageCallbackTestParams( + SolverType::kXpress, + /*support_message_callback=*/true, + /*support_interrupter=*/true, + /*integer_variables=*/true, + /*ending_substring*/ "Optimal solution found"))); INSTANTIATE_TEST_SUITE_P( XpressCallbackTest, CallbackTest, From 69efba4b38a1d80ee414706f0e51562d8bc3f972 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 9 Oct 2025 07:45:07 +0200 Subject: [PATCH 37/70] Add status tests with integer variables. --- ortools/math_opt/solvers/xpress_solver_test.cc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 9b03d2a570d..30105ce847c 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -261,6 +261,15 @@ std::vector MakeStatusTestConfigs() { /*supports_node_limit=*/true, /*support_interrupter=*/true, /*supports_one_thread=*/true)); } + // Add a test with default LP algorithm and integer variables + SolveParameters solve_parameters = {.lp_algorithm = std::nullopt}; + test_parameters.push_back(StatusTestParameters( + SolverType::kXpress, solve_parameters, + /*disallow_primal_or_dual_infeasible=*/false, + /*supports_iteration_limit=*/true, + /*use_integer_variables=*/true, + /*supports_node_limit=*/true, + /*support_interrupter=*/true, /*supports_one_thread=*/true)); return test_parameters; } From 7475b5eb8bd64951429224fa4e589766b452b009 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 9 Oct 2025 07:50:16 +0200 Subject: [PATCH 38/70] Fix handling of memory limit. --- ortools/math_opt/solvers/xpress_solver.cc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index c8743e3cc9f..4924832e9ad 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -1132,6 +1132,8 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( return TerminateForReason(isMax, TERMINATION_REASON_OTHER_ERROR, "Generic error"); case XPRS_STOP_MEMORYERROR: + // This can actually not happen since despite its name, this is + // not an error but indicates hitting a user defined memory limit return TerminateForReason(isMax, TERMINATION_REASON_OTHER_ERROR, "Memory error"); case XPRS_STOP_LICENSELOST: @@ -1175,6 +1177,13 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( return NoSolutionFoundTerminationProto( isMax, LIMIT_OTHER, std::nullopt, /** TODO: bound? */ "Work limit hit"); + case XPRS_STOP_MEMORYERROR: + // Despite its name, MEMORYERROR is not actually an error + // but instead indicates that we hit a user defined memory + // limit. + return NoSolutionFoundTerminationProto( + isMax, LIMIT_MEMORY, std::nullopt, /** TODO: bound? */ + "Memory limit hit"); default: return TerminateForReason(isMax, TERMINATION_REASON_NO_SOLUTION_FOUND); @@ -1206,6 +1215,13 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( return FeasibleTerminationProto(isMax, LIMIT_OTHER, best_primal_bound, best_dual_bound, "Work limit hit"); + case XPRS_STOP_MEMORYERROR: + // Despite its name, MEMORYERROR is not actually an error + // but instead indicates that we hit a user defined memory + // limit. + return FeasibleTerminationProto(isMax, LIMIT_MEMORY, + best_primal_bound, best_dual_bound, + "Memory limit hit"); default: return FeasibleTerminationProto(isMax, LIMIT_UNDETERMINED, best_primal_bound, best_dual_bound); From 0983ea997c1f164afc9b1e0cc1f2beb68bee4c4a Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 9 Oct 2025 08:13:15 +0200 Subject: [PATCH 39/70] Extend all tests to run with and without integer variables. --- .../math_opt/solvers/xpress_solver_test.cc | 112 ++++++++++++------ 1 file changed, 75 insertions(+), 37 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 30105ce847c..753092f9838 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -37,6 +37,11 @@ #include "ortools/math_opt/solver_tests/status_tests.h" #include "ortools/third_party_solvers/xpress_environment.h" +/** A string in the log file that indicates that the solution process + * finished successfully and found the optimal solution. + */ +#define OPTIMAL_SOLUTION_FOUND "Optimal solution found" + namespace operations_research { namespace math_opt { namespace { @@ -95,27 +100,44 @@ INSTANTIATE_TEST_SUITE_P( GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(IncrementalLpTest); -INSTANTIATE_TEST_SUITE_P(XpressMessageCallbackTest, MessageCallbackTest, - testing::Values(MessageCallbackTestParams( - SolverType::kXpress, - /*support_message_callback=*/true, - /*support_interrupter=*/true, - /*integer_variables=*/true, - /*ending_substring*/ "Optimal solution found"))); +INSTANTIATE_TEST_SUITE_P( + XpressMessageCallbackTest, MessageCallbackTest, + testing::ValuesIn( + {MessageCallbackTestParams(SolverType::kXpress, + /*support_message_callback=*/true, + /*support_interrupter=*/true, + /*integer_variables=*/false, + /*ending_substring*/ OPTIMAL_SOLUTION_FOUND), + MessageCallbackTestParams( + SolverType::kXpress, + /*support_message_callback=*/true, + /*support_interrupter=*/true, + /*integer_variables=*/true, + /*ending_substring*/ OPTIMAL_SOLUTION_FOUND)})); INSTANTIATE_TEST_SUITE_P( XpressCallbackTest, CallbackTest, - testing::Values(CallbackTestParams(SolverType::kXpress, - /*integer_variables=*/true, - /*add_lazy_constraints=*/false, - /*add_cuts=*/false, - /*supported_events=*/{}, - /*all_solutions=*/std::nullopt, - /*reaches_cut_callback*/ std::nullopt))); + testing::ValuesIn( + {CallbackTestParams(SolverType::kXpress, + /*integer_variables=*/false, + /*add_lazy_constraints=*/false, + /*add_cuts=*/false, + /*supported_events=*/{}, + /*all_solutions=*/std::nullopt, + /*reaches_cut_callback*/ std::nullopt), + CallbackTestParams(SolverType::kXpress, + /*integer_variables=*/true, + /*add_lazy_constraints=*/false, + /*add_cuts=*/false, + /*supported_events=*/{}, + /*all_solutions=*/std::nullopt, + /*reaches_cut_callback*/ std::nullopt)})); INSTANTIATE_TEST_SUITE_P(XpressInvalidInputTest, InvalidInputTest, testing::Values(InvalidInputTestParameters( SolverType::kXpress, + // Invalid parameters do not depend on integer + // variables /*use_integer_variables=*/false))); InvalidParameterTestParams InvalidObjectiveLimitParameters() { @@ -147,11 +169,17 @@ INSTANTIATE_TEST_SUITE_P(XpressInvalidParameterTest, InvalidParameterTest, InvalidBestBoundLimitParameters(), InvalidSolutionPoolSizeParameters()})); -INSTANTIATE_TEST_SUITE_P(XpressGenericTest, GenericTest, - testing::Values(GenericTestParameters( - SolverType::kXpress, /*support_interrupter=*/true, - /*integer_variables=*/true, - /*expected_log=*/"Optimal solution found"))); +INSTANTIATE_TEST_SUITE_P( + XpressGenericTest, GenericTest, + testing::ValuesIn( + {GenericTestParameters(SolverType::kXpress, + /*support_interrupter=*/true, + /*integer_variables=*/false, + /*expected_log=*/OPTIMAL_SOLUTION_FOUND), + GenericTestParameters(SolverType::kXpress, + /*support_interrupter=*/true, + /*integer_variables=*/true, + /*expected_log=*/OPTIMAL_SOLUTION_FOUND)})); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TimeLimitTest); @@ -207,31 +235,41 @@ INSTANTIATE_TEST_SUITE_P( XpressIncrementalMultiObjectiveTest, IncrementalMultiObjectiveTest, testing::Values(GetXpressMultiObjectiveTestParameters())); -QpTestParameters GetXpressQpTestParameters() { - return QpTestParameters(SolverType::kXpress, SolveParameters(), - /*qp_support=*/QpSupportType::kConvexQp, - /*supports_incrementalism_not_modifying_qp=*/false, - /*supports_qp_incrementalism=*/false, - /*use_integer_variables=*/false); +std::vector GetXpressQpTestParameters() { + std::vector test_parameters; + for (int i = 0; i < 2; ++i) { + test_parameters.push_back( + QpTestParameters(SolverType::kXpress, SolveParameters(), + /*qp_support=*/QpSupportType::kConvexQp, + /*supports_incrementalism_not_modifying_qp=*/false, + /*supports_qp_incrementalism=*/false, + /*use_integer_variables=*/i != 0)); + } + return test_parameters; } INSTANTIATE_TEST_SUITE_P(XpressSimpleQpTest, SimpleQpTest, - testing::Values(GetXpressQpTestParameters())); + testing::ValuesIn(GetXpressQpTestParameters())); INSTANTIATE_TEST_SUITE_P(XpressIncrementalQpTest, IncrementalQpTest, - testing::Values(GetXpressQpTestParameters())); + testing::ValuesIn(GetXpressQpTestParameters())); INSTANTIATE_TEST_SUITE_P(XpressQpDualsTest, QpDualsTest, - testing::Values(GetXpressQpTestParameters())); - -QcTestParameters GetXpressQcTestParameters() { - return QcTestParameters(SolverType::kXpress, SolveParameters(), - /*supports_qc=*/false, - /*supports_incremental_add_and_deletes=*/false, - /*supports_incremental_variable_deletions=*/false, - /*use_integer_variables=*/false); + testing::ValuesIn(GetXpressQpTestParameters())); + +std::vector GetXpressQcTestParameters() { + std::vector test_parameters; + for (int i = 0; i < 2; ++i) { + test_parameters.push_back( + QcTestParameters(SolverType::kXpress, SolveParameters(), + /*supports_qc=*/false, + /*supports_incremental_add_and_deletes=*/false, + /*supports_incremental_variable_deletions=*/false, + /*use_integer_variables=*/i != 0)); + } + return test_parameters; } INSTANTIATE_TEST_SUITE_P(XpressSimpleQcTest, SimpleQcTest, - testing::Values(GetXpressQcTestParameters())); + testing::ValuesIn(GetXpressQcTestParameters())); INSTANTIATE_TEST_SUITE_P(XpressIncrementalQcTest, IncrementalQcTest, - testing::Values(GetXpressQcTestParameters())); + testing::ValuesIn(GetXpressQcTestParameters())); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(QcDualsTest); SecondOrderConeTestParameters GetXpressSecondOrderConeTestParameters() { From 285d1d48c54a2ea7fd0d63d75f62bc75ac24e114 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 9 Oct 2025 09:16:51 +0200 Subject: [PATCH 40/70] Propagate variable types to low-level Xpress. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 68 ++++++++++++++------- ortools/math_opt/solvers/xpress/g_xpress.h | 8 +-- ortools/math_opt/solvers/xpress_solver.cc | 22 +++++-- 3 files changed, 64 insertions(+), 34 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 57376b16839..50c698c3c7c 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -110,38 +110,62 @@ void Xpress::initIntControlDefaults() { } } -absl::Status Xpress::AddVars(const absl::Span obj, - const absl::Span lb, - const absl::Span ub, - const absl::Span vtype) { - return AddVars({}, {}, {}, obj, lb, ub, vtype); -} - -absl::Status Xpress::AddVars(const absl::Span vbegin, - const absl::Span vind, - const absl::Span vval, +// All arguments can be empty to indicate "use default values". +// Default objective value: 0 +// Default lower bound: 0 +// Default upper bound: infinity +// Default type: continuous +absl::Status Xpress::AddVars(std::size_t count, const absl::Span obj, const absl::Span lb, const absl::Span ub, const absl::Span vtype) { - if (checkInt32Overflow(lb.size())) { + ASSIGN_OR_RETURN(int const oldCols, GetIntAttr(XPRS_COLS)); + if (checkInt32Overflow(count) || + checkInt32Overflow(std::size_t(oldCols) + std::size_t(count))) { return absl::InvalidArgumentError( "XPRESS cannot handle more than 2^31 variables"); } - const int num_vars = static_cast(lb.size()); - 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)) { + const int num_vars = static_cast(count); + double const* c_obj = nullptr; + if (!obj.empty()) { + if (obj.size() != count) + return absl::InvalidArgumentError( + "Xpress::AddVars objective argument has bad size"); + c_obj = obj.data(); + } + if (!lb.empty() && lb.size() != count) return absl::InvalidArgumentError( - "Xpress::AddVars arguments are of inconsistent sizes"); + "Xpress::AddVars lower bound argument has bad size"); + if (!ub.empty() && ub.size() != count) + return absl::InvalidArgumentError( + "Xpress::AddVars upper bound argument has bad size"); + std::vector colind; + if (!vtype.empty()) { + if (vtype.size() != count) + return absl::InvalidArgumentError( + "Xpress::AddVars type argument has bad size"); + colind.reserve(count); // So that we don't OOM after adding } - double* c_obj = nullptr; - if (!obj.empty()) { - c_obj = const_cast(obj.data()); + // XPRSaddcols64() allows to add variables with more than INT_MAX + // non-zero coefficients here. It does NOT allow adding more than INT_MAX + // variables. + // Since we don't add any non-zeros here, it is safe to use XPRSaddcols(). + RETURN_IF_ERROR(ToStatus(XPRSaddcols( + xpress_model_, num_vars, 0, c_obj, nullptr, nullptr, nullptr, + lb.size() ? lb.data() : nullptr, ub.size() ? ub.data() : nullptr))); + if (!vtype.empty()) { + for (int i = 0; i < num_vars; ++i) colind.push_back(oldCols + i); + int const ret = + XPRSchgcoltype(xpress_model_, num_vars, colind.data(), vtype.data()); + if (ret != 0) { + // Changing the column type failed. We must roll back XPRSaddcols() and + // then return an error. + XPRSdelcols(xpress_model_, num_vars, colind.data()); + } + return ToStatus(ret); } - // TODO: look into int64_t support for number of vars (use XPRSaddcols64) - return ToStatus(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 sense, diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 4f017b2ab14..aeabb2ffd29 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -68,13 +68,7 @@ class Xpress { absl::StatusOr GetDoubleAttr(int attribute) const; - absl::Status AddVars(absl::Span obj, - absl::Span lb, absl::Span ub, - absl::Span vtype); - - absl::Status AddVars(absl::Span vbegin, absl::Span vind, - absl::Span vval, - absl::Span obj, + absl::Status AddVars(std::size_t count, absl::Span obj, absl::Span lb, absl::Span ub, absl::Span vtype); diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 4924832e9ad..ad4d829c52c 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -539,15 +539,27 @@ absl::Status XpressSolver::AddNewVariables( const int num_new_variables = new_variables.lower_bounds().size(); std::vector variable_type(num_new_variables); int n_variables = xpress_->GetNumberOfVariables(); + bool have_integers = false; for (int j = 0; j < num_new_variables; ++j) { const VarId id = new_variables.ids(j); gtl::InsertOrDie(&variables_map_, id, j + n_variables); - variable_type[j] = - new_variables.integers(j) ? XPRS_INTEGER : XPRS_CONTINUOUS; + if (new_variables.integers(j)) { + // Note: ortools does not distinguish between binary variables and + // integer variables in {0,1} + variable_type[j] = XPRS_INTEGER; + have_integers = true; + } else { + variable_type[j] = XPRS_CONTINUOUS; + } } - RETURN_IF_ERROR(xpress_->AddVars({}, new_variables.lower_bounds(), - new_variables.upper_bounds(), - variable_type)); + if (!have_integers) { + // There are no integer variables, so we clear variable_type to + // safe the call to XPRSchgcoltype() in AddVars() + variable_type.clear(); + } + RETURN_IF_ERROR( + xpress_->AddVars(num_new_variables, {}, new_variables.lower_bounds(), + new_variables.upper_bounds(), variable_type)); // Not adding names for performance (have to call XPRSaddnames) // TODO: keep names in a cache and add them when needed From 360969ca5934ffa1bf70bc15a853b2f2545d3147 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 9 Oct 2025 11:37:09 +0200 Subject: [PATCH 41/70] Do not attach empty solutions for MIPs. --- ortools/math_opt/solvers/xpress_solver.cc | 185 +++++++++++----------- ortools/math_opt/solvers/xpress_solver.h | 9 +- 2 files changed, 92 insertions(+), 102 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index ad4d829c52c..54282b0c8a3 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -737,9 +737,7 @@ absl::StatusOr XpressSolver::ExtractSolveResultProto( absl::Time start, const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters) { SolveResultProto result; - ASSIGN_OR_RETURN(SolutionProto solution, - GetSolution(model_parameters, solve_parameters)); - *result.add_solutions() = std::move(solution); + RETURN_IF_ERROR(AppendSolution(result, model_parameters, solve_parameters)); ASSIGN_OR_RETURN(*result.mutable_solve_stats(), GetSolveStats(start)); ASSIGN_OR_RETURN(const double best_primal_bound, GetBestPrimalBound()); ASSIGN_OR_RETURN(const double best_dual_bound, GetBestDualBound()); @@ -779,16 +777,17 @@ absl::StatusOr XpressSolver::GetBestDualBound() const { return objsen * kMinusInf; } -absl::StatusOr XpressSolver::GetSolution( +absl::Status XpressSolver::AppendSolution( + SolveResultProto& solve_result, const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters) { + int const nVars = xpress_->GetNumberOfVariables(); if (is_mip_) { - int nVars = xpress_->GetNumberOfVariables(); std::vector x(nVars); int avail; RETURN_IF_ERROR(xpress_->GetSolution(&avail, x.data(), 0, nVars - 1)); - SolutionProto solution{}; if (avail != XPRS_SOLAVAILABLE_NOTFOUND) { + SolutionProto solution{}; solution.mutable_primal_solution()->set_feasibility_status( getPrimalSolutionStatus()); ASSIGN_OR_RETURN(const double objval, @@ -798,102 +797,96 @@ absl::StatusOr XpressSolver::GetSolution( x, variables_map_, *solution.mutable_primal_solution()->mutable_variable_values(), model_parameters.variable_values_filter()); + *solve_result.add_solutions() = std::move(solution); } - return solution; } else { - return GetLpSolution(model_parameters, solve_parameters); - } -} + // Fetch all results from XPRESS + int const nCons = xpress_->GetNumberOfConstraints(); + std::vector primals(nVars); + std::vector duals(nCons); + std::vector reducedCosts(nVars); + + auto hasSolution = + xpress_ + ->GetLpSol(absl::MakeSpan(primals), absl::MakeSpan(duals), + absl::MakeSpan(reducedCosts)) + .ok(); -absl::StatusOr XpressSolver::GetLpSolution( - const ModelSolveParametersProto& model_parameters, - const SolveParametersProto& solve_parameters) { - // Fetch all results from XPRESS - int nVars = xpress_->GetNumberOfVariables(); - int nCons = xpress_->GetNumberOfConstraints(); - std::vector primals(nVars); - std::vector duals(nCons); - std::vector reducedCosts(nVars); - - auto hasSolution = - xpress_ - ->GetLpSol(absl::MakeSpan(primals), absl::MakeSpan(duals), - absl::MakeSpan(reducedCosts)) - .ok(); - - SolutionProto solution{}; - bool storeSolutions = (solvestatus_ == XPRS_SOLVESTATUS_STOPPED || - solvestatus_ == XPRS_SOLVESTATUS_COMPLETED); - - if (isPrimalFeasible()) { - // The preferred methods for obtaining primal information are - // XPRSgetsolution() and XPRSgetslacks() (not used here) - RETURN_IF_ERROR( - xpress_->GetSolution(nullptr, primals.data(), 0, nVars - 1)); - solution.mutable_primal_solution()->set_feasibility_status( - getPrimalSolutionStatus()); - ASSIGN_OR_RETURN(const double primalBound, GetBestPrimalBound()); - solution.mutable_primal_solution()->set_objective_value(primalBound); - XpressVectorToSparseDoubleVector( - primals, variables_map_, - *solution.mutable_primal_solution()->mutable_variable_values(), - model_parameters.variable_values_filter()); - } else if (storeSolutions) { - // Even if we are not primal feasible, store the results we obtained - // from XPRSgetlpsolution(). The feasibility status of this vector - // is undetermined, though. - solution.mutable_primal_solution()->set_feasibility_status( - SOLUTION_STATUS_UNDETERMINED); - ASSIGN_OR_RETURN(const double primalBound, GetBestPrimalBound()); - solution.mutable_primal_solution()->set_objective_value(primalBound); - XpressVectorToSparseDoubleVector( - primals, variables_map_, - *solution.mutable_primal_solution()->mutable_variable_values(), - model_parameters.variable_values_filter()); - } + SolutionProto solution{}; + bool storeSolutions = (solvestatus_ == XPRS_SOLVESTATUS_STOPPED || + solvestatus_ == XPRS_SOLVESTATUS_COMPLETED); - if (isDualFeasible()) { - // The preferred methods for obtain dual information are XPRSgetduals() - // and XPRSgetredcosts(). - RETURN_IF_ERROR(xpress_->GetDuals(nullptr, duals.data(), 0, nCons - 1)); - RETURN_IF_ERROR( - xpress_->GetRedCosts(nullptr, reducedCosts.data(), 0, nVars - 1)); - solution.mutable_dual_solution()->set_feasibility_status( - getDualSolutionStatus()); - ASSIGN_OR_RETURN(const double dualBound, GetBestDualBound()); - solution.mutable_dual_solution()->set_objective_value(dualBound); - XpressVectorToSparseDoubleVector( - duals, linear_constraints_map_, - *solution.mutable_dual_solution()->mutable_dual_values(), - model_parameters.dual_values_filter()); - XpressVectorToSparseDoubleVector( - reducedCosts, variables_map_, - *solution.mutable_dual_solution()->mutable_reduced_costs(), - model_parameters.reduced_costs_filter()); - } else if (storeSolutions) { - // Even if we are not dual feasible, store the results we obtained from - // XPRSgetlpsolution(). The feasibility status of this vector - // is undetermined, though. - solution.mutable_dual_solution()->set_feasibility_status( - SOLUTION_STATUS_UNDETERMINED); - ASSIGN_OR_RETURN(const double dualBound, GetBestDualBound()); - solution.mutable_dual_solution()->set_objective_value(dualBound); - XpressVectorToSparseDoubleVector( - duals, linear_constraints_map_, - *solution.mutable_dual_solution()->mutable_dual_values(), - model_parameters.dual_values_filter()); - XpressVectorToSparseDoubleVector( - reducedCosts, variables_map_, - *solution.mutable_dual_solution()->mutable_reduced_costs(), - model_parameters.reduced_costs_filter()); - } + if (isPrimalFeasible()) { + // The preferred methods for obtaining primal information are + // XPRSgetsolution() and XPRSgetslacks() (not used here) + RETURN_IF_ERROR( + xpress_->GetSolution(nullptr, primals.data(), 0, nVars - 1)); + solution.mutable_primal_solution()->set_feasibility_status( + getPrimalSolutionStatus()); + ASSIGN_OR_RETURN(const double primalBound, GetBestPrimalBound()); + solution.mutable_primal_solution()->set_objective_value(primalBound); + XpressVectorToSparseDoubleVector( + primals, variables_map_, + *solution.mutable_primal_solution()->mutable_variable_values(), + model_parameters.variable_values_filter()); + } else if (storeSolutions) { + // Even if we are not primal feasible, store the results we obtained + // from XPRSgetlpsolution(). The feasibility status of this vector + // is undetermined, though. + solution.mutable_primal_solution()->set_feasibility_status( + SOLUTION_STATUS_UNDETERMINED); + ASSIGN_OR_RETURN(const double primalBound, GetBestPrimalBound()); + solution.mutable_primal_solution()->set_objective_value(primalBound); + XpressVectorToSparseDoubleVector( + primals, variables_map_, + *solution.mutable_primal_solution()->mutable_variable_values(), + model_parameters.variable_values_filter()); + } - // Get basis - ASSIGN_OR_RETURN(auto basis, GetBasisIfAvailable(solve_parameters)); - if (basis.has_value()) { - *solution.mutable_basis() = std::move(*basis); + if (isDualFeasible()) { + // The preferred methods for obtain dual information are XPRSgetduals() + // and XPRSgetredcosts(). + RETURN_IF_ERROR(xpress_->GetDuals(nullptr, duals.data(), 0, nCons - 1)); + RETURN_IF_ERROR( + xpress_->GetRedCosts(nullptr, reducedCosts.data(), 0, nVars - 1)); + solution.mutable_dual_solution()->set_feasibility_status( + getDualSolutionStatus()); + ASSIGN_OR_RETURN(const double dualBound, GetBestDualBound()); + solution.mutable_dual_solution()->set_objective_value(dualBound); + XpressVectorToSparseDoubleVector( + duals, linear_constraints_map_, + *solution.mutable_dual_solution()->mutable_dual_values(), + model_parameters.dual_values_filter()); + XpressVectorToSparseDoubleVector( + reducedCosts, variables_map_, + *solution.mutable_dual_solution()->mutable_reduced_costs(), + model_parameters.reduced_costs_filter()); + } else if (storeSolutions) { + // Even if we are not dual feasible, store the results we obtained from + // XPRSgetlpsolution(). The feasibility status of this vector + // is undetermined, though. + solution.mutable_dual_solution()->set_feasibility_status( + SOLUTION_STATUS_UNDETERMINED); + ASSIGN_OR_RETURN(const double dualBound, GetBestDualBound()); + solution.mutable_dual_solution()->set_objective_value(dualBound); + XpressVectorToSparseDoubleVector( + duals, linear_constraints_map_, + *solution.mutable_dual_solution()->mutable_dual_values(), + model_parameters.dual_values_filter()); + XpressVectorToSparseDoubleVector( + reducedCosts, variables_map_, + *solution.mutable_dual_solution()->mutable_reduced_costs(), + model_parameters.reduced_costs_filter()); + } + + // Get basis + ASSIGN_OR_RETURN(auto basis, GetBasisIfAvailable(solve_parameters)); + if (basis.has_value()) { + *solution.mutable_basis() = std::move(*basis); + } + *solve_result.add_solutions() = std::move(solution); } - return solution; + return absl::OkStatus(); } bool XpressSolver::isPrimalFeasible() const { diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index a882df35738..fd90f42a599 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -107,9 +107,9 @@ class XpressSolver : public SolverInterface { absl::StatusOr ExtractSolveResultProto( absl::Time start, const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters); - absl::StatusOr GetSolution( - const ModelSolveParametersProto& model_parameters, - const SolveParametersProto& solve_parameters); + absl::Status AppendSolution(SolveResultProto& solve_result, + const ModelSolveParametersProto& model_parameters, + const SolveParametersProto& solve_parameters); absl::StatusOr GetSolveStats(absl::Time start) const; absl::StatusOr GetBestPrimalBound() const; @@ -118,9 +118,6 @@ class XpressSolver : public SolverInterface { absl::StatusOr ConvertTerminationReason( double best_primal_bound, double best_dual_bound) const; - absl::StatusOr GetLpSolution( - const ModelSolveParametersProto& model_parameters, - const SolveParametersProto& solve_parameters); bool isPrimalFeasible() const; bool isDualFeasible() const; From 53498c041c9429bb0ee7b10879fd320fb089322a Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 9 Oct 2025 12:06:23 +0200 Subject: [PATCH 42/70] Fix expected valus for MIPs. --- .../math_opt/solvers/xpress_solver_test.cc | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 753092f9838..5437839839d 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -38,9 +38,13 @@ #include "ortools/third_party_solvers/xpress_environment.h" /** A string in the log file that indicates that the solution process - * finished successfully and found the optimal solution. + * finished successfully and found the optimal solution for LPs. */ -#define OPTIMAL_SOLUTION_FOUND "Optimal solution found" +#define OPTIMAL_SOLUTION_FOUND_LP "Optimal solution found" +/** A string in the log file that indicates that the solution process + * finished successfully and found the optimal solution for MIPs. + */ +#define OPTIMAL_SOLUTION_FOUND_MIP "*** Search completed ***" namespace operations_research { namespace math_opt { @@ -102,18 +106,18 @@ GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(IncrementalLpTest); INSTANTIATE_TEST_SUITE_P( XpressMessageCallbackTest, MessageCallbackTest, - testing::ValuesIn( - {MessageCallbackTestParams(SolverType::kXpress, - /*support_message_callback=*/true, - /*support_interrupter=*/true, - /*integer_variables=*/false, - /*ending_substring*/ OPTIMAL_SOLUTION_FOUND), - MessageCallbackTestParams( - SolverType::kXpress, - /*support_message_callback=*/true, - /*support_interrupter=*/true, - /*integer_variables=*/true, - /*ending_substring*/ OPTIMAL_SOLUTION_FOUND)})); + testing::ValuesIn({MessageCallbackTestParams( + SolverType::kXpress, + /*support_message_callback=*/true, + /*support_interrupter=*/true, + /*integer_variables=*/false, + /*ending_substring*/ OPTIMAL_SOLUTION_FOUND_LP), + MessageCallbackTestParams( + SolverType::kXpress, + /*support_message_callback=*/true, + /*support_interrupter=*/true, + /*integer_variables=*/true, + /*ending_substring*/ OPTIMAL_SOLUTION_FOUND_MIP)})); INSTANTIATE_TEST_SUITE_P( XpressCallbackTest, CallbackTest, @@ -175,11 +179,11 @@ INSTANTIATE_TEST_SUITE_P( {GenericTestParameters(SolverType::kXpress, /*support_interrupter=*/true, /*integer_variables=*/false, - /*expected_log=*/OPTIMAL_SOLUTION_FOUND), + /*expected_log=*/OPTIMAL_SOLUTION_FOUND_LP), GenericTestParameters(SolverType::kXpress, /*support_interrupter=*/true, /*integer_variables=*/true, - /*expected_log=*/OPTIMAL_SOLUTION_FOUND)})); + /*expected_log=*/OPTIMAL_SOLUTION_FOUND_MIP)})); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TimeLimitTest); From 424d179e151258c4c3d4544abf1883777dc1e5c2 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 9 Oct 2025 12:47:12 +0200 Subject: [PATCH 43/70] Bail out if library has bad version. --- .../third_party_solvers/xpress_environment.cc | 19 ++++++++++++- .../third_party_solvers/xpress_environment.h | 27 ++++++++++--------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index a7f2508bf00..f57eb969816 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -29,6 +29,7 @@ #include "absl/strings/str_join.h" #include "absl/synchronization/mutex.h" #include "ortools/base/logging.h" +#include "ortools/base/status_builder.h" #include "ortools/third_party_solvers/dynamic_library.h" namespace operations_research { @@ -52,6 +53,7 @@ std::function XPRSgetlicerrmsg = nullptr; std::function XPRSlicense = nullptr; std::function XPRSgetbanner = nullptr; std::function XPRSgetversion = nullptr; +std::function XPRSgetversionnumbers = nullptr; std::function XPRSsetprobname = nullptr; std::function XPRSsetdefaultcontrol = nullptr; std::function XPRSinterrupt = nullptr; @@ -132,6 +134,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSlicense, "XPRSlicense"); xpress_dynamic_library->GetFunction(&XPRSgetbanner, "XPRSgetbanner"); xpress_dynamic_library->GetFunction(&XPRSgetversion, "XPRSgetversion"); + xpress_dynamic_library->GetFunction(&XPRSgetversionnumbers, "XPRSgetversionnumbers"); xpress_dynamic_library->GetFunction(&XPRSsetprobname, "XPRSsetprobname"); xpress_dynamic_library->GetFunction(&XPRSsetdefaultcontrol, "XPRSsetdefaultcontrol"); xpress_dynamic_library->GetFunction(&XPRSinterrupt, "XPRSinterrupt"); @@ -274,7 +277,21 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath) { if (xpress_library->LibraryIsLoaded()) { LOG(INFO) << "Loading all Xpress functions"; LoadXpressFunctions(xpress_library); - *xpress_load_status = absl::OkStatus(); + // Make sure the library we just loaded is recent enough. + int major = -1, minor = -1, build = -1; + if (!XPRSgetversionnumbers || + XPRSgetversionnumbers(&major, &minor, &build) != 0) + *xpress_load_status = + util::StatusBuilder(absl::StatusCode::kNotFound) + << "Xpress optimizer library too old, need at least version " + << XPVERSION; + else if (major < XPVERSION) + *xpress_load_status = util::StatusBuilder(absl::StatusCode::kNotFound) + << "Xpress optimizer library version " << major + << " too old, need at least version " + << XPVERSION; + else + *xpress_load_status = absl::OkStatus(); } else { *xpress_load_status = absl::NotFoundError( absl::StrCat("Could not find the Xpress shared library. Looked in: [", diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index 0eaa68ec97b..cb815decfb5 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -94,29 +94,29 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); // *************************************************************************** // * values related to SOLAVAILABLE * // *************************************************************************** -#define XPRS_SOLAVAILABLE_NOTFOUND 0 -#define XPRS_SOLAVAILABLE_OPTIMAL 1 -#define XPRS_SOLAVAILABLE_FEASIBLE 2 +#define XPRS_SOLAVAILABLE_NOTFOUND 0 +#define XPRS_SOLAVAILABLE_OPTIMAL 1 +#define XPRS_SOLAVAILABLE_FEASIBLE 2 // *************************************************************************** // * values related to SOLVESTATUS * // *************************************************************************** -#define XPRS_SOLVESTATUS_UNSTARTED 0 -#define XPRS_SOLVESTATUS_STOPPED 1 -#define XPRS_SOLVESTATUS_FAILED 2 -#define XPRS_SOLVESTATUS_COMPLETED 3 +#define XPRS_SOLVESTATUS_UNSTARTED 0 +#define XPRS_SOLVESTATUS_STOPPED 1 +#define XPRS_SOLVESTATUS_FAILED 2 +#define XPRS_SOLVESTATUS_COMPLETED 3 // *************************************************************************** // * values related to DEFAULTALG and ALGORITHM * // *************************************************************************** -#define XPRS_ALG_DEFAULT 1 -#define XPRS_ALG_DUAL 2 -#define XPRS_ALG_PRIMAL 3 -#define XPRS_ALG_BARRIER 4 -#define XPRS_ALG_NETWORK 5 +#define XPRS_ALG_DEFAULT 1 +#define XPRS_ALG_DUAL 2 +#define XPRS_ALG_PRIMAL 3 +#define XPRS_ALG_BARRIER 4 +#define XPRS_ALG_NETWORK 5 #define XPRS_PLUSINFINITY 1.0e+20 #define XPRS_MINUSINFINITY -1.0e+20 #define XPRS_MAXBANNERLENGTH 512 -#define XPVERSION 45 // >= 45 for XPRS_SOLAVAILABLE flags, XPRSgetduals(), etc. +#define XPVERSION 45 // >= 45 for XPRS_SOLAVAILABLE flags, XPRSgetduals(), etc. #define XPRS_MIPENTS 1032 #define XPRS_ALGORITHM 1049 #define XPRS_STOPSTATUS 1179 @@ -515,6 +515,7 @@ extern std::function XPRSgetlicerrmsg; extern std::function XPRSlicense; extern std::function XPRSgetbanner; extern std::function XPRSgetversion; +extern std::function XPRSgetversionnumbers; extern std::function XPRSsetprobname; extern std::function XPRSsetdefaultcontrol; extern std::function XPRSinterrupt; From dee7da435be140789b6217d164ff60982da89eef Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Fri, 10 Oct 2025 13:44:54 +0200 Subject: [PATCH 44/70] Fix iteration limit for hybrid gradient. --- ortools/math_opt/solvers/xpress_solver.cc | 10 ++++++++-- ortools/third_party_solvers/xpress_environment.h | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 54282b0c8a3..180fec3b9a6 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -301,8 +301,14 @@ class ScopedSolverContext { RETURN_IF_ERROR(Set(XPRS_TIMELIMIT, absl::ToDoubleSeconds(time_limit))); } if (parameters.has_iteration_limit()) { - RETURN_IF_ERROR(Set(XPRS_LPITERLIMIT, parameters.iteration_limit())); - RETURN_IF_ERROR(Set(XPRS_BARITERLIMIT, parameters.iteration_limit())); + if (parameters.lp_algorithm() == LP_ALGORITHM_FIRST_ORDER) { + // Iteration limit for PDHG is BARHGMAXRESTARTS + RETURN_IF_ERROR( + Set(XPRS_BARHGMAXRESTARTS, parameters.iteration_limit())); + } else { + RETURN_IF_ERROR(Set(XPRS_LPITERLIMIT, parameters.iteration_limit())); + RETURN_IF_ERROR(Set(XPRS_BARITERLIMIT, parameters.iteration_limit())); + } } if (parameters.has_node_limit()) { RETURN_IF_ERROR(Set(XPRS_MAXNODE, parameters.node_limit())); diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index cb815decfb5..7daa02a18cd 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -443,6 +443,7 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); #define XPRS_GLOBALSPATIALBRANCHIFPREFERORIG 8465 #define XPRS_PRECONFIGURATION 8470 #define XPRS_FEASIBILITYJUMP 8471 +#define XPRS_BARHGMAXRESTARTS 8484 #define XPRS_EXTRAELEMS 8006 #define XPRS_EXTRASETELEMS 8191 #define XPRS_LPOBJVAL 2001 From 7881ad6b9325f3c285c7d1bec916d257a3fd7165 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Fri, 10 Oct 2025 14:13:46 +0200 Subject: [PATCH 45/70] Support arbitrary Xpress parameters. --- ortools/math_opt/cpp/parameters.cc | 22 +++++++++++ ortools/math_opt/cpp/parameters.h | 22 +++++++++++ ortools/math_opt/parameters.proto | 3 ++ ortools/math_opt/solvers/xpress.proto | 35 +++++++++++++++++ ortools/math_opt/solvers/xpress_solver.cc | 39 ++++++++++++++++++- .../third_party_solvers/xpress_environment.cc | 1 + 6 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 ortools/math_opt/solvers/xpress.proto diff --git a/ortools/math_opt/cpp/parameters.cc b/ortools/math_opt/cpp/parameters.cc index d0826f5e0ce..e890a9de2a5 100644 --- a/ortools/math_opt/cpp/parameters.cc +++ b/ortools/math_opt/cpp/parameters.cc @@ -32,6 +32,7 @@ #include "ortools/math_opt/solvers/glpk.pb.h" #include "ortools/math_opt/solvers/gurobi.pb.h" #include "ortools/math_opt/solvers/highs.pb.h" +#include "ortools/math_opt/solvers/xpress.pb.h" #include "ortools/port/proto_utils.h" #include "ortools/util/status_macros.h" @@ -213,6 +214,25 @@ GlpkParameters GlpkParameters::FromProto(const GlpkParametersProto& proto) { return result; } +XpressParametersProto XpressParameters::Proto() const { + XpressParametersProto result; + for (const auto& [key, val] : param_values) { + XpressParametersProto::Parameter& p = *result.add_parameters(); + p.set_name(key); + p.set_value(val); + } + return result; +} + +XpressParameters XpressParameters::FromProto( + const XpressParametersProto& proto) { + XpressParameters result; + for (const XpressParametersProto::Parameter& p : proto.parameters()) { + result.param_values[p.name()] = p.value(); + } + return result; +} + SolveParametersProto SolveParameters::Proto() const { SolveParametersProto result; result.set_enable_output(enable_output); @@ -265,6 +285,7 @@ SolveParametersProto SolveParameters::Proto() const { *result.mutable_pdlp() = pdlp; *result.mutable_glpk() = glpk.Proto(); *result.mutable_highs() = highs; + *result.mutable_xpress() = xpress.Proto(); return result; } @@ -324,6 +345,7 @@ absl::StatusOr SolveParameters::FromProto( result.pdlp = proto.pdlp(); result.glpk = GlpkParameters::FromProto(proto.glpk()); result.highs = proto.highs(); + result.xpress = XpressParameters::FromProto(proto.xpress()); return result; } diff --git a/ortools/math_opt/cpp/parameters.h b/ortools/math_opt/cpp/parameters.h index 9cf7d7692b1..f4f432583f6 100644 --- a/ortools/math_opt/cpp/parameters.h +++ b/ortools/math_opt/cpp/parameters.h @@ -254,6 +254,27 @@ struct GlpkParameters { static GlpkParameters FromProto(const GlpkParametersProto& proto); }; +// Xpress specific parameters for solving. See +// https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/chapter7.html +// for a list of possible parameters (called "controls" in Xpress). +// +// Example use: +// XpressParameters xpress; +// xpress.param_values["BarIterLimit"] = "10"; +// +// Parameters are applied in the following order: +// * Any parameters derived from ortools parameters (like LP algorithm). +// * param_values in iteration order (insertion order). +struct XpressParameters { + // Parameter name-value pairs to set in insertion order. + gtl::linked_hash_map param_values; + + XpressParametersProto Proto() const; + static XpressParameters FromProto(const XpressParametersProto& proto); + + bool empty() const { return param_values.empty(); } +}; + // Parameters to control a single solve. // // Contains both parameters common to all solvers, e.g. time_limit, and @@ -426,6 +447,7 @@ struct SolveParameters { GlpkParameters glpk; HighsOptionsProto highs; + XpressParameters xpress; SolveParametersProto Proto() const; static absl::StatusOr FromProto( diff --git a/ortools/math_opt/parameters.proto b/ortools/math_opt/parameters.proto index 23ebe0623ee..a8fddb542cd 100644 --- a/ortools/math_opt/parameters.proto +++ b/ortools/math_opt/parameters.proto @@ -24,6 +24,7 @@ import "ortools/math_opt/solvers/gscip/gscip.proto"; import "ortools/math_opt/solvers/gurobi.proto"; import "ortools/math_opt/solvers/highs.proto"; import "ortools/math_opt/solvers/osqp.proto"; +import "ortools/math_opt/solvers/xpress.proto"; import "ortools/sat/sat_parameters.proto"; option java_package = "com.google.ortools.mathopt"; @@ -376,5 +377,7 @@ message SolveParametersProto { HighsOptionsProto highs = 27; + XpressParametersProto xpress = 28; + reserved 11; // Deleted } diff --git a/ortools/math_opt/solvers/xpress.proto b/ortools/math_opt/solvers/xpress.proto new file mode 100644 index 00000000000..ed23f2df1e5 --- /dev/null +++ b/ortools/math_opt/solvers/xpress.proto @@ -0,0 +1,35 @@ +// Copyright 2010-2025 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. + +// Proto messages specific to Xpress. +syntax = "proto3"; + +package operations_research.math_opt; +// Xpress specific parameters for solving. See +// https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/chapter7.html +// for a list of possible parameters (called "controls" in Xpress). +// +// Example use: +// XpressParameters xpress; +// xpress.param_values["BarIterLimit"] = "10"; +// +// Parameters are applied in the following order: +// * Any parameters derived from ortools parameters (like LP algorithm). +// * param_values in iteration order (insertion order). +message XpressParametersProto { + message Parameter { + string name = 1; + string value = 2; + } + repeated Parameter parameters = 1; +} \ No newline at end of file diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 180fec3b9a6..9fa703a7e89 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -423,7 +423,44 @@ class ScopedSolverContext { break; } } - /** TODO: Add XpressParameters to structure and apply settings. */ + + for (const XpressParametersProto::Parameter& parameter : + parameters.xpress().parameters()) { + std::string const& name = parameter.name(); + std::string const& value = parameter.value(); + int id, type; + int64_t l; + double d; + RETURN_IF_ERROR(xpress->GetControlInfo(name.c_str(), &id, &type)); + switch (type) { + case XPRS_TYPE_INT: // fallthrough + case XPRS_TYPE_INT64: + if (!absl::SimpleAtoi(value, &l)) + return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "value " << value << " for " << name + << " is not an integer"; + if (type == XPRS_TYPE_INT && (l > std::numeric_limits::max() || + l < std::numeric_limits::min())) + return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "value " << value << " for " << name + << " is out of range"; + RETURN_IF_ERROR(Set(id, l)); + break; + case XPRS_TYPE_DOUBLE: + if (!absl::SimpleAtod(value, &d)) + return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "value " << value << " for " << name + << " is not a floating pointer number"; + RETURN_IF_ERROR(Set(id, d)); + break; + case XPRS_TYPE_STRING: + RETURN_IF_ERROR(Set(id, value)); + break; + default: + return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "bad control type for " << name; + } + } if (!warnings.empty()) { return absl::InvalidArgumentError(absl::StrJoin(warnings, "; ")); diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index f57eb969816..6928078c5aa 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -149,6 +149,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSgetintattrib, "XPRSgetintattrib"); xpress_dynamic_library->GetFunction(&XPRSgetstringattrib, "XPRSgetstringattrib"); xpress_dynamic_library->GetFunction(&XPRSgetdblattrib, "XPRSgetdblattrib"); + xpress_dynamic_library->GetFunction(&XPRSgetcontrolinfo, "XPRSgetcontrolinfo"); xpress_dynamic_library->GetFunction(&XPRSgetobj, "XPRSgetobj"); xpress_dynamic_library->GetFunction(&XPRSgetrhs, "XPRSgetrhs"); xpress_dynamic_library->GetFunction(&XPRSgetrhsrange, "XPRSgetrhsrange"); From 2a7689252f4fb189cee57040e3a5a514c79d3be2 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Fri, 10 Oct 2025 16:52:50 +0200 Subject: [PATCH 46/70] Implement model specific parameters. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 15 ++ ortools/math_opt/solvers/xpress/g_xpress.h | 6 + ortools/math_opt/solvers/xpress_solver.cc | 219 +++++++++++++----- ortools/math_opt/solvers/xpress_solver.h | 5 +- .../third_party_solvers/xpress_environment.cc | 4 + .../third_party_solvers/xpress_environment.h | 2 + 6 files changed, 186 insertions(+), 65 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 50c698c3c7c..ddb6a734552 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -426,4 +426,19 @@ absl::Status Xpress::GetRedCosts(int* p_status, double* dj, int first, return ToStatus(XPRSgetredcosts(xpress_model_, p_status, dj, first, last)); } +absl::Status Xpress::AddMIPSol(int len, double const* vals, int const* colind, + char const* name) { + return ToStatus(XPRSaddmipsol(xpress_model_, len, vals, colind, name)); +} + +absl::Status Xpress::LoadDelayedRows(int len, int const* rows) { + return ToStatus(XPRSloaddelayedrows(xpress_model_, len, rows)); +} + +absl::Status Xpress::LoadDirs(int len, int const* cols, int const* prio, + char const* dir, double const* up, + double const* down) { + return ToStatus(XPRSloaddirs(xpress_model_, len, cols, prio, dir, up, down)); +} + } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index aeabb2ffd29..fa51cbcaa94 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -138,6 +138,12 @@ class Xpress { absl::Status GetSolution(int* p_status, double* x, int first, int last); absl::Status GetRedCosts(int* p_status, double* dj, int first, int last); + absl::Status AddMIPSol(int len, double const* vals, int const* colind, + char const* name = nullptr); + absl::Status LoadDelayedRows(int len, int const* rows); + absl::Status LoadDirs(int len, int const* cols, int const* prio, + char const* dir, double const* up, double const* down); + private: XPRSprob xpress_model_; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 9fa703a7e89..51786b5f18e 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -223,6 +223,44 @@ static void stdoutMessageCallback(std::vector const& lines) { for (auto& l : lines) std::cout << l << std::endl; } +inline BasisStatusProto XpressToMathOptBasisStatus(const int status, + bool isConstraint) { + // XPRESS row basis status is that of the slack variable + // For example, if the slack variable is at LB, the constraint is at UB + switch (status) { + case XPRS_BASIC: + return BASIS_STATUS_BASIC; + case XPRS_AT_LOWER: + return isConstraint ? BASIS_STATUS_AT_UPPER_BOUND + : BASIS_STATUS_AT_LOWER_BOUND; + case XPRS_AT_UPPER: + return isConstraint ? BASIS_STATUS_AT_LOWER_BOUND + : BASIS_STATUS_AT_UPPER_BOUND; + case XPRS_FREE_SUPER: + return BASIS_STATUS_FREE; + default: + return BASIS_STATUS_UNSPECIFIED; + } +} + +inline int MathOptToXpressBasisStatus(const BasisStatusProto status, + bool isConstraint) { + // XPRESS row basis status is that of the slack variable + // For example, if the slack variable is at LB, the constraint is at UB + switch (status) { + case BASIS_STATUS_BASIC: + return XPRS_BASIC; + case BASIS_STATUS_AT_LOWER_BOUND: + return isConstraint ? XPRS_AT_UPPER : XPRS_AT_LOWER; + case BASIS_STATUS_AT_UPPER_BOUND: + return isConstraint ? XPRS_AT_LOWER : XPRS_AT_UPPER; + case BASIS_STATUS_FREE: + return XPRS_FREE_SUPER; + default: + return XPRS_FREE_SUPER; + } +} + /** Temporary settings for a solve. * Instances of this class capture settings in the XPRSprob instance that are * made only temporarily for a solve. @@ -467,11 +505,122 @@ class ScopedSolverContext { } return absl::OkStatus(); } - /** TODO: Implement this (only for Solve(), not for - * ComputeInfeasibleSubsystem()) - * absl::Status ApplyParameters(const ModelSolveParametersProto& - * model_parameters); + absl::Status ApplyModelParameters( + ModelSolveParametersProto const& model_parameters, + gtl::linked_hash_map const& + variables_map, + gtl::linked_hash_map const& + linear_constraints_map) { + ASSIGN_OR_RETURN(int const cols, xpress->GetIntAttr(XPRS_COLS)); + ASSIGN_OR_RETURN(int const rows, xpress->GetIntAttr(XPRS_ROWS)); + // Set initial basis + if (model_parameters.has_initial_basis()) { + auto const& basis = model_parameters.initial_basis(); + std::vector xpress_var_basis_status(cols); + for (const auto [id, value] : MakeView(basis.variable_status())) { + xpress_var_basis_status[variables_map.at(id)] = + MathOptToXpressBasisStatus(static_cast(value), + false); + } + std::vector xpress_constr_basis_status(rows); + for (const auto [id, value] : MakeView(basis.constraint_status())) { + xpress_constr_basis_status[linear_constraints_map.at(id) + .constraint_index] = + MathOptToXpressBasisStatus(static_cast(value), + true); + } + RETURN_IF_ERROR(xpress->SetStartingBasis(xpress_constr_basis_status, + xpress_var_basis_status)); + } + std::vector colind; + + // Install solution hints. Xpress does not explicitly have solutions + // hints but it supports partial MIP starts. So we just add each solution + // hint as MIP start. + if (model_parameters.solution_hints_size() > 0) { + unsigned int cnt = 0; + std::vector mipStart; + colind.reserve(cols); + mipStart.reserve(cols); + for (auto const& hint : model_parameters.solution_hints()) { + colind.clear(); + mipStart.clear(); + for (const auto [id, value] : MakeView(hint.variable_values())) { + colind.push_back(variables_map.at(id)); + mipStart.push_back(value); + } + if (mipStart.size() > cols) + return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "more solution hints than columns"; + RETURN_IF_ERROR(xpress->AddMIPSol( + static_cast(mipStart.size()), mipStart.data(), colind.data(), + absl::StrCat("SolutionHint", cnt).c_str())); + ++cnt; + } + } + + // Install branching priorities. + // Note that in ortools higher priority takes precedence while in Xpress + // lower priority takes precedence. + if (model_parameters.has_branching_priorities()) { + auto const& prios = model_parameters.branching_priorities(); + colind.clear(); + colind.reserve(prios.ids_size()); + std::vector priority; + priority.reserve(prios.ids_size()); + for (const auto [id, prio] : MakeView(prios)) { + colind.push_back(variables_map.at(id)); + /** TODO: Xpress prios must be in [0,1000]. */ + priority.push_back( + -prio); // Smaller ids have higher precedence in Xpress! + } + + if (colind.size() > 0) + return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "more branching priorities than columns"; + RETURN_IF_ERROR(xpress->LoadDirs(static_cast(colind.size()), + colind.data(), priority.data(), nullptr, + nullptr, nullptr)); + } + + /** TODO: Install (multi-)objective parameters. */ + // ObjectiveMap objective_parameters; + + if (model_parameters.lazy_linear_constraint_ids_size() > 0) { + std::vector delayedRows; + delayedRows.reserve(rows); + for (auto const& idx : model_parameters.lazy_linear_constraint_ids()) { + delayedRows.push_back(linear_constraints_map.at(idx).constraint_index); + } + if (delayedRows.size() > rows) + return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "more lazy constraints than rows"; + + RETURN_IF_ERROR(xpress->LoadDelayedRows( + static_cast(delayedRows.size()), delayedRows.data())); + } + + return absl::OkStatus(); + } + /** Interrupt the current solve with the given reason. */ + void Interrupt(int reason) { CHECK_OK(xpress->Interrupt(reason)); } + /** Reraise any pending exception from a callback. */ + void ReraiseCallbackException() { + if (callbackException) { + std::exception_ptr old = callbackException; + callbackException = nullptr; + std::rethrow_exception(old); + } + } + /** Set exception raised in callback. + * Will not overwrite an existing pending exception. */ + void SetCallbackException(std::exception_ptr ex) { + const absl::MutexLock lock(&mutex); + if (!callbackException) callbackException = ex; + } ~ScopedSolverContext() { ~SolveContext() { @@ -747,17 +896,14 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(inverted_bounds.ToStatus()); } - // Set initial basis - if (model_parameters.has_initial_basis()) { - RETURN_IF_ERROR(SetXpressStartingBasis(model_parameters.initial_basis())); - } - // Register callbacks and create scoped context to automatically if an // exception has been thrown during optimization. { ScopedSolverContext solveContext(xpress_.get()); RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback)); + RETURN_IF_ERROR(solveContext.ApplyModelParameters( + model_parameters, variables_map_, linear_constraints_map_)); // Solve. We use the generic XPRSoptimize() and let Xpress decide what is // the best algorithm. Note that we do not pass flags to the function either. // We assume that algorithms are configured via controls like LPFLAGS. @@ -976,61 +1122,6 @@ SolutionStatusProto XpressSolver::getDualSolutionStatus() const { return SOLUTION_STATUS_UNDETERMINED; } -inline BasisStatusProto XpressToMathOptBasisStatus(const int status, - bool isConstraint) { - // XPRESS row basis status is that of the slack variable - // For example, if the slack variable is at LB, the constraint is at UB - switch (status) { - case XPRS_BASIC: - return BASIS_STATUS_BASIC; - case XPRS_AT_LOWER: - return isConstraint ? BASIS_STATUS_AT_UPPER_BOUND - : BASIS_STATUS_AT_LOWER_BOUND; - case XPRS_AT_UPPER: - return isConstraint ? BASIS_STATUS_AT_LOWER_BOUND - : BASIS_STATUS_AT_UPPER_BOUND; - case XPRS_FREE_SUPER: - return BASIS_STATUS_FREE; - default: - return BASIS_STATUS_UNSPECIFIED; - } -} - -inline int MathOptToXpressBasisStatus(const BasisStatusProto status, - bool isConstraint) { - // XPRESS row basis status is that of the slack variable - // For example, if the slack variable is at LB, the constraint is at UB - switch (status) { - case BASIS_STATUS_BASIC: - return XPRS_BASIC; - case BASIS_STATUS_AT_LOWER_BOUND: - return isConstraint ? XPRS_AT_UPPER : XPRS_AT_LOWER; - case BASIS_STATUS_AT_UPPER_BOUND: - return isConstraint ? XPRS_AT_LOWER : XPRS_AT_UPPER; - case BASIS_STATUS_FREE: - return XPRS_FREE_SUPER; - default: - return XPRS_FREE_SUPER; - } -} - -absl::Status XpressSolver::SetXpressStartingBasis(const BasisProto& basis) { - std::vector xpress_var_basis_status(xpress_->GetNumberOfVariables()); - for (const auto [id, value] : MakeView(basis.variable_status())) { - xpress_var_basis_status[variables_map_.at(id)] = - MathOptToXpressBasisStatus(static_cast(value), false); - } - std::vector xpress_constr_basis_status( - xpress_->GetNumberOfConstraints()); - for (const auto [id, value] : MakeView(basis.constraint_status())) { - xpress_constr_basis_status[linear_constraints_map_.at(id) - .constraint_index] = - MathOptToXpressBasisStatus(static_cast(value), true); - } - return xpress_->SetStartingBasis(xpress_constr_basis_status, - xpress_var_basis_status); -} - absl::StatusOr> XpressSolver::GetBasisIfAvailable( const SolveParametersProto&) { std::vector xprs_variable_basis_status; diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index fd90f42a599..4f252a1d08a 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -70,6 +70,7 @@ class XpressSolver : public SolverInterface { private: explicit XpressSolver(std::unique_ptr g_xpress); + public: // For easing reading the code, we declare these types: using VarId = int64_t; using AuxiliaryObjectiveId = int64_t; @@ -88,6 +89,7 @@ class XpressSolver : public SolverInterface { using XpressGeneralConstraintIndex = int; using XpressAnyConstraintIndex = int; + private: static constexpr XpressVariableIndex kUnspecifiedIndex = -1; static constexpr XpressAnyConstraintIndex kUnspecifiedConstraint = -2; static constexpr double kPlusInf = XPRS_PLUSINFINITY; @@ -98,12 +100,14 @@ class XpressSolver : public SolverInterface { } // Data associated with each linear constraint + public: struct LinearConstraintData { XpressLinearConstraintIndex constraint_index = kUnspecifiedConstraint; double lower_bound = kMinusInf; double upper_bound = kPlusInf; }; + private: absl::StatusOr ExtractSolveResultProto( absl::Time start, const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters); @@ -159,7 +163,6 @@ class XpressSolver : public SolverInterface { SolutionStatusProto getPrimalSolutionStatus() const; SolutionStatusProto getDualSolutionStatus() const; absl::StatusOr ListInvertedBounds() const; - absl::Status SetXpressStartingBasis(const BasisProto& basis); bool is_mip_ = false; // Results of the last solve diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index 6928078c5aa..1ce8e8a49fe 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -95,6 +95,8 @@ std::function XPRSgetro std::function XPRSgetcoltype = nullptr; std::function XPRSchgbounds = nullptr; std::function XPRSaddmipsol = nullptr; +std::function XPRSloaddelayedrows = nullptr; +std::function XPRSloaddirs; std::function XPRSgetlpsol = nullptr; std::function XPRSgetmipsol = nullptr; std::function XPRSchgobj = nullptr; @@ -176,6 +178,8 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSgetcoltype, "XPRSgetcoltype"); xpress_dynamic_library->GetFunction(&XPRSchgbounds, "XPRSchgbounds"); xpress_dynamic_library->GetFunction(&XPRSaddmipsol, "XPRSaddmipsol"); + xpress_dynamic_library->GetFunction(&XPRSloaddelayedrows, "XPRSloaddelayedrows"); + xpress_dynamic_library->GetFunction(&XPRSloaddirs, "XPRSloaddirs"); xpress_dynamic_library->GetFunction(&XPRSgetlpsol, "XPRSgetlpsol"); xpress_dynamic_library->GetFunction(&XPRSgetmipsol, "XPRSgetmipsol"); xpress_dynamic_library->GetFunction(&XPRSchgobj, "XPRSchgobj"); diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index 7daa02a18cd..da89f51994b 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -558,6 +558,8 @@ OR_DLL extern std::function XPRSgetcoltype; extern std::function XPRSchgbounds; extern std::function XPRSaddmipsol; +extern std::function XPRSloaddelayedrows; +extern std::function XPRSloaddirs; extern std::function XPRSgetlpsol; extern std::function XPRSgetmipsol; extern std::function XPRSchgobj; From e90135b2be0f729bb477f5f3ebf53b84e273bac9 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Mon, 13 Oct 2025 10:27:22 +0200 Subject: [PATCH 47/70] Fixes after rebase. --- ortools/math_opt/solvers/xpress_solver.cc | 149 +++++++++------------- 1 file changed, 62 insertions(+), 87 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 51786b5f18e..79533c5ce5f 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -113,6 +113,7 @@ class ScopedCallback { ctx = context; RETURN_IF_ERROR( ProtoT::Add(ctx->xpress, low_level_cb, reinterpret_cast(this))); + or_tools_cb = cb; return absl::OkStatus(); } @@ -291,6 +292,28 @@ class ScopedSolverContext { ScopedSolverContext(Xpress* xpress) : removeInterrupterCallback(nullptr) { shared_ctx.xpress = xpress; } + absl::Status Set(int id, int32_t const& value) { + return Set(id, int64_t(value)); + } + absl::Status Set(int id, int64_t const& value) { + ASSIGN_OR_RETURN(int64_t old, shared_ctx.xpress->GetIntControl64(id)); + modifiedControls.push_back({OneControl::INT_CONTROL, id, old, 0.0, ""}); + RETURN_IF_ERROR(shared_ctx.xpress->SetIntControl64(id, value)); + return absl::OkStatus(); + } + absl::Status Set(int id, double const& value) { + ASSIGN_OR_RETURN(double old, shared_ctx.xpress->GetDblControl(id)); + modifiedControls.push_back({OneControl::DBL_CONTROL, id, 0LL, old, ""}); + RETURN_IF_ERROR(shared_ctx.xpress->SetDblControl(id, value)); + return absl::OkStatus(); + } + absl::Status Set(int id, std::string const& value) { + ASSIGN_OR_RETURN(std::string old, shared_ctx.xpress->GetStrControl(id)); + modifiedControls.push_back({OneControl::STR_CONTROL, id, 0LL, 0.0, old}); + RETURN_IF_ERROR(shared_ctx.xpress->SetStrControl(id, value)); + return absl::OkStatus(); + } + absl::Status AddCallbacks(MessageCallback message_callback, const SolveInterrupter* interrupter) { if (message_callback) @@ -323,11 +346,12 @@ class ScopedSolverContext { absl::Status ApplyParameters(const SolveParametersProto& parameters, MessageCallback message_callback) { std::vector warnings; - ASSIGN_OR_RETURN(bool const isMIP, xpress->IsMIP()); + ASSIGN_OR_RETURN(bool const isMIP, shared_ctx.xpress->IsMIP()); if (parameters.enable_output()) { // This is considered only if no message callback is set. if (!message_callback) { - RETURN_IF_ERROR(messageCallback.Add(this, stdoutMessageCallback)); + RETURN_IF_ERROR( + messageCallback.Add(&shared_ctx, stdoutMessageCallback)); } } absl::Duration time_limit = absl::InfiniteDuration(); @@ -469,7 +493,8 @@ class ScopedSolverContext { int id, type; int64_t l; double d; - RETURN_IF_ERROR(xpress->GetControlInfo(name.c_str(), &id, &type)); + RETURN_IF_ERROR( + shared_ctx.xpress->GetControlInfo(name.c_str(), &id, &type)); switch (type) { case XPRS_TYPE_INT: // fallthrough case XPRS_TYPE_INT64: @@ -513,8 +538,8 @@ class ScopedSolverContext { gtl::linked_hash_map const& linear_constraints_map) { - ASSIGN_OR_RETURN(int const cols, xpress->GetIntAttr(XPRS_COLS)); - ASSIGN_OR_RETURN(int const rows, xpress->GetIntAttr(XPRS_ROWS)); + ASSIGN_OR_RETURN(int const cols, shared_ctx.xpress->GetIntAttr(XPRS_COLS)); + ASSIGN_OR_RETURN(int const rows, shared_ctx.xpress->GetIntAttr(XPRS_ROWS)); // Set initial basis if (model_parameters.has_initial_basis()) { auto const& basis = model_parameters.initial_basis(); @@ -531,8 +556,8 @@ class ScopedSolverContext { MathOptToXpressBasisStatus(static_cast(value), true); } - RETURN_IF_ERROR(xpress->SetStartingBasis(xpress_constr_basis_status, - xpress_var_basis_status)); + RETURN_IF_ERROR(shared_ctx.xpress->SetStartingBasis( + xpress_constr_basis_status, xpress_var_basis_status)); } std::vector colind; @@ -554,7 +579,7 @@ class ScopedSolverContext { if (mipStart.size() > cols) return util::StatusBuilder(absl::StatusCode::kInvalidArgument) << "more solution hints than columns"; - RETURN_IF_ERROR(xpress->AddMIPSol( + RETURN_IF_ERROR(shared_ctx.xpress->AddMIPSol( static_cast(mipStart.size()), mipStart.data(), colind.data(), absl::StrCat("SolutionHint", cnt).c_str())); ++cnt; @@ -580,9 +605,9 @@ class ScopedSolverContext { if (colind.size() > 0) return util::StatusBuilder(absl::StatusCode::kInvalidArgument) << "more branching priorities than columns"; - RETURN_IF_ERROR(xpress->LoadDirs(static_cast(colind.size()), - colind.data(), priority.data(), nullptr, - nullptr, nullptr)); + RETURN_IF_ERROR(shared_ctx.xpress->LoadDirs( + static_cast(colind.size()), colind.data(), priority.data(), + nullptr, nullptr, nullptr)); } /** TODO: Install (multi-)objective parameters. */ @@ -598,43 +623,35 @@ class ScopedSolverContext { return util::StatusBuilder(absl::StatusCode::kInvalidArgument) << "more lazy constraints than rows"; - RETURN_IF_ERROR(xpress->LoadDelayedRows( + RETURN_IF_ERROR(shared_ctx.xpress->LoadDelayedRows( static_cast(delayedRows.size()), delayedRows.data())); } return absl::OkStatus(); } /** Interrupt the current solve with the given reason. */ - void Interrupt(int reason) { CHECK_OK(xpress->Interrupt(reason)); } - /** Reraise any pending exception from a callback. */ - void ReraiseCallbackException() { - if (callbackException) { - std::exception_ptr old = callbackException; - callbackException = nullptr; - std::rethrow_exception(old); + void Interrupt(int reason) { CHECK_OK(shared_ctx.xpress->Interrupt(reason)); } + + void ReraiseException() { + if (shared_ctx.callbackException) { + std::exception_ptr ex = shared_ctx.callbackException; + shared_ctx.callbackException = nullptr; + std::rethrow_exception(ex); } } - /** Set exception raised in callback. - * Will not overwrite an existing pending exception. - */ - void SetCallbackException(std::exception_ptr ex) { - const absl::MutexLock lock(&mutex); - if (!callbackException) callbackException = ex; - } ~ScopedSolverContext() { - ~SolveContext() { for (auto it = modifiedControls.rbegin(); it != modifiedControls.rend(); ++it) { switch (it->type) { case OneControl::INT_CONTROL: - CHECK_OK(xpress->SetIntControl64(it->id, it->l)); + CHECK_OK(shared_ctx.xpress->SetIntControl64(it->id, it->l)); break; case OneControl::DBL_CONTROL: - CHECK_OK(xpress->SetDblControl(it->id, it->d)); + CHECK_OK(shared_ctx.xpress->SetDblControl(it->id, it->d)); break; case OneControl::STR_CONTROL: - CHECK_OK(xpress->SetStrControl(it->id, it->s.c_str())); + CHECK_OK(shared_ctx.xpress->SetStrControl(it->id, it->s.c_str())); break; } } @@ -643,51 +660,8 @@ class ScopedSolverContext { if (shared_ctx.callbackException) std::rethrow_exception(shared_ctx.callbackException); } - - absl::Status Set(int id, int32_t const& value) { - return Set(id, int64_t(value)); - } - absl::Status Set(int id, int64_t const& value) { - ASSIGN_OR_RETURN(int64_t old, xpress->GetIntControl64(id)); - modifiedControls.push_back({OneControl::INT_CONTROL, id, old, 0.0, ""}); - RETURN_IF_ERROR(xpress->SetIntControl64(id, value)); - return absl::OkStatus(); - } - absl::Status Set(int id, double const& value) { - ASSIGN_OR_RETURN(double old, xpress->GetDblControl(id)); - modifiedControls.push_back({OneControl::DBL_CONTROL, id, 0LL, old, ""}); - RETURN_IF_ERROR(xpress->SetDblControl(id, value)); - return absl::OkStatus(); - } - absl::Status Set(int id, std::string const& value) { - ASSIGN_OR_RETURN(std::string old, xpress->GetStrControl(id)); - modifiedControls.push_back({OneControl::STR_CONTROL, id, 0LL, 0.0, old}); - RETURN_IF_ERROR(xpress->SetStrControl(id, value)); - return absl::OkStatus(); - } }; -template -absl::Status ScopedCallback::Add(SolveContext* ctx, - typename T::DataType data) { - RETURN_IF_ERROR(T::Add(ctx->xpress, reinterpret_cast(this))); - callbackData = data; - context = ctx; - return absl::OkStatus(); -} -template -void ScopedCallback::Interrupt(int reason) { - context->Interrupt(reason); -} -template -void ScopedCallback::SetCallbackException(std::exception_ptr ex) { - context->SetCallbackException(ex); -} - -template -ScopedCallback::~ScopedCallback() { - if (context) T::Remove(context->xpress, reinterpret_cast(this)); -} } // namespace constexpr SupportedProblemStructures kXpressSupportedStructures = { @@ -898,18 +872,22 @@ absl::StatusOr XpressSolver::Solve( // Register callbacks and create scoped context to automatically if an // exception has been thrown during optimization. - { - ScopedSolverContext solveContext(xpress_.get()); - RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); - RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback)); - RETURN_IF_ERROR(solveContext.ApplyModelParameters( + ScopedSolverContext solveContext(xpress_.get()); + RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); + RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback)); + RETURN_IF_ERROR(solveContext.ApplyModelParameters( model_parameters, variables_map_, linear_constraints_map_)); - // Solve. We use the generic XPRSoptimize() and let Xpress decide what is - // the best algorithm. Note that we do not pass flags to the function either. - // We assume that algorithms are configured via controls like LPFLAGS. - - RETURN_IF_ERROR(xpress_->Optimize("", &solvestatus_, &solstatus_)); - } + // Solve. We use the generic XPRSoptimize() and let Xpress decide what is + // the best algorithm. Note that we do not pass flags to the function + // either. We assume that algorithms are configured via controls like + // LPFLAGS. + RETURN_IF_ERROR(xpress_->Optimize("", &solvestatus_, &solstatus_)); + // Reraise any exception now. Note that we cannot just limit the scope of + // solveContext since its destructor will restore controls settings. + // On the other hand, when fetching results we need to check some controls + // (for example, BARALG to decide whether we need to report barrier or + // first order iterations). + solveContext.ReraiseException(); RETURN_IF_ERROR(xpress_->GetSolution(&primal_sol_avail_, nullptr, 0, -1)); RETURN_IF_ERROR(xpress_->GetDuals(&dual_sol_avail_, nullptr, 0, -1)); ASSIGN_OR_RETURN(algorithm_, xpress_->GetIntAttr(XPRS_ALGORITHM)); @@ -1393,9 +1371,6 @@ XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, return absl::UnimplementedError( "XPRESS does not provide a method to compute an infeasible subsystem"); - /** TODO: - solveContext.ReraiseCallbackException(); - */ } absl::StatusOr XpressSolver::ListInvertedBounds() const { From 3a9b1ad1e09a789675af1d137feacfb5efca2992 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Mon, 13 Oct 2025 14:03:54 +0200 Subject: [PATCH 48/70] Add support for multi-objective. --- .../solver_tests/multi_objective_tests.cc | 20 ++ ortools/math_opt/solvers/xpress/g_xpress.cc | 50 +++++ ortools/math_opt/solvers/xpress/g_xpress.h | 10 + ortools/math_opt/solvers/xpress_solver.cc | 197 ++++++++++++++++-- ortools/math_opt/solvers/xpress_solver.h | 10 +- .../math_opt/solvers/xpress_solver_test.cc | 2 +- .../third_party_solvers/xpress_environment.cc | 11 + .../third_party_solvers/xpress_environment.h | 16 ++ 8 files changed, 291 insertions(+), 25 deletions(-) diff --git a/ortools/math_opt/solver_tests/multi_objective_tests.cc b/ortools/math_opt/solver_tests/multi_objective_tests.cc index 6f9b28d4047..098d3717b03 100644 --- a/ortools/math_opt/solver_tests/multi_objective_tests.cc +++ b/ortools/math_opt/solver_tests/multi_objective_tests.cc @@ -371,6 +371,10 @@ TEST_P(SimpleMultiObjectiveTest, if (!GetParam().supports_integer_variables) { GTEST_SKIP() << kNoIntegerVariableSupportMessage; } + if (GetParam().solver_type == SolverType::kXpress) { + GTEST_SKIP() << "Ignoring this test because Xpress does not support per " + "objective time limits at the moment"; + } ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, Load23588MiplibInstance()); const Objective aux_obj = model->AddMaximizationObjective( @@ -402,6 +406,10 @@ TEST_P(SimpleMultiObjectiveTest, if (!GetParam().supports_integer_variables) { GTEST_SKIP() << kNoIntegerVariableSupportMessage; } + if (GetParam().solver_type == SolverType::kXpress) { + GTEST_SKIP() << "Ignoring this test because Xpress does not support per " + "objective time limits at the moment"; + } ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, Load23588MiplibInstance()); model->AddMaximizationObjective(0, /*priority=*/1); @@ -463,6 +471,10 @@ TEST_P(SimpleMultiObjectiveTest, if (!GetParam().supports_integer_variables) { GTEST_SKIP() << kNoIntegerVariableSupportMessage; } + if (GetParam().solver_type == SolverType::kXpress) { + GTEST_SKIP() << "Ignoring this test because Xpress does not support per " + "objective time limits at the moment"; + } ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, Load23588MiplibInstance()); SolveArguments args = { @@ -583,6 +595,10 @@ TEST_P(IncrementalMultiObjectiveTest, AddObjectiveToMultiObjectiveModel) { if (!GetParam().supports_auxiliary_objectives) { GTEST_SKIP() << kNoMultiObjectiveSupportMessage; } + if (!GetParam().supports_incremental_objective_add_and_delete) { + GTEST_SKIP() + << "Ignoring this test as it requires support for incremental solve"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -636,6 +652,10 @@ TEST_P(IncrementalMultiObjectiveTest, DeleteObjectiveFromMultiObjectiveModel) { if (!GetParam().supports_auxiliary_objectives) { GTEST_SKIP() << kNoMultiObjectiveSupportMessage; } + if (!GetParam().supports_incremental_objective_add_and_delete) { + GTEST_SKIP() + << "Ignoring this test as it requires support for incremental solve"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index ddb6a734552..30fc513d426 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -345,6 +345,15 @@ absl::StatusOr Xpress::GetDoubleAttr(int attribute) const { return result; } +absl::StatusOr Xpress::GetObjectiveDoubleAttr(int objidx, + int attribute) const { + double result; + RETURN_IF_ERROR( + ToStatus(XPRSgetobjdblattrib(xpress_model_, objidx, attribute, &result))) + << "Error getting Xpress objective double attribute: " << attribute; + return result; +} + int Xpress::GetNumberOfConstraints() const { int n; XPRSgetintattrib(xpress_model_, XPRS_ROWS, &n); @@ -441,4 +450,45 @@ absl::Status Xpress::LoadDirs(int len, int const* cols, int const* prio, return ToStatus(XPRSloaddirs(xpress_model_, len, cols, prio, dir, up, down)); } +absl::Status Xpress::SetObjectiveIntControl(int obj, int control, int value) { + return ToStatus(XPRSsetobjintcontrol(xpress_model_, obj, control, value)); +} +absl::Status Xpress::SetObjectiveDoubleControl(int obj, int control, + double value) { + return ToStatus(XPRSsetobjdblcontrol(xpress_model_, obj, control, value)); +} +absl::StatusOr Xpress::AddObjective(double constant, int ncols, + int const* colind, + double const* objcoef, int priority, + double weight) { + ASSIGN_OR_RETURN(int const objs, GetIntAttr(XPRS_OBJECTIVES)); + if (objs == INT_MAX) { + return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "too many objectives"; + } + int ret = XPRSaddobj(xpress_model_, ncols, colind, objcoef, priority, weight); + if (ret) { + return ToStatus(ret); + } + if (constant != 0.0) { + ret = + XPRSsetobjdblcontrol(xpress_model_, objs, XPRS_OBJECTIVE_RHS, constant); + if (ret) { + XPRSdelobj(xpress_model_, objs); + return ToStatus(ret); + } + } + return objs; +} + +absl::StatusOr Xpress::CalculateObjectiveN(int objidx, + double const* solution) { + double objval; + int ret = XPRScalcobjn(xpress_model_, objidx, solution, &objval); + if (ret) { + return ToStatus(ret); + } + return objval; +} + } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index fa51cbcaa94..b7a97d71f58 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -67,6 +67,8 @@ class Xpress { absl::StatusOr GetIntAttr(int attribute) const; absl::StatusOr GetDoubleAttr(int attribute) const; + absl::StatusOr GetObjectiveDoubleAttr(int objidx, + int attribute) const; absl::Status AddVars(std::size_t count, absl::Span obj, absl::Span lb, absl::Span ub, @@ -144,6 +146,14 @@ class Xpress { absl::Status LoadDirs(int len, int const* cols, int const* prio, char const* dir, double const* up, double const* down); + absl::Status SetObjectiveIntControl(int obj, int control, int value); + absl::Status SetObjectiveDoubleControl(int obj, int control, double value); + absl::StatusOr AddObjective(double constant, int ncols, + int const* colind, double const* objcoef, + int priority, double weight); + absl::StatusOr CalculateObjectiveN(int objidx, + double const* solution); + private: XPRSprob xpress_model_; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 79533c5ce5f..5cd68779750 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -499,19 +499,19 @@ class ScopedSolverContext { case XPRS_TYPE_INT: // fallthrough case XPRS_TYPE_INT64: if (!absl::SimpleAtoi(value, &l)) - return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + return util::InvalidArgumentErrorBuilder() << "value " << value << " for " << name << " is not an integer"; if (type == XPRS_TYPE_INT && (l > std::numeric_limits::max() || l < std::numeric_limits::min())) - return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + return util::InvalidArgumentErrorBuilder() << "value " << value << " for " << name << " is out of range"; RETURN_IF_ERROR(Set(id, l)); break; case XPRS_TYPE_DOUBLE: if (!absl::SimpleAtod(value, &d)) - return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + return util::InvalidArgumentErrorBuilder() << "value " << value << " for " << name << " is not a floating pointer number"; RETURN_IF_ERROR(Set(id, d)); @@ -520,7 +520,7 @@ class ScopedSolverContext { RETURN_IF_ERROR(Set(id, value)); break; default: - return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + return util::InvalidArgumentErrorBuilder() << "bad control type for " << name; } } @@ -537,7 +537,10 @@ class ScopedSolverContext { variables_map, gtl::linked_hash_map const& - linear_constraints_map) { + linear_constraints_map, + gtl::linked_hash_map const& + objectives_map) { ASSIGN_OR_RETURN(int const cols, shared_ctx.xpress->GetIntAttr(XPRS_COLS)); ASSIGN_OR_RETURN(int const rows, shared_ctx.xpress->GetIntAttr(XPRS_ROWS)); // Set initial basis @@ -577,7 +580,7 @@ class ScopedSolverContext { mipStart.push_back(value); } if (mipStart.size() > cols) - return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + return util::InvalidArgumentErrorBuilder() << "more solution hints than columns"; RETURN_IF_ERROR(shared_ctx.xpress->AddMIPSol( static_cast(mipStart.size()), mipStart.data(), colind.data(), @@ -603,15 +606,60 @@ class ScopedSolverContext { } if (colind.size() > 0) - return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + return util::InvalidArgumentErrorBuilder() << "more branching priorities than columns"; RETURN_IF_ERROR(shared_ctx.xpress->LoadDirs( static_cast(colind.size()), colind.data(), priority.data(), nullptr, nullptr, nullptr)); } - /** TODO: Install (multi-)objective parameters. */ - // ObjectiveMap objective_parameters; + // Objective parameters: primary/single objective + if (model_parameters.has_primary_objective_parameters()) { + auto const& p = model_parameters.primary_objective_parameters(); + // Objective violation tolerances only need to be installed for + // multi-objective models. We just set them blindly here. They don't + // hurt for a single-objective model. + if (p.has_objective_degradation_absolute_tolerance()) { + RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl( + 0, XPRS_OBJECTIVE_ABSTOL, + p.objective_degradation_absolute_tolerance())); + } + if (p.has_objective_degradation_relative_tolerance()) { + RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl( + 0, XPRS_OBJECTIVE_RELTOL, + p.objective_degradation_relative_tolerance())); + } + if (p.has_time_limit()) { + // We support a time limit but only if there is one single objective. + if (objectives_map.size() > 0) { + return util::InvalidArgumentErrorBuilder() + << "Xpress does not support per-objective time limits"; + } + ASSIGN_OR_RETURN(auto l, + util_time::DecodeGoogleApiProto(p.time_limit())); + + RETURN_IF_ERROR(shared_ctx.xpress->SetDblControl( + XPRS_TIMELIMIT, absl::ToDoubleSeconds(l))); + } + } + // Objective parameters: auxiliary objectives + for (auto const& [id, p] : + model_parameters.auxiliary_objective_parameters()) { + if (p.has_objective_degradation_absolute_tolerance()) { + RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl( + objectives_map.at(id), XPRS_OBJECTIVE_ABSTOL, + p.objective_degradation_absolute_tolerance())); + } + if (p.has_objective_degradation_relative_tolerance()) { + RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl( + objectives_map.at(id), XPRS_OBJECTIVE_RELTOL, + p.objective_degradation_relative_tolerance())); + } + if (p.has_time_limit()) { + return util::InvalidArgumentErrorBuilder() + << "Xpress does not support per-objective time limits"; + } + } if (model_parameters.lazy_linear_constraint_ids_size() > 0) { std::vector delayedRows; @@ -620,7 +668,7 @@ class ScopedSolverContext { delayedRows.push_back(linear_constraints_map.at(idx).constraint_index); } if (delayedRows.size() > rows) - return util::StatusBuilder(absl::StatusCode::kInvalidArgument) + return util::InvalidArgumentErrorBuilder() << "more lazy constraints than rows"; RETURN_IF_ERROR(shared_ctx.xpress->LoadDelayedRows( @@ -666,7 +714,7 @@ class ScopedSolverContext { constexpr SupportedProblemStructures kXpressSupportedStructures = { .integer_variables = SupportType::kSupported, - .multi_objectives = SupportType::kNotSupported, + .multi_objectives = SupportType::kSupported, .quadratic_objectives = SupportType::kSupported, .quadratic_constraints = SupportType::kNotSupported, .second_order_cone_constraints = SupportType::kNotSupported, @@ -697,7 +745,19 @@ absl::Status XpressSolver::LoadModel(const ModelProto& input_model) { RETURN_IF_ERROR(AddNewVariables(input_model.variables())); RETURN_IF_ERROR(AddNewLinearConstraints(input_model.linear_constraints())); RETURN_IF_ERROR(ChangeCoefficients(input_model.linear_constraint_matrix())); - RETURN_IF_ERROR(AddSingleObjective(input_model.objective())); + RETURN_IF_ERROR(AddObjective(input_model.objective(), std::nullopt, + !input_model.auxiliary_objectives().empty())); + // Tests expect an error on duplicate priorities, so raise one. + absl::flat_hash_set prios = { + input_model.objective().priority()}; + for (auto const& [id, obj] : input_model.auxiliary_objectives()) { + auto const prio = obj.priority(); + if (!prios.insert(prio).second) { + return util::InvalidArgumentErrorBuilder() + << "repeated objective priority: " << prio; + } + RETURN_IF_ERROR(AddObjective(obj, id, true)); + } return absl::OkStatus(); } absl::Status XpressSolver::AddNewVariables( @@ -794,20 +854,56 @@ absl::Status XpressSolver::AddNewLinearConstraints( return xpress_->AddConstrs(constraint_sense, constraint_rhs, constraint_rng); } -absl::Status XpressSolver::AddSingleObjective(const ObjectiveProto& objective) { - // Sense - RETURN_IF_ERROR(xpress_->SetObjectiveSense(objective.maximize())); - // Linear terms - std::vector index; - index.reserve(objective.linear_coefficients().ids_size()); - for (const int64_t id : objective.linear_coefficients().ids()) { - index.push_back(variables_map_.at(id)); +absl::Status XpressSolver::AddObjective( + const ObjectiveProto& objective, + std::optional objective_id, bool multiobj) { + double weight = 1.0; + bool haveId = objective_id.has_value(); + + if (multiobj) { + // In ortools smaller priority means more important, in Xpress, + // higher priority means more important, so we must invert priorities. + // Moreover, in Xpress priorities are 32bit. + // In ortools it seems unspecified what happens to objectives with the + // same priority, in Xpress these are merged. + if (objective.priority() <= INT_MIN || objective.priority() > INT_MAX) { + return util::InvalidArgumentErrorBuilder() + << "Xpress only supports 32bit signed integers as objective " + "priority, not " + << objective.priority(); + } + } + + // Set/adjust objective sense. + if (!multiobj) { + // Not a multi-objective model + RETURN_IF_ERROR(xpress_->SetObjectiveSense(objective.maximize())); + } else if (!objective_id.has_value()) { + // First objective in multi-objective. + RETURN_IF_ERROR(xpress_->SetObjectiveSense(objective.maximize())); + is_multiobj_ = true; + } else { + // Auxiliary objective in multi-objective. Xpress does not support + // different objective senses for different objectives. So if the sense + // does not match we set the weight to -1.0 to inver the objective + // coefficients. + ASSIGN_OR_RETURN(double const objsen, + xpress_->GetDoubleAttr(XPRS_OBJSENSE)); + if (objective.maximize() != (objsen < 0.0)) { + weight = -1.0; + } } - RETURN_IF_ERROR(xpress_->SetLinearObjective( - objective.offset(), index, objective.linear_coefficients().values())); + + // Extract the objective. + // First do quadratic terms since these are illegal for auxiliary objectives // Quadratic terms const int num_terms = objective.quadratic_coefficients().row_ids().size(); if (num_terms > 0) { + if (multiobj && objective_id.has_value()) { + return util::InvalidArgumentErrorBuilder() + << "Xpress does not support quadratic terms in anything but the " + "first objective"; + } std::vector first_var_index(num_terms); std::vector second_var_index(num_terms); std::vector coefficients(num_terms); @@ -825,6 +921,41 @@ absl::Status XpressSolver::AddSingleObjective(const ObjectiveProto& objective) { RETURN_IF_ERROR(xpress_->SetQuadraticObjective( first_var_index, second_var_index, coefficients)); } + + // Linear terms + std::vector index; + index.reserve(objective.linear_coefficients().ids_size()); + for (const int64_t id : objective.linear_coefficients().ids()) { + index.push_back(variables_map_.at(id)); + } + + if (multiobj) { + if (!objective_id.has_value()) { + // Primary objective + RETURN_IF_ERROR(xpress_->SetLinearObjective( + objective.offset(), index, objective.linear_coefficients().values())); + RETURN_IF_ERROR(xpress_->SetObjectiveIntControl( + 0, XPRS_OBJECTIVE_PRIORITY, + // checked above + static_cast(-objective.priority()))); + RETURN_IF_ERROR( + xpress_->SetObjectiveDoubleControl(0, XPRS_OBJECTIVE_WEIGHT, weight)); + } else { + // Auxiliary objective + ASSIGN_OR_RETURN( + int const newid, + xpress_->AddObjective( + objective.offset(), static_cast(index.size()), index.data(), + objective.linear_coefficients().values().data(), + // checked above + static_cast(-objective.priority()), weight)); + gtl::InsertOrDie(&objectives_map_, objective_id.value(), newid); + } + } else { + RETURN_IF_ERROR(xpress_->SetLinearObjective( + objective.offset(), index, objective.linear_coefficients().values())); + } + return absl::OkStatus(); } @@ -876,7 +1007,8 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback)); RETURN_IF_ERROR(solveContext.ApplyModelParameters( - model_parameters, variables_map_, linear_constraints_map_)); + model_parameters, variables_map_, linear_constraints_map_, + objectives_map_)); // Solve. We use the generic XPRSoptimize() and let Xpress decide what is // the best algorithm. Note that we do not pass flags to the function // either. We assume that algorithms are configured via controls like @@ -944,6 +1076,23 @@ absl::StatusOr XpressSolver::GetBestDualBound() const { return objsen * kMinusInf; } +/** Extend a solution with multi-objective information (if there is more than + * one objective). + */ +absl::Status XpressSolver::ExtendWithMultiobj(SolutionProto& solution) { + // We may not have solved for all objectives, so make sure we query only + // those that were solved. + ASSIGN_OR_RETURN(int const nSolved, xpress_->GetIntAttr(XPRS_SOLVEDOBJS)); + auto* objvals = + solution.mutable_primal_solution()->mutable_auxiliary_objective_values(); + for (auto const& [ortoolsId, xpressId] : objectives_map_) { + ASSIGN_OR_RETURN(double const thisobj, + xpress_->CalculateObjectiveN(xpressId, nullptr)); + (*objvals)[ortoolsId] = thisobj; + } + return absl::OkStatus(); +} + absl::Status XpressSolver::AppendSolution( SolveResultProto& solve_result, const ModelSolveParametersProto& model_parameters, @@ -960,6 +1109,7 @@ absl::Status XpressSolver::AppendSolution( ASSIGN_OR_RETURN(const double objval, xpress_->GetDoubleAttr(XPRS_OBJVAL)); solution.mutable_primal_solution()->set_objective_value(objval); + RETURN_IF_ERROR(ExtendWithMultiobj(solution)); XpressVectorToSparseDoubleVector( x, variables_map_, *solution.mutable_primal_solution()->mutable_variable_values(), @@ -996,6 +1146,7 @@ absl::Status XpressSolver::AppendSolution( primals, variables_map_, *solution.mutable_primal_solution()->mutable_variable_values(), model_parameters.variable_values_filter()); + RETURN_IF_ERROR(ExtendWithMultiobj(solution)); } else if (storeSolutions) { // Even if we are not primal feasible, store the results we obtained // from XPRSgetlpsolution(). The feasibility status of this vector diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 4f252a1d08a..6af5329ce97 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -111,6 +111,7 @@ class XpressSolver : public SolverInterface { absl::StatusOr ExtractSolveResultProto( absl::Time start, const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters); + absl::Status ExtendWithMultiobj(SolutionProto& solution); absl::Status AppendSolution(SolveResultProto& solve_result, const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters); @@ -131,7 +132,9 @@ class XpressSolver : public SolverInterface { absl::Status AddNewLinearConstraints( const LinearConstraintsProto& constraints); absl::Status AddNewVariables(const VariablesProto& new_variables); - absl::Status AddSingleObjective(const ObjectiveProto& objective); + absl::Status AddObjective(const ObjectiveProto& objective, + std::optional objective_id, + bool multiobj); absl::Status ChangeCoefficients(const SparseDoubleMatrixProto& matrix); absl::Status LoadModel(const ModelProto& input_model); @@ -155,6 +158,10 @@ class XpressSolver : public SolverInterface { // Xpress-numbered linear constraint and extra information. gtl::linked_hash_map linear_constraints_map_; + // Internal correspondence from objective proto IDs to Xpress-numbered + // objectives. + gtl::linked_hash_map + objectives_map_; int get_model_index(XpressVariableIndex index) const { return index; } int get_model_index(const LinearConstraintData& index) const { @@ -164,6 +171,7 @@ class XpressSolver : public SolverInterface { SolutionStatusProto getDualSolutionStatus() const; absl::StatusOr ListInvertedBounds() const; + bool is_multiobj_ = false; bool is_mip_ = false; // Results of the last solve int primal_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 5437839839d..62dfc3385d0 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -225,7 +225,7 @@ INSTANTIATE_TEST_SUITE_P( MultiObjectiveTestParameters GetXpressMultiObjectiveTestParameters() { return MultiObjectiveTestParameters( /*solver_type=*/SolverType::kXpress, /*parameters=*/SolveParameters(), - /*supports_auxiliary_objectives=*/false, + /*supports_auxiliary_objectives=*/true, /*supports_incremental_objective_add_and_delete=*/false, /*supports_incremental_objective_modification=*/false, /*supports_integer_variables=*/true); diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index 1ce8e8a49fe..843aacad7b6 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -61,6 +61,8 @@ std::function XPRSsetintcontrol = nu std::function XPRSsetintcontrol64 = nullptr; std::function XPRSsetdblcontrol = nullptr; std::function XPRSsetstrcontrol = nullptr; +std::function XPRSsetobjintcontrol = nullptr; +std::function XPRSsetobjdblcontrol = nullptr; std::function XPRSgetintcontrol = nullptr; std::function XPRSgetintcontrol64 = nullptr; std::function XPRSgetdblcontrol = nullptr; @@ -68,6 +70,8 @@ std::function XPRSgetintattrib = nullptr; std::function XPRSgetstringattrib = nullptr; std::function XPRSgetdblattrib = nullptr; +std::function XPRSgetobjdblattrib = nullptr; +std::function XPRScalcobjn = nullptr; std::function XPRSgetcontrolinfo = nullptr; std::function XPRSgetobj = nullptr; std::function XPRSgetrhs = nullptr; @@ -81,6 +85,7 @@ std::function XPRSaddrows = nullptr; std::function XPRSdelrows = nullptr; std::function XPRSaddcols = nullptr; +std::function XPRSaddobj = nullptr; std::function XPRSaddnames = nullptr; std::function XPRSgetnames = nullptr; std::function XPRSdelcols = nullptr; @@ -144,6 +149,8 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSsetintcontrol64, "XPRSsetintcontrol64"); xpress_dynamic_library->GetFunction(&XPRSsetdblcontrol, "XPRSsetdblcontrol"); xpress_dynamic_library->GetFunction(&XPRSsetstrcontrol, "XPRSsetstrcontrol"); + xpress_dynamic_library->GetFunction(&XPRSsetobjintcontrol, "XPRSsetobjintcontrol"); + xpress_dynamic_library->GetFunction(&XPRSsetobjdblcontrol, "XPRSsetobjdblcontrol"); xpress_dynamic_library->GetFunction(&XPRSgetintcontrol, "XPRSgetintcontrol"); xpress_dynamic_library->GetFunction(&XPRSgetintcontrol64, "XPRSgetintcontrol64"); xpress_dynamic_library->GetFunction(&XPRSgetdblcontrol, "XPRSgetdblcontrol"); @@ -151,6 +158,8 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSgetintattrib, "XPRSgetintattrib"); xpress_dynamic_library->GetFunction(&XPRSgetstringattrib, "XPRSgetstringattrib"); xpress_dynamic_library->GetFunction(&XPRSgetdblattrib, "XPRSgetdblattrib"); + xpress_dynamic_library->GetFunction(&XPRSgetobjdblattrib, "XPRSgetobjdblattrib"); + xpress_dynamic_library->GetFunction(&XPRScalcobjn, "XPRScalcobjn"); xpress_dynamic_library->GetFunction(&XPRSgetcontrolinfo, "XPRSgetcontrolinfo"); xpress_dynamic_library->GetFunction(&XPRSgetobj, "XPRSgetobj"); xpress_dynamic_library->GetFunction(&XPRSgetrhs, "XPRSgetrhs"); @@ -163,7 +172,9 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSgetredcosts, "XPRSgetredcosts"); xpress_dynamic_library->GetFunction(&XPRSaddrows, "XPRSaddrows"); xpress_dynamic_library->GetFunction(&XPRSdelrows, "XPRSdelrows"); + xpress_dynamic_library->GetFunction(&XPRSdelobj, "XPRSdelobj"); xpress_dynamic_library->GetFunction(&XPRSaddcols, "XPRSaddcols"); + xpress_dynamic_library->GetFunction(&XPRSaddobj, "XPRSaddobj"); xpress_dynamic_library->GetFunction(&XPRSaddnames, "XPRSaddnames"); xpress_dynamic_library->GetFunction(&XPRSgetnames, "XPRSgetnames"); xpress_dynamic_library->GetFunction(&XPRSdelcols, "XPRSdelcols"); diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index da89f51994b..b12b910cf65 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -121,6 +121,8 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); #define XPRS_ALGORITHM 1049 #define XPRS_STOPSTATUS 1179 #define XPRS_SOLVESTATUS 1394 +#define XPRS_OBJECTIVES 1397 +#define XPRS_SOLVEDOBJS 1399 #define XPRS_OBJVAL 2118 #define XPRS_BARPRIMALOBJ 4001 #define XPRS_BARDUALOBJ 4002 @@ -505,6 +507,15 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); #define XPRS_AT_UPPER 2 #define XPRS_FREE_SUPER 3 +// *************************************************************************** +// * values related to Objective control * +// *************************************************************************** +#define XPRS_OBJECTIVE_PRIORITY 20001 +#define XPRS_OBJECTIVE_WEIGHT 20002 +#define XPRS_OBJECTIVE_ABSTOL 20003 +#define XPRS_OBJECTIVE_RELTOL 20004 +#define XPRS_OBJECTIVE_RHS 20005 + // Let's not reformat for rest of the file. // NOLINTBEGIN(whitespace/line_length) // clang-format off @@ -524,6 +535,8 @@ extern std::function XPRSsetintcontr extern std::function XPRSsetintcontrol64; extern std::function XPRSsetdblcontrol; extern std::function XPRSsetstrcontrol; +extern std::function XPRSsetobjintcontrol; +extern std::function XPRSsetobjdblcontrol; OR_DLL extern std::function XPRSgetintcontrol; OR_DLL extern std::function XPRSgetintcontrol64; OR_DLL extern std::function XPRSgetdblcontrol; @@ -531,6 +544,8 @@ OR_DLL extern std::function XPRSgetintattrib; OR_DLL extern std::function XPRSgetstringattrib; OR_DLL extern std::function XPRSgetdblattrib; +extern std::function XPRSgetobjdblattrib; +extern std::function XPRScalcobjn; extern std::function XPRSgetcontrolinfo; OR_DLL extern std::function XPRSgetobj; OR_DLL extern std::function XPRSgetrhs; @@ -544,6 +559,7 @@ extern std::function XPRSaddrows; extern std::function XPRSdelrows; extern std::function XPRSaddcols; +extern std::function XPRSaddobj; extern std::function XPRSaddnames; extern std::function XPRSgetnames; extern std::function XPRSdelcols; From c8b32d91467ca0f95cc5d751f1e00e59bc66b300 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 14 Oct 2025 11:05:22 +0200 Subject: [PATCH 49/70] Add support for special ordered sets. --- .../solver_tests/logical_constraint_tests.cc | 8 +++ ortools/math_opt/solvers/xpress/g_xpress.cc | 15 ++++++ ortools/math_opt/solvers/xpress/g_xpress.h | 4 ++ ortools/math_opt/solvers/xpress_solver.cc | 54 ++++++++++++++++++- ortools/math_opt/solvers/xpress_solver.h | 9 ++++ .../math_opt/solvers/xpress_solver_test.cc | 4 ++ .../third_party_solvers/xpress_environment.cc | 2 + .../third_party_solvers/xpress_environment.h | 5 ++ 8 files changed, 99 insertions(+), 2 deletions(-) diff --git a/ortools/math_opt/solver_tests/logical_constraint_tests.cc b/ortools/math_opt/solver_tests/logical_constraint_tests.cc index eed2ac39462..37dea3419af 100644 --- a/ortools/math_opt/solver_tests/logical_constraint_tests.cc +++ b/ortools/math_opt/solver_tests/logical_constraint_tests.cc @@ -89,6 +89,10 @@ constexpr absl::string_view no_indicator_support_message = // We test SOS1 constraints with both explicit weights and default weights. TEST_P(SimpleLogicalConstraintTest, CanBuildSos1Model) { + if (GetParam().solver_type == SolverType::kXpress) { + GTEST_SKIP() << "skipped since Xpress only supports SOS on variables (not " + "expressions)"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); model.AddSos1Constraint({3.0 * x + 2.0}, {3.0}); @@ -105,6 +109,10 @@ TEST_P(SimpleLogicalConstraintTest, CanBuildSos1Model) { // We test SOS2 constraints with both explicit weights and default weights. TEST_P(SimpleLogicalConstraintTest, CanBuildSos2Model) { + if (GetParam().solver_type == SolverType::kXpress) { + GTEST_SKIP() << "skipped since Xpress only supports SOS on variables (not " + "expressions)"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); model.AddSos2Constraint({3.0 * x + 2.0}, {3.0}); diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 30fc513d426..ed8f4758e50 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -491,4 +491,19 @@ absl::StatusOr Xpress::CalculateObjectiveN(int objidx, return objval; } +absl::Status Xpress::AddSets(absl::Span settype, + absl::Span start, + absl::Span colind, + absl::Span refval) { + ASSIGN_OR_RETURN(int const oldSets, GetIntAttr(XPRS_ORIGINALSETS)); + if (checkInt32Overflow(settype.size()) || + checkInt32Overflow(std::size_t(oldSets) + settype.size())) { + return absl::InvalidArgumentError( + "XPRESS cannot handle more than 2^31 SOSs"); + } + return ToStatus(XPRSaddsets64(xpress_model_, static_cast(settype.size()), + colind.size(), settype.data(), start.data(), + colind.data(), refval.data())); +} + } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index b7a97d71f58..b3f751e34f2 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -153,6 +153,10 @@ class Xpress { int priority, double weight); absl::StatusOr CalculateObjectiveN(int objidx, double const* solution); + absl::Status AddSets(absl::Span settype, + absl::Span start, + absl::Span colind, + absl::Span refval); private: XPRSprob xpress_model_; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 5cd68779750..3749b519d72 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -718,8 +718,8 @@ constexpr SupportedProblemStructures kXpressSupportedStructures = { .quadratic_objectives = SupportType::kSupported, .quadratic_constraints = SupportType::kNotSupported, .second_order_cone_constraints = SupportType::kNotSupported, - .sos1_constraints = SupportType::kNotSupported, - .sos2_constraints = SupportType::kNotSupported, + .sos1_constraints = SupportType::kSupported, + .sos2_constraints = SupportType::kSupported, .indicator_constraints = SupportType::kNotSupported}; absl::StatusOr> XpressSolver::New( @@ -758,6 +758,8 @@ absl::Status XpressSolver::LoadModel(const ModelProto& input_model) { } RETURN_IF_ERROR(AddObjective(obj, id, true)); } + RETURN_IF_ERROR(AddSOS(input_model.sos1_constraints(), true)); + RETURN_IF_ERROR(AddSOS(input_model.sos2_constraints(), false)); return absl::OkStatus(); } absl::Status XpressSolver::AddNewVariables( @@ -959,6 +961,54 @@ absl::Status XpressSolver::AddObjective( return absl::OkStatus(); } +/** Add an SOS constraint. + * Note that in ortools an SOS constraint is made up from expressions and not + * just variables. Here, we only support SOSs in which each expression is just + * a single variable with coefficient 1. + * Also, ortools supports SOSs with identical elements and assumes that + * something like { x, x } is reduced to just { x }. This is debatable: + * If you consider { x, x } a set, then clearly it is the same as { x }. + * But if you consider "at most one of x and x may be non-zero", then { x, x } + * implies x=0. This is not how ortools interprets SOSs with duplicate entries + * (see the tests). + * We do not check for duplicate entries here, but Xpress will choke on them. + */ +absl::Status XpressSolver::AddSOS( + const google::protobuf::Map& sets, + bool sos1) { + if (sets.empty()) return absl::OkStatus(); + std::vector start; + std::vector colind; + std::vector refval; + ASSIGN_OR_RETURN(int nextId, xpress_->GetIntAttr(XPRS_ORIGINALSETS)); + auto* sosmap = sos1 ? &sos1_map_ : &sos2_map_; + for (auto const& [sosId, sos] : sets) { + start.push_back(colind.size()); + auto count = sos.expressions_size(); + bool const has_weight = sos.weights_size() > 0; + for (decltype(count) i = 0; i < count; ++i) { + auto const& expr = sos.expressions(i); + double const weight = has_weight ? sos.weights(i) : (i + 1); + if (expr.offset() != 0.0) + return util::InvalidArgumentErrorBuilder() + << "Xpress only supports SOS on singleton variables (offset)"; + if (expr.ids_size() != 1) + return util::InvalidArgumentErrorBuilder() + << "Xpress only supports SOS on singleton variables (multiple)"; + if (expr.coefficients(0) != 1.0) + return util::InvalidArgumentErrorBuilder() + << "Xpress only supports SOS on singleton variables (non-unit)"; + colind.push_back(variables_map_.at(expr.ids(0))); + refval.push_back(weight); + } + gtl::InsertOrDie(sosmap, sosId, nextId); + ++nextId; + } + std::vector settype(start.size(), sos1 ? '1' : '2'); + RETURN_IF_ERROR(xpress_->AddSets(settype, start, colind, refval)); + return absl::OkStatus(); +} + absl::Status XpressSolver::ChangeCoefficients( const SparseDoubleMatrixProto& matrix) { const int num_coefficients = matrix.row_ids().size(); diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 6af5329ce97..4857009f6cb 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -135,6 +135,9 @@ class XpressSolver : public SolverInterface { absl::Status AddObjective(const ObjectiveProto& objective, std::optional objective_id, bool multiobj); + absl::Status AddSOS( + const google::protobuf::Map& sets, + bool sos1); absl::Status ChangeCoefficients(const SparseDoubleMatrixProto& matrix); absl::Status LoadModel(const ModelProto& input_model); @@ -162,6 +165,12 @@ class XpressSolver : public SolverInterface { // objectives. gtl::linked_hash_map objectives_map_; + // Internal correspondence from SOS1 proto IDs to Xpress-numbered + // objectives. + gtl::linked_hash_map sos1_map_; + // Internal correspondence from SOS2 proto IDs to Xpress-numbered + // objectives. + gtl::linked_hash_map sos2_map_; int get_model_index(XpressVariableIndex index) const { return index; } int get_model_index(const LinearConstraintData& index) const { diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 62dfc3385d0..33b0a05c8b8 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -206,6 +206,10 @@ LogicalConstraintTestParameters GetXpressLogicalConstraintTestParameters() { return LogicalConstraintTestParameters( SolverType::kXpress, SolveParameters(), /*supports_integer_variables=*/true, + // Note: Xpress supports SOS, but it only supports SOSs that comprise + // solely of variables (not expressions) and it does not support + // duplicate entries. Many of the SOS tests construct things + // like this, so we skip them. /*supports_sos1=*/false, /*supports_sos2=*/false, /*supports_indicator_constraints=*/false, diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index 843aacad7b6..5796514a565 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -88,6 +88,7 @@ std::function XPRSaddobj = nullptr; std::function XPRSaddnames = nullptr; std::function XPRSgetnames = nullptr; +extern std::function XPRSaddsets64 = nullptr; std::function XPRSdelcols = nullptr; std::function XPRSchgcoltype = nullptr; std::function XPRSloadbasis = nullptr; @@ -177,6 +178,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSaddobj, "XPRSaddobj"); xpress_dynamic_library->GetFunction(&XPRSaddnames, "XPRSaddnames"); xpress_dynamic_library->GetFunction(&XPRSgetnames, "XPRSgetnames"); + xpress_dynamic_library->GetFunction(&XPRSaddsets64, "XPRSaddsets64"); xpress_dynamic_library->GetFunction(&XPRSdelcols, "XPRSdelcols"); xpress_dynamic_library->GetFunction(&XPRSchgcoltype, "XPRSchgcoltype"); xpress_dynamic_library->GetFunction(&XPRSloadbasis, "XPRSloadbasis"); diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index b12b910cf65..1a886faf075 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -123,6 +123,10 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); #define XPRS_SOLVESTATUS 1394 #define XPRS_OBJECTIVES 1397 #define XPRS_SOLVEDOBJS 1399 +#define XPRS_ORIGINALCOLS 1214 +#define XPRS_ORIGINALROWS 1124 +#define XPRS_ORIGINALMIPENTS 1191 +#define XPRS_ORIGINALSETS 1194 #define XPRS_OBJVAL 2118 #define XPRS_BARPRIMALOBJ 4001 #define XPRS_BARDUALOBJ 4002 @@ -563,6 +567,7 @@ extern std::function XPRSaddnames; extern std::function XPRSgetnames; extern std::function XPRSdelcols; +extern std::function XPRSaddsets64; extern std::function XPRSchgcoltype; extern std::function XPRSloadbasis; extern std::function XPRSpostsolve; From 09fe47e5e89142daf12b4d98722d7d78d643d021 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 14 Oct 2025 15:01:09 +0200 Subject: [PATCH 50/70] Add support for indicator constraints. --- .../solver_tests/logical_constraint_tests.cc | 40 +++++ ortools/math_opt/solvers/xpress/g_xpress.cc | 55 +++++++ ortools/math_opt/solvers/xpress/g_xpress.h | 10 ++ ortools/math_opt/solvers/xpress_solver.cc | 151 ++++++++++++++---- ortools/math_opt/solvers/xpress_solver.h | 25 ++- .../math_opt/solvers/xpress_solver_test.cc | 2 +- .../third_party_solvers/xpress_environment.cc | 6 +- .../third_party_solvers/xpress_environment.h | 4 + 8 files changed, 257 insertions(+), 36 deletions(-) diff --git a/ortools/math_opt/solver_tests/logical_constraint_tests.cc b/ortools/math_opt/solver_tests/logical_constraint_tests.cc index 37dea3419af..32246937d31 100644 --- a/ortools/math_opt/solver_tests/logical_constraint_tests.cc +++ b/ortools/math_opt/solver_tests/logical_constraint_tests.cc @@ -86,6 +86,15 @@ constexpr absl::string_view no_sos2_support_message = constexpr absl::string_view no_indicator_support_message = "This test is disabled as the solver does not support indicator " "constraints"; +constexpr absl::string_view no_updating_binary_variables_message = + "This test is disabled as the solver does not support updating " + "binary variables"; +constexpr absl::string_view no_deleting_indicator_variables_message = + "This test is disabled as the solver does not support deleting " + "indicator variables"; +constexpr absl::string_view no_incremental_add_and_deletes_message = + "This test is disabled as the solver does not support incremental " + "add/delete"; // We test SOS1 constraints with both explicit weights and default weights. TEST_P(SimpleLogicalConstraintTest, CanBuildSos1Model) { @@ -292,6 +301,9 @@ TEST_P(SimpleLogicalConstraintTest, Sos2VariableInMultipleTerms) { // The optimal solution for the modified problem is (x*, y*) = (0, 1) with // objective value 2. TEST_P(IncrementalLogicalConstraintTest, LinearToSos1Update) { + if (!GetParam().supports_incremental_add_and_deletes) { + GTEST_SKIP() << no_incremental_add_and_deletes_message; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -343,6 +355,9 @@ TEST_P(IncrementalLogicalConstraintTest, LinearToSos1Update) { // The optimal solution for the modified problem is (x*, y*, z*) = (0, 1, 1) // with objective value 4. TEST_P(IncrementalLogicalConstraintTest, LinearToSos2Update) { + if (!GetParam().supports_incremental_add_and_deletes) { + GTEST_SKIP() << no_incremental_add_and_deletes_message; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -768,6 +783,13 @@ TEST_P(SimpleLogicalConstraintTest, IndicatorsWithOddButValidBounds) { if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } + if (GetParam().solver_type == SolverType::kXpress) { + // This test does not work with Xpress because the Xpress library will + // refuse to create an indicator constraint if the indicator variable is + // not a binary variable (it will raise error 1029); + GTEST_SKIP() << "This test is disabled as Xpress does not support " + "indicator constraints on non-binary variables"; + } Model model; const Variable x = model.AddIntegerVariable(0.0, 0.0, "x"); const Variable y = model.AddIntegerVariable(1.0, 1.0, "y"); @@ -863,6 +885,9 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesIndicatorConstraint) { if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } + if (!GetParam().supports_incremental_add_and_deletes) { + GTEST_SKIP() << no_incremental_add_and_deletes_message; + } Model model; const Variable x = model.AddBinaryVariable("x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -906,6 +931,9 @@ TEST_P(IncrementalLogicalConstraintTest, if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } + if (!GetParam().supports_incremental_add_and_deletes) { + GTEST_SKIP() << no_incremental_add_and_deletes_message; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable indicator = model.AddBinaryVariable("indicator"); @@ -950,6 +978,9 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesIndicatorVariable) { if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } + if (!GetParam().supports_deleting_indicator_variables) { + GTEST_SKIP() << no_deleting_indicator_variables_message; + } Model model; const Variable x = model.AddBinaryVariable("x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -1030,6 +1061,9 @@ TEST_P(IncrementalLogicalConstraintTest, if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } + if (!GetParam().supports_updating_binary_variables) { + GTEST_SKIP() << no_updating_binary_variables_message; + } Model model; const Variable x = model.AddBinaryVariable("x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -1077,6 +1111,9 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateChangesIndicatorVariableBound) { if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } + if (!GetParam().supports_updating_binary_variables) { + GTEST_SKIP() << no_updating_binary_variables_message; + } Model model; const Variable x = model.AddIntegerVariable(0.0, 1.0, "x"); const Variable y = model.AddIntegerVariable(0.0, 1.0, "y"); @@ -1141,6 +1178,9 @@ TEST_P(IncrementalLogicalConstraintTest, if (!GetParam().supports_indicator_constraints) { GTEST_SKIP() << no_indicator_support_message; } + if (!GetParam().supports_updating_binary_variables) { + GTEST_SKIP() << no_updating_binary_variables_message; + } Model model; const Variable x = model.AddIntegerVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index ed8f4758e50..5384fbabb38 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -506,4 +506,59 @@ absl::Status Xpress::AddSets(absl::Span settype, colind.data(), refval.data())); } +absl::Status Xpress::SetIndicators(absl::Span rowind, + absl::Span colind, + absl::Span complement) { + ASSIGN_OR_RETURN(int const oldInds, GetIntAttr(XPRS_ORIGINALINDICATORS)); + if (checkInt32Overflow(rowind.size()) || + checkInt32Overflow(std::size_t(oldInds) + rowind.size())) { + return absl::InvalidArgumentError( + "XPRESS cannot handle more than 2^31 indicators"); + } + if (rowind.size() != colind.size() || rowind.size() != complement.size()) { + return absl::InvalidArgumentError( + "inconsistent arguments to SetInidicators"); + } + return ToStatus( + XPRSsetindicators(xpress_model_, static_cast(rowind.size()), + rowind.data(), colind.data(), complement.data())); +} + +absl::Status Xpress::AddRows(absl::Span rowtype, + absl::Span rhs, + absl::Span rng, + absl::Span start, + absl::Span colind, + absl::Span rowcoef) { + ASSIGN_OR_RETURN(int const oldRows, GetIntAttr(XPRS_ORIGINALROWS)); + if (checkInt32Overflow(rowtype.size()) || + checkInt32Overflow(std::size_t(oldRows) + rowtype.size())) { + return absl::InvalidArgumentError( + "XPRESS cannot handle more than 2^31 rows"); + } + if (rowtype.size() != rhs.size() || rowtype.size() != rng.size() || + rowtype.size() != start.size() || colind.size() != rowcoef.size()) + return absl::InvalidArgumentError("inconsistent arguments to AddRows"); + return ToStatus(XPRSaddrows64(xpress_model_, static_cast(rowtype.size()), + colind.size(), rowtype.data(), rhs.data(), + rng.data(), start.data(), colind.data(), + rowcoef.data())); +} + +absl::StatusOr Xpress::IsBinary(int colidx) { + char ctype; + RETURN_IF_ERROR( + ToStatus(XPRSgetcoltype(xpress_model_, &ctype, colidx, colidx))); + if (ctype == XPRS_BINARY) + return true; + else if (ctype != XPRS_INTEGER) + return false; + double bnd; + RETURN_IF_ERROR(ToStatus(XPRSgetlb(xpress_model_, &bnd, colidx, colidx))); + if (bnd < 0.0 || bnd > 1.0) return false; + RETURN_IF_ERROR(ToStatus(XPRSgetub(xpress_model_, &bnd, colidx, colidx))); + if (bnd < 0.0 || bnd > 1.0) return false; + return true; +} + } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index b3f751e34f2..8c2321df3ac 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -157,6 +157,16 @@ class Xpress { absl::Span start, absl::Span colind, absl::Span refval); + absl::Status SetIndicators(absl::Span rowind, + absl::Span colind, + absl::Span complement); + absl::Status AddRows(absl::Span rowtype, + absl::Span rhs, + absl::Span rng, + absl::Span start, + absl::Span colind, + absl::Span rowcoef); + absl::StatusOr IsBinary(int colidx); private: XPRSprob xpress_model_; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 3749b519d72..57d2463ac27 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -720,7 +720,7 @@ constexpr SupportedProblemStructures kXpressSupportedStructures = { .second_order_cone_constraints = SupportType::kNotSupported, .sos1_constraints = SupportType::kSupported, .sos2_constraints = SupportType::kSupported, - .indicator_constraints = SupportType::kNotSupported}; + .indicator_constraints = SupportType::kSupported}; absl::StatusOr> XpressSolver::New( const ModelProto& model, const InitArgs&) { @@ -760,6 +760,7 @@ absl::Status XpressSolver::LoadModel(const ModelProto& input_model) { } RETURN_IF_ERROR(AddSOS(input_model.sos1_constraints(), true)); RETURN_IF_ERROR(AddSOS(input_model.sos2_constraints(), false)); + RETURN_IF_ERROR(AddIndicators(input_model.indicator_constraints())); return absl::OkStatus(); } absl::Status XpressSolver::AddNewVariables( @@ -798,6 +799,40 @@ absl::Status XpressSolver::AddNewVariables( XpressSolver::XpressSolver(std::unique_ptr g_xpress) : xpress_(std::move(g_xpress)) {} +void XpressSolver::ParseBounds(double lb, double ub, char& sense, double& rhs, + double& rng) { + sense = XPRS_EQUAL; + rhs = 0.0; + rng = 0.0; + const bool lb_is_xprs_neg_inf = lb <= kMinusInf; + const bool ub_is_xprs_pos_inf = ub >= kPlusInf; + if (lb_is_xprs_neg_inf && ub_is_xprs_pos_inf) { + // We have a row + // -inf <= expression <= inf + // Xpress has no way to submit this as a ranged constraint. For Xpress + // the upper bound of the constraint is just the ub and the lower bound + // is computed as ub-abs(lb). This would result in inf-inf=nan if you + // use IEEE infinity or XPRS_INFINITY - XPRS_INFINITY = 0. Both are wrong. + // So we explicitly register this as free row. + sense = XPRS_NONBINDING; + rhs = 0.0; + rng = 0.0; + } else if (lb_is_xprs_neg_inf && !ub_is_xprs_pos_inf) { + sense = XPRS_LESS_EQUAL; + rhs = ub; + } else if (!lb_is_xprs_neg_inf && ub_is_xprs_pos_inf) { + sense = XPRS_GREATER_EQUAL; + rhs = lb; + } else if (lb == ub) { + sense = XPRS_EQUAL; + rhs = lb; + } else { + sense = XPRS_RANGE; + rhs = ub; + rng = ub - lb; + } +} + absl::Status XpressSolver::AddNewLinearConstraints( const LinearConstraintsProto& constraints) { // TODO: we might be able to improve performance by setting coefs also @@ -813,41 +848,14 @@ absl::Status XpressSolver::AddNewLinearConstraints( const int64_t id = constraints.ids(i); LinearConstraintData& constraint_data = gtl::InsertKeyOrDie(&linear_constraints_map_, id); - const double lb = constraints.lower_bounds(i); - const double ub = constraints.upper_bounds(i); - constraint_data.lower_bound = lb; - constraint_data.upper_bound = ub; + constraint_data.lower_bound = constraints.lower_bounds(i); + constraint_data.upper_bound = constraints.upper_bounds(i); constraint_data.constraint_index = i + n_constraints; char sense = XPRS_EQUAL; double rhs = 0.0; double rng = 0.0; - const bool lb_is_xprs_neg_inf = lb <= kMinusInf; - const bool ub_is_xprs_pos_inf = ub >= kPlusInf; - if (lb_is_xprs_neg_inf && ub_is_xprs_pos_inf) { - // We have a row - // -inf <= expression <= inf - // Xpress has no way to submit this as a ranged constraint. For Xpress - // the upper bound of the constraint is just the ub and the lower bound - // is computed as ub-abs(lb). This would result in inf-inf=nan if you - // use IEEE infinity or XPRS_INFINITY - XPRS_INFINITY = 0. Both are wrong. - // So we explicitly register this as free row. - sense = XPRS_NONBINDING; - rhs = 0.0; - rng = 0.0; - } else if (lb_is_xprs_neg_inf && !ub_is_xprs_pos_inf) { - sense = XPRS_LESS_EQUAL; - rhs = ub; - } else if (!lb_is_xprs_neg_inf && ub_is_xprs_pos_inf) { - sense = XPRS_GREATER_EQUAL; - rhs = lb; - } else if (lb == ub) { - sense = XPRS_EQUAL; - rhs = lb; - } else { - sense = XPRS_RANGE; - rhs = ub; - rng = ub - lb; - } + ParseBounds(constraint_data.lower_bound, constraint_data.upper_bound, sense, + rhs, rng); constraint_sense.emplace_back(sense); constraint_rhs.emplace_back(rhs); constraint_rng.emplace_back(rng); @@ -1009,6 +1017,80 @@ absl::Status XpressSolver::AddSOS( return absl::OkStatus(); } +void XpressSolver::ParseLinear(SparseDoubleVectorProto const& expr, double lb, + double ub, std::vector& colind, + std::vector& coef, char& sense, + double& rhs, double& rng) { + ParseBounds(lb, ub, sense, rhs, rng); + auto terms = expr.ids_size(); + for (decltype(terms) i = 0; i < terms; ++i) { + colind.push_back(variables_map_.at(expr.ids(i))); + coef.push_back(expr.values(i)); + } + /** TODO: How do we handle constant terms in expressions? */ +} + +absl::Status XpressSolver::AddIndicators( + const google::protobuf::Map& indicators) { + if (indicators.empty()) return absl::OkStatus(); + // For XPRSaddrows() + size_t count = indicators.size(); + std::vector rhs(count); + std::vector rng(count); + std::vector sense(count); + std::vector start(count); + std::vector colind; + std::vector rowcoef; + // For XPRSsetindicators() + std::vector i_rowind(count); + std::vector i_colind(count); + std::vector i_complement(count); + ASSIGN_OR_RETURN(int const oldRows, xpress_->GetIntAttr(XPRS_ORIGINALROWS)); + int next = 0; + for (auto const& [ortoolsId, indicator] : indicators) { + start[next] = colind.size(); + ParseLinear(indicator.expression(), indicator.lower_bound(), + indicator.upper_bound(), colind, rowcoef, sense[next], + rhs[next], rng[next]); + + // ortools tests require us to raise an error on ranged indicator + // constraints + if (sense[next] == XPRS_RANGE) { + return util::InvalidArgumentErrorBuilder() + << "indicator constraint on ranged constraint"; + } + + i_rowind[next] = oldRows + next; + if (indicator.has_indicator_id()) { + i_colind[next] = variables_map_.at(indicator.indicator_id()); + i_complement[next] = indicator.activate_on_zero() ? -1 : 1; + /** TODO: Can we do a faster check? Directly in ortools? */ + ASSIGN_OR_RETURN(bool const isBin, xpress_->IsBinary(i_colind[next])); + if (!isBin) { + // See the definition of nonbinary_indicator_ why we cannot raise + // an error right here. + nonbinary_indicator_ = true; + } + } else { + // By definition, this is an inactive constraint, see + // indicator_constraint.h + i_colind[next] = -1; + i_complement[next] = 0; + sense[next] = XPRS_NONBINDING; + } + LinearConstraintData& data = + gtl::InsertKeyOrDie(&indicator_map_, ortoolsId); + data.constraint_index = oldRows + next; + data.lower_bound = indicator.lower_bound(); + data.upper_bound = indicator.upper_bound(); + ++next; + } + RETURN_IF_ERROR(xpress_->AddRows(sense, rhs, rng, start, colind, rowcoef)); + RETURN_IF_ERROR(xpress_->SetIndicators(i_rowind, i_colind, i_complement)); + return absl::OkStatus(); +} + absl::Status XpressSolver::ChangeCoefficients( const SparseDoubleMatrixProto& matrix) { const int num_coefficients = matrix.row_ids().size(); @@ -1050,6 +1132,11 @@ absl::StatusOr XpressSolver::Solve( ListInvertedBounds()); RETURN_IF_ERROR(inverted_bounds.ToStatus()); } + // Check that we don't have non-binary indicator variables + if (nonbinary_indicator_) { + return util::InvalidArgumentErrorBuilder() + << "indicator variable is not binary"; + } // Register callbacks and create scoped context to automatically if an // exception has been thrown during optimization. diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 4857009f6cb..6386d07958c 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -126,6 +126,11 @@ class XpressSolver : public SolverInterface { bool isPrimalFeasible() const; bool isDualFeasible() const; + void ParseBounds(double lb, double ub, char& sense, double& rhs, double& rng); + void ParseLinear(SparseDoubleVectorProto const& expr, double lb, double ub, + std::vector& colind, std::vector& coef, + char& sense, double& rhs, double& rng); + absl::StatusOr> GetBasisIfAvailable( const SolveParametersProto& parameters); @@ -138,6 +143,9 @@ class XpressSolver : public SolverInterface { absl::Status AddSOS( const google::protobuf::Map& sets, bool sos1); + absl::Status AddIndicators( + const google::protobuf::Map& indicators); absl::Status ChangeCoefficients(const SparseDoubleMatrixProto& matrix); absl::Status LoadModel(const ModelProto& input_model); @@ -166,11 +174,15 @@ class XpressSolver : public SolverInterface { gtl::linked_hash_map objectives_map_; // Internal correspondence from SOS1 proto IDs to Xpress-numbered - // objectives. + // SOS1 constraints. gtl::linked_hash_map sos1_map_; // Internal correspondence from SOS2 proto IDs to Xpress-numbered - // objectives. + // SOS2 constraints. gtl::linked_hash_map sos2_map_; + // Internal correspondence from indicator proto IDs to Xpress-numbered + // indicators. + gtl::linked_hash_map + indicator_map_; int get_model_index(XpressVariableIndex index) const { return index; } int get_model_index(const LinearConstraintData& index) const { @@ -180,6 +192,15 @@ class XpressSolver : public SolverInterface { SolutionStatusProto getDualSolutionStatus() const; absl::StatusOr ListInvertedBounds() const; + /** Whether the model has a non-binary indicator variable. + * The behavior expected by ortools is that + * - we can happily create a model with non-binary indicators + * - this must fail at _solve_ time + * Xpress implicitly converts indicator variables to binaries, though, + * so we must keep track of this fact at build time and raise an error + * only at solve time. + */ + bool nonbinary_indicator_ = false; bool is_multiobj_ = false; bool is_mip_ = false; // Results of the last solve diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 33b0a05c8b8..5cc6d0b9a66 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -212,7 +212,7 @@ LogicalConstraintTestParameters GetXpressLogicalConstraintTestParameters() { // like this, so we skip them. /*supports_sos1=*/false, /*supports_sos2=*/false, - /*supports_indicator_constraints=*/false, + /*supports_indicator_constraints=*/true, /*supports_incremental_add_and_deletes=*/false, /*supports_incremental_variable_deletions=*/false, /*supports_deleting_indicator_variables=*/false, diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index 5796514a565..c812480338f 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -83,12 +83,13 @@ std::function std::function XPRSgetduals = nullptr; std::function XPRSgetredcosts = nullptr; std::function XPRSaddrows = nullptr; +std::function XPRSaddrows64 = nullptr; std::function XPRSdelrows = nullptr; std::function XPRSaddcols = nullptr; std::function XPRSaddobj = nullptr; std::function XPRSaddnames = nullptr; std::function XPRSgetnames = nullptr; -extern std::function XPRSaddsets64 = nullptr; +std::function XPRSaddsets64 = nullptr; std::function XPRSdelcols = nullptr; std::function XPRSchgcoltype = nullptr; std::function XPRSloadbasis = nullptr; @@ -102,6 +103,7 @@ std::function XPRSgetco std::function XPRSchgbounds = nullptr; std::function XPRSaddmipsol = nullptr; std::function XPRSloaddelayedrows = nullptr; +std::function XPRSsetindicators = nullptr; std::function XPRSloaddirs; std::function XPRSgetlpsol = nullptr; std::function XPRSgetmipsol = nullptr; @@ -172,6 +174,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSgetduals, "XPRSgetduals"); xpress_dynamic_library->GetFunction(&XPRSgetredcosts, "XPRSgetredcosts"); xpress_dynamic_library->GetFunction(&XPRSaddrows, "XPRSaddrows"); + xpress_dynamic_library->GetFunction(&XPRSaddrows64, "XPRSaddrows64"); xpress_dynamic_library->GetFunction(&XPRSdelrows, "XPRSdelrows"); xpress_dynamic_library->GetFunction(&XPRSdelobj, "XPRSdelobj"); xpress_dynamic_library->GetFunction(&XPRSaddcols, "XPRSaddcols"); @@ -192,6 +195,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSchgbounds, "XPRSchgbounds"); xpress_dynamic_library->GetFunction(&XPRSaddmipsol, "XPRSaddmipsol"); xpress_dynamic_library->GetFunction(&XPRSloaddelayedrows, "XPRSloaddelayedrows"); + xpress_dynamic_library->GetFunction(&XPRSsetindicators, "XPRSsetindicators"); xpress_dynamic_library->GetFunction(&XPRSloaddirs, "XPRSloaddirs"); xpress_dynamic_library->GetFunction(&XPRSgetlpsol, "XPRSgetlpsol"); xpress_dynamic_library->GetFunction(&XPRSgetmipsol, "XPRSgetmipsol"); diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index 1a886faf075..3ac0fa96e49 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -127,6 +127,7 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); #define XPRS_ORIGINALROWS 1124 #define XPRS_ORIGINALMIPENTS 1191 #define XPRS_ORIGINALSETS 1194 +#define XPRS_ORIGINALINDICATORS 1255 #define XPRS_OBJVAL 2118 #define XPRS_BARPRIMALOBJ 4001 #define XPRS_BARDUALOBJ 4002 @@ -493,6 +494,7 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); // *************************************************************************** // * variable types * // *************************************************************************** +#define XPRS_BINARY 'B' #define XPRS_INTEGER 'I' #define XPRS_CONTINUOUS 'C' // *************************************************************************** @@ -561,6 +563,7 @@ extern std::function XPRSgetduals; extern std::function XPRSgetredcosts; extern std::function XPRSaddrows; +extern std::function XPRSaddrows64; extern std::function XPRSdelrows; extern std::function XPRSaddcols; extern std::function XPRSaddobj; @@ -580,6 +583,7 @@ OR_DLL extern std::function XPRSchgbounds; extern std::function XPRSaddmipsol; extern std::function XPRSloaddelayedrows; +extern std::function XPRSsetindicators; extern std::function XPRSloaddirs; extern std::function XPRSgetlpsol; extern std::function XPRSgetmipsol; From d47383ea4aaeeea18b9b914a0352d67215c8bcd6 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 15 Oct 2025 13:39:48 +0200 Subject: [PATCH 51/70] Add support for quadratic constraints. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 25 ++++++ ortools/math_opt/solvers/xpress/g_xpress.h | 5 ++ ortools/math_opt/solvers/xpress_solver.cc | 84 +++++++++++++++++-- ortools/math_opt/solvers/xpress_solver.h | 17 +++- .../math_opt/solvers/xpress_solver_test.cc | 3 +- .../third_party_solvers/xpress_environment.cc | 2 + .../third_party_solvers/xpress_environment.h | 1 + 7 files changed, 124 insertions(+), 13 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 5384fbabb38..574d698d1b8 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -561,4 +561,29 @@ absl::StatusOr Xpress::IsBinary(int colidx) { return true; } +absl::Status Xpress::AddQRow(char sense, double rhs, double rng, + absl::Span colind, + absl::Span rowcoef, + absl::Span qcol1, + absl::Span qcol2, + absl::Span qcoef) { + ASSIGN_OR_RETURN(int const oldRows, GetIntAttr(XPRS_ORIGINALROWS)); + if (checkInt32Overflow(std::size_t(oldRows) + 1)) + return absl::InvalidArgumentError( + "XPRESS cannot handle more than 2^31 rows"); + XPRSint64 const start = 0; + RETURN_IF_ERROR( + ToStatus(XPRSaddrows64(xpress_model_, 1, colind.size(), &sense, &rhs, + &rng, &start, colind.data(), rowcoef.data()))); + if (qcol1.size() > 0) { + int const ret = XPRSaddqmatrix64(xpress_model_, oldRows, qcol1.size(), + qcol1.data(), qcol2.data(), qcoef.data()); + if (ret != 0) { + XPRSdelrows(xpress_model_, 1, &oldRows); + return ToStatus(ret); + } + } + return absl::OkStatus(); +} + } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 8c2321df3ac..7e136d04aad 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -166,6 +166,11 @@ class Xpress { absl::Span start, absl::Span colind, absl::Span rowcoef); + absl::Status AddQRow(char sense, double rhs, double rng, + absl::Span colind, + absl::Span rowcoef, + absl::Span qcol1, absl::Span qcol2, + absl::Span qcoef); absl::StatusOr IsBinary(int colidx); private: diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 57d2463ac27..474badc385e 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -716,7 +716,7 @@ constexpr SupportedProblemStructures kXpressSupportedStructures = { .integer_variables = SupportType::kSupported, .multi_objectives = SupportType::kSupported, .quadratic_objectives = SupportType::kSupported, - .quadratic_constraints = SupportType::kNotSupported, + .quadratic_constraints = SupportType::kSupported, .second_order_cone_constraints = SupportType::kNotSupported, .sos1_constraints = SupportType::kSupported, .sos2_constraints = SupportType::kSupported, @@ -761,6 +761,7 @@ absl::Status XpressSolver::LoadModel(const ModelProto& input_model) { RETURN_IF_ERROR(AddSOS(input_model.sos1_constraints(), true)); RETURN_IF_ERROR(AddSOS(input_model.sos2_constraints(), false)); RETURN_IF_ERROR(AddIndicators(input_model.indicator_constraints())); + RETURN_IF_ERROR(AddQuadraticConstraints(input_model.quadratic_constraints())); return absl::OkStatus(); } absl::Status XpressSolver::AddNewVariables( @@ -1017,12 +1018,12 @@ absl::Status XpressSolver::AddSOS( return absl::OkStatus(); } -void XpressSolver::ParseLinear(SparseDoubleVectorProto const& expr, double lb, - double ub, std::vector& colind, - std::vector& coef, char& sense, - double& rhs, double& rng) { - ParseBounds(lb, ub, sense, rhs, rng); +void XpressSolver::ParseLinear(SparseDoubleVectorProto const& expr, + std::vector& colind, + std::vector& coef) { auto terms = expr.ids_size(); + colind.reserve(colind.size() + terms); + coef.reserve(coef.size() + terms); for (decltype(terms) i = 0; i < terms; ++i) { colind.push_back(variables_map_.at(expr.ids(i))); coef.push_back(expr.values(i)); @@ -1030,6 +1031,37 @@ void XpressSolver::ParseLinear(SparseDoubleVectorProto const& expr, double lb, /** TODO: How do we handle constant terms in expressions? */ } +void XpressSolver::ParseQuadratic(QuadraticConstraintProto const& expr, + std::vector& lin_colind, + std::vector& lin_coef, + std::vector& quad_col1, + std::vector& quad_col2, + std::vector& quad_coef) { + auto const& lin = expr.linear_terms(); + auto linTerms = lin.ids_size(); + lin_colind.reserve(lin_colind.size() + linTerms); + lin_coef.reserve(lin_coef.size() + linTerms); + for (decltype(linTerms) i = 0; i < linTerms; ++i) { + lin_colind.push_back(variables_map_.at(lin.ids(i))); + lin_coef.push_back(lin.values(i)); + } + auto const& quad = expr.quadratic_terms(); + auto quadTerms = quad.row_ids_size(); + quad_col1.reserve(quad_col1.size() + quadTerms); + quad_col2.reserve(quad_col2.size() + quadTerms); + quad_coef.reserve(quad_coef.size() + quadTerms); + for (decltype(quadTerms) i = 0; i < quadTerms; ++i) { + int const col1 = variables_map_.at(quad.row_ids(i)); + int const col2 = variables_map_.at(quad.column_ids(i)); + double coef = quad.coefficients(i); + if (col1 != col2) coef *= 0.5; + quad_col1.push_back(col1); + quad_col2.push_back(col2); + quad_coef.push_back(coef); + } + /** TODO: How do we handle constant terms in expressions? */ +} + absl::Status XpressSolver::AddIndicators( const google::protobuf::Map& indicators) { @@ -1050,10 +1082,8 @@ absl::Status XpressSolver::AddIndicators( int next = 0; for (auto const& [ortoolsId, indicator] : indicators) { start[next] = colind.size(); - ParseLinear(indicator.expression(), indicator.lower_bound(), - indicator.upper_bound(), colind, rowcoef, sense[next], + ParseBounds(indicator.lower_bound(), indicator.upper_bound(), sense[next], rhs[next], rng[next]); - // ortools tests require us to raise an error on ranged indicator // constraints if (sense[next] == XPRS_RANGE) { @@ -1061,6 +1091,8 @@ absl::Status XpressSolver::AddIndicators( << "indicator constraint on ranged constraint"; } + ParseLinear(indicator.expression(), colind, rowcoef); + i_rowind[next] = oldRows + next; if (indicator.has_indicator_id()) { i_colind[next] = variables_map_.at(indicator.indicator_id()); @@ -1091,6 +1123,40 @@ absl::Status XpressSolver::AddIndicators( return absl::OkStatus(); } +absl::Status XpressSolver::AddQuadraticConstraints( + const google::protobuf::Map& constraints) { + std::vector lin_colind; + std::vector lin_coef; + std::vector quad_col1; + std::vector quad_col2; + std::vector quad_coef; + ASSIGN_OR_RETURN(int next, xpress_->GetIntAttr(XPRS_ORIGINALROWS)); + for (const auto& [ortoolsId, quad] : constraints) { + // Xpress has no function to multiple quadratic rows in one shot, so we + // add the linear part one by one as well. + char sense; + double rhs; + double rng; + ParseBounds(quad.lower_bound(), quad.upper_bound(), sense, rhs, rng); + lin_colind.clear(); + lin_coef.clear(); + quad_col1.clear(); + quad_col2.clear(); + quad_coef.clear(); + ParseQuadratic(quad, lin_colind, lin_coef, quad_col1, quad_col2, quad_coef); + RETURN_IF_ERROR(xpress_->AddQRow(sense, rhs, rng, lin_colind, lin_coef, + quad_col1, quad_col2, quad_coef)); + LinearConstraintData& data = + gtl::InsertKeyOrDie(&indicator_map_, ortoolsId); + data.constraint_index = next; + data.lower_bound = quad.lower_bound(); + data.upper_bound = quad.upper_bound(); + ++next; + } + return absl::OkStatus(); +} + absl::Status XpressSolver::ChangeCoefficients( const SparseDoubleMatrixProto& matrix) { const int num_coefficients = matrix.row_ids().size(); diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 6386d07958c..3de3bea1aa6 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -127,9 +127,13 @@ class XpressSolver : public SolverInterface { bool isDualFeasible() const; void ParseBounds(double lb, double ub, char& sense, double& rhs, double& rng); - void ParseLinear(SparseDoubleVectorProto const& expr, double lb, double ub, - std::vector& colind, std::vector& coef, - char& sense, double& rhs, double& rng); + void ParseLinear(SparseDoubleVectorProto const& expr, + std::vector& colind, std::vector& coef); + void ParseQuadratic(QuadraticConstraintProto const& expr, + std::vector& lin_colind, + std::vector& lin_coef, + std::vector& quad_col1, std::vector& quad_col2, + std::vector& quad_coef); absl::StatusOr> GetBasisIfAvailable( const SolveParametersProto& parameters); @@ -146,6 +150,9 @@ class XpressSolver : public SolverInterface { absl::Status AddIndicators( const google::protobuf::Map& indicators); + absl::Status AddQuadraticConstraints( + const google::protobuf::Map& constraints); absl::Status ChangeCoefficients(const SparseDoubleMatrixProto& matrix); absl::Status LoadModel(const ModelProto& input_model); @@ -183,6 +190,10 @@ class XpressSolver : public SolverInterface { // indicators. gtl::linked_hash_map indicator_map_; + // Internal correspondence from indicator proto IDs to Xpress-numbered + // indicators. + gtl::linked_hash_map + quad_constraints_map_; int get_model_index(XpressVariableIndex index) const { return index; } int get_model_index(const LinearConstraintData& index) const { diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 5cc6d0b9a66..a1d4dc56d19 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -247,6 +247,7 @@ std::vector GetXpressQpTestParameters() { std::vector test_parameters; for (int i = 0; i < 2; ++i) { test_parameters.push_back( + // TODO: Xpress also supports non-convex QP. QpTestParameters(SolverType::kXpress, SolveParameters(), /*qp_support=*/QpSupportType::kConvexQp, /*supports_incrementalism_not_modifying_qp=*/false, @@ -267,7 +268,7 @@ std::vector GetXpressQcTestParameters() { for (int i = 0; i < 2; ++i) { test_parameters.push_back( QcTestParameters(SolverType::kXpress, SolveParameters(), - /*supports_qc=*/false, + /*supports_qc=*/true, /*supports_incremental_add_and_deletes=*/false, /*supports_incremental_variable_deletions=*/false, /*use_integer_variables=*/i != 0)); diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index c812480338f..2831a74391d 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -87,6 +87,7 @@ std::function XPRSdelrows = nullptr; std::function XPRSaddcols = nullptr; std::function XPRSaddobj = nullptr; +std::function XPRSaddqmatrix64 = nullptr; std::function XPRSaddnames = nullptr; std::function XPRSgetnames = nullptr; std::function XPRSaddsets64 = nullptr; @@ -179,6 +180,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSdelobj, "XPRSdelobj"); xpress_dynamic_library->GetFunction(&XPRSaddcols, "XPRSaddcols"); xpress_dynamic_library->GetFunction(&XPRSaddobj, "XPRSaddobj"); + xpress_dynamic_library->GetFunction(&XPRSaddqmatrix64, "XPRSaddqmatrix64"); xpress_dynamic_library->GetFunction(&XPRSaddnames, "XPRSaddnames"); xpress_dynamic_library->GetFunction(&XPRSgetnames, "XPRSgetnames"); xpress_dynamic_library->GetFunction(&XPRSaddsets64, "XPRSaddsets64"); diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index 3ac0fa96e49..45522a3e80d 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -567,6 +567,7 @@ extern std::function XPRSdelrows; extern std::function XPRSaddcols; extern std::function XPRSaddobj; +extern std::function XPRSaddqmatrix64; extern std::function XPRSaddnames; extern std::function XPRSgetnames; extern std::function XPRSdelcols; From 2b9db82afe5b8a8b76181b1133fd607277edcc7c Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 15 Oct 2025 14:46:03 +0200 Subject: [PATCH 52/70] Factor out checking for singleton variables. --- ortools/math_opt/solvers/xpress_solver.cc | 35 ++++++++++++++++------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 474badc385e..9ddce4e7df5 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -710,6 +710,29 @@ class ScopedSolverContext { } }; +// ortools supports SOS constraints and second order cone constraints on +// expressions. Xpress only supports these constructs on singleton variables. +// We could create auxiliary variables here, set each of them equal to one of +// the expressions and then formulate SOS/SOC on the auxiliary variables. +// This however seems a bit of overkill at the moment, so we just error out +// if elements are non-singleton. +absl::StatusOr ExtractSingleton( + LinearExpressionProto const& expr, char const* what) { + if (expr.offset() != 0.0) + return util::InvalidArgumentErrorBuilder() + << "Xpress only supports " << what + << " on singleton variables (offset)"; + if (expr.ids_size() != 1) + return util::InvalidArgumentErrorBuilder() + << "Xpress only supports " << what + << " on singleton variables (empty or multiple)"; + if (expr.coefficients(0) != 1.0) + return util::InvalidArgumentErrorBuilder() + << "Xpress only supports " << what + << " on singleton variables (non-unit)"; + return expr.ids(0); +} + } // namespace constexpr SupportedProblemStructures kXpressSupportedStructures = { @@ -998,16 +1021,8 @@ absl::Status XpressSolver::AddSOS( for (decltype(count) i = 0; i < count; ++i) { auto const& expr = sos.expressions(i); double const weight = has_weight ? sos.weights(i) : (i + 1); - if (expr.offset() != 0.0) - return util::InvalidArgumentErrorBuilder() - << "Xpress only supports SOS on singleton variables (offset)"; - if (expr.ids_size() != 1) - return util::InvalidArgumentErrorBuilder() - << "Xpress only supports SOS on singleton variables (multiple)"; - if (expr.coefficients(0) != 1.0) - return util::InvalidArgumentErrorBuilder() - << "Xpress only supports SOS on singleton variables (non-unit)"; - colind.push_back(variables_map_.at(expr.ids(0))); + ASSIGN_OR_RETURN(VarId const x, ExtractSingleton(expr, "SOS")); + colind.push_back(variables_map_.at(x)); refval.push_back(weight); } gtl::InsertOrDie(sosmap, sosId, nextId); From af80e2b273761f71aaca453c27f5c8136f3879af Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 15 Oct 2025 15:12:18 +0200 Subject: [PATCH 53/70] Support `EXPORT_MODEL` control to dump model. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 10 +++++++ ortools/math_opt/solvers/xpress/g_xpress.h | 3 +++ ortools/math_opt/solvers/xpress_solver.cc | 27 ++++++++++++++++--- .../third_party_solvers/xpress_environment.cc | 2 ++ .../third_party_solvers/xpress_environment.h | 1 + 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 574d698d1b8..6532288a612 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -586,4 +586,14 @@ absl::Status Xpress::AddQRow(char sense, double rhs, double rng, return absl::OkStatus(); } +absl::Status Xpress::WriteProb(std::string const& filename, + std::string const& flags) { + return ToStatus( + XPRSwriteprob(xpress_model_, filename.c_str(), flags.c_str())); +} + +absl::Status Xpress::SaveAs(std::string const& filename) { + return ToStatus(XPRSsaveas(xpress_model_, filename.c_str())); +} + } // namespace operations_research::math_opt diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 7e136d04aad..27e00cecdff 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -172,6 +172,9 @@ class Xpress { absl::Span qcol1, absl::Span qcol2, absl::Span qcoef); absl::StatusOr IsBinary(int colidx); + absl::Status WriteProb(std::string const& filename, + std::string const& flags = ""); + absl::Status SaveAs(std::string const& filename); private: XPRSprob xpress_model_; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 9ddce4e7df5..17144c0f925 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -344,7 +344,8 @@ class ScopedSolverContext { } /** Setup model specific parameters. */ absl::Status ApplyParameters(const SolveParametersProto& parameters, - MessageCallback message_callback) { + MessageCallback message_callback, + std::string* export_model) { std::vector warnings; ASSIGN_OR_RETURN(bool const isMIP, shared_ctx.xpress->IsMIP()); if (parameters.enable_output()) { @@ -490,6 +491,10 @@ class ScopedSolverContext { parameters.xpress().parameters()) { std::string const& name = parameter.name(); std::string const& value = parameter.value(); + if (name == "EXPORT_MODEL") { + if (export_model) *export_model = value; + continue; + } int id, type; int64_t l; double d; @@ -1223,10 +1228,25 @@ absl::StatusOr XpressSolver::Solve( // exception has been thrown during optimization. ScopedSolverContext solveContext(xpress_.get()); RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); - RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback)); + std::string export_model = ""; + RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback, + &export_model)); RETURN_IF_ERROR(solveContext.ApplyModelParameters( model_parameters, variables_map_, linear_constraints_map_, objectives_map_)); + + // We are ready to solve the problem. If we are asked to export the + // problem, then do that now. Depending on the file name extension we + // either create a save file or an LP/MPS file. + if (export_model.length() > 0) { + if (export_model.length() >= 4 && + export_model.compare(export_model.length() - 4, 4, ".svf") == 0) { + RETURN_IF_ERROR(xpress_->SaveAs(export_model.c_str())); + } else { + RETURN_IF_ERROR(xpress_->WriteProb(export_model.c_str())); + } + } + // Solve. We use the generic XPRSoptimize() and let Xpress decide what is // the best algorithm. Note that we do not pass flags to the function // either. We assume that algorithms are configured via controls like @@ -1736,7 +1756,8 @@ XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, const SolveInterrupter* interrupter) { ScopedSolverContext solveContext(xpress_.get()); RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); - RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback)); + RETURN_IF_ERROR( + solveContext.ApplyParameters(parameters, message_callback, nullptr)); return absl::UnimplementedError( "XPRESS does not provide a method to compute an infeasible subsystem"); diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index 2831a74391d..1885cb75967 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -99,6 +99,7 @@ std::function XPRSchgobjsense = nullptr; std::function XPRSgetlasterror = nullptr; std::function XPRSgetbasis = nullptr; std::function XPRSwriteprob = nullptr; +std::function XPRSsaveas = nullptr; std::function XPRSgetrowtype = nullptr; std::function XPRSgetcoltype = nullptr; std::function XPRSchgbounds = nullptr; @@ -192,6 +193,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSgetlasterror, "XPRSgetlasterror"); xpress_dynamic_library->GetFunction(&XPRSgetbasis, "XPRSgetbasis"); xpress_dynamic_library->GetFunction(&XPRSwriteprob, "XPRSwriteprob"); + xpress_dynamic_library->GetFunction(&XPRSsaveas, "XPRSsaveas"); xpress_dynamic_library->GetFunction(&XPRSgetrowtype, "XPRSgetrowtype"); xpress_dynamic_library->GetFunction(&XPRSgetcoltype, "XPRSgetcoltype"); xpress_dynamic_library->GetFunction(&XPRSchgbounds, "XPRSchgbounds"); diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index 45522a3e80d..87b233caa15 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -579,6 +579,7 @@ extern std::function XPRSchgobjsense; extern std::function XPRSgetlasterror; extern std::function XPRSgetbasis; extern std::function XPRSwriteprob; +extern std::function XPRSsaveas; OR_DLL extern std::function XPRSgetrowtype; OR_DLL extern std::function XPRSgetcoltype; extern std::function XPRSchgbounds; From b0a3fdbe13f62109e172cc66b01a98ccbdcc1b95 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Wed, 15 Oct 2025 15:25:35 +0200 Subject: [PATCH 54/70] Add support for second order cone constraints. --- .../solver_tests/second_order_cone_tests.cc | 34 ++++ ortools/math_opt/solvers/xpress_solver.cc | 146 +++++++++++++++--- ortools/math_opt/solvers/xpress_solver.h | 10 +- .../math_opt/solvers/xpress_solver_test.cc | 2 +- 4 files changed, 171 insertions(+), 21 deletions(-) diff --git a/ortools/math_opt/solver_tests/second_order_cone_tests.cc b/ortools/math_opt/solver_tests/second_order_cone_tests.cc index a411f4a96d2..a2ce56b2e74 100644 --- a/ortools/math_opt/solver_tests/second_order_cone_tests.cc +++ b/ortools/math_opt/solver_tests/second_order_cone_tests.cc @@ -62,6 +62,9 @@ constexpr double kTolerance = 1.0e-3; constexpr absl::string_view kNoSocSupportMessage = "This test is disabled as the solver does not support second-order cone " "constraints"; +constexpr absl::string_view kNoIncrementalAddAndDeletes = + "This test is disabled as the solver does not support incremental add and " + "deletes"; // Builds the simple (and uninteresting) SOC model: // @@ -69,6 +72,14 @@ constexpr absl::string_view kNoSocSupportMessage = // s.t. ||x||_2 <= 2x // 0 <= x <= 1. TEST_P(SimpleSecondOrderConeTest, CanBuildSecondOrderConeModel) { + if (GetParam().solver_type == SolverType::kXpress) { + // For Xpress the second order cone constraint results in + // x^2 - 4x^2 <= 0 + // This has two entries for x and will thus be rejected by the library. + // Hence we have to skip the test. + GTEST_SKIP() + << "This test is disabled as Xpress rejects duplicate Q entries"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); model.AddSecondOrderConeConstraint({x}, 2 * x); @@ -94,6 +105,10 @@ TEST_P(SimpleSecondOrderConeTest, SolveSimpleSocModel) { if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } + if (GetParam().solver_type == SolverType::kXpress) { + GTEST_SKIP() << "This test is disabled as Xpress only supports second " + "order cone constraints on singletons"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -117,6 +132,10 @@ TEST_P(SimpleSecondOrderConeTest, SolveMultipleSocConstraintModel) { if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } + if (GetParam().solver_type == SolverType::kXpress) { + GTEST_SKIP() << "This test is disabled as Xpress only supports second " + "order cone constraints on singletons"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -169,6 +188,9 @@ TEST_P(SimpleSecondOrderConeTest, SolveModelWithSocAndLinearConstraints) { // The unique optimal solution is then (x*, y*) = (0.5, 0.5) with objective // value 1. TEST_P(IncrementalSecondOrderConeTest, LinearToSecondOrderConeUpdate) { + if (!GetParam().supports_incremental_add_and_deletes) { + GTEST_SKIP() << kNoIncrementalAddAndDeletes; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -232,6 +254,9 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesSecondOrderConeConstraint) { if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } + if (!GetParam().supports_incremental_add_and_deletes) { + GTEST_SKIP() << kNoIncrementalAddAndDeletes; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -319,6 +344,9 @@ TEST_P(IncrementalSecondOrderConeTest, if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } + if (!GetParam().supports_incremental_add_and_deletes) { + GTEST_SKIP() << kNoIncrementalAddAndDeletes; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 2.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -361,6 +389,9 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesVariableThatIsAnArgument) { if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } + if (!GetParam().supports_incremental_add_and_deletes) { + GTEST_SKIP() << kNoIncrementalAddAndDeletes; + } Model model; const Variable x = model.AddContinuousVariable(1.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -403,6 +434,9 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesVariableInAnArgument) { if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } + if (!GetParam().supports_incremental_add_and_deletes) { + GTEST_SKIP() << kNoIncrementalAddAndDeletes; + } Model model; const Variable x = model.AddContinuousVariable(1.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 2.0, "y"); diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 17144c0f925..224f40f278a 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -715,27 +715,84 @@ class ScopedSolverContext { } }; +/** Different modes for ExtractSingleton(). */ +typedef enum { + SingletonForSOS, /**< SOS constraint. */ + SingletonForSOCBound, /**< Second order cone constraint bound. */ + SingletonForSOCNorm /**< Second order cone constraint norm. */ +} SingletonType; + // ortools supports SOS constraints and second order cone constraints on // expressions. Xpress only supports these constructs on singleton variables. // We could create auxiliary variables here, set each of them equal to one of // the expressions and then formulate SOS/SOC on the auxiliary variables. // This however seems a bit of overkill at the moment, so we just error out // if elements are non-singleton. -absl::StatusOr ExtractSingleton( - LinearExpressionProto const& expr, char const* what) { - if (expr.offset() != 0.0) - return util::InvalidArgumentErrorBuilder() - << "Xpress only supports " << what - << " on singleton variables (offset)"; - if (expr.ids_size() != 1) - return util::InvalidArgumentErrorBuilder() - << "Xpress only supports " << what - << " on singleton variables (empty or multiple)"; - if (expr.coefficients(0) != 1.0) +// Returns the variable of the singleton as return value and its coefficient +// in *p_coef. +absl::StatusOr> ExtractSingleton( + LinearExpressionProto const& expr, SingletonType type, double* p_coef) { + double const constant = expr.offset(); + if (expr.ids_size() == 1 && constant == 0.0) { + // We have a single variable in the expression and no constant. + double const coef = expr.coefficients(0); + switch (type) { + case SingletonForSOS: + // A non-zero coefficient does not change anything, so is allowed. + if (coef == 0.0) { + return util::InvalidArgumentErrorBuilder() + << "Xpress does not support coefficient " << coef << " in SOS"; + } + break; + case SingletonForSOCBound: // fallthrough + case SingletonForSOCNorm: + // We are going to square the coefficient, so anything non-negative + // is allowed. + if (coef < 0) { + return util::InvalidArgumentErrorBuilder() + << "Xpress does not support coefficient " << coef + << " in a second order cone constraint " + << (type == SingletonForSOCBound ? "bound" : "norm"); + } + break; + } + if (p_coef) *p_coef = coef; + return std::optional(expr.ids(0)); + } else if (expr.ids_size() == 0) { + // The expression is constant. + switch (type) { + case SingletonForSOS: + // Any non-zero constant would force all other variables to 0. + // Any zero constant would be redundant. + // Both are edge cases that we do not support at the moment. + return util::InvalidArgumentErrorBuilder() + << "Xpress does not support constant expressions in SOS"; + case SingletonForSOCBound: + // We are going to square the bound, so it should not be negative. + if (constant < 0.0) { + return util::InvalidArgumentErrorBuilder() + << "Xpress does not support constant " << constant + << " in a second order cone constraint bound"; + } + break; + case SingletonForSOCNorm: + // Constant entries in the norm are not supported (we would have to + // move them to the right-hand side). + return util::InvalidArgumentErrorBuilder() + << "Xpress does not support constants in a second order cone " + "constraint norm"; + } + if (p_coef) *p_coef = constant; + return std::nullopt; + } else { + // Multiple coefficients + static char const* const name[] = {"SOS", + "second order cone constraint bound", + "second order cone constraint norm"}; return util::InvalidArgumentErrorBuilder() - << "Xpress only supports " << what - << " on singleton variables (non-unit)"; - return expr.ids(0); + << "Xpress does not support general linear expressions in " + << name[type]; + } } } // namespace @@ -745,7 +802,15 @@ constexpr SupportedProblemStructures kXpressSupportedStructures = { .multi_objectives = SupportType::kSupported, .quadratic_objectives = SupportType::kSupported, .quadratic_constraints = SupportType::kSupported, - .second_order_cone_constraints = SupportType::kNotSupported, + // Limitation: We only implemented support for constraints of type + // norm(x1,...,xn) <= x0 + // General linear expressions in the norm or in the bound are not + // supported at the moment. They must be emulated by the caller using + // auxiliary variables. + .second_order_cone_constraints = SupportType::kSupported, + // Limitation: We only implemented support for SOS constraints on singleton + // variables. General expressions in the SOS are not supported. They must + // be emulated by the caller using auxiliary variables. .sos1_constraints = SupportType::kSupported, .sos2_constraints = SupportType::kSupported, .indicator_constraints = SupportType::kSupported}; @@ -790,6 +855,8 @@ absl::Status XpressSolver::LoadModel(const ModelProto& input_model) { RETURN_IF_ERROR(AddSOS(input_model.sos2_constraints(), false)); RETURN_IF_ERROR(AddIndicators(input_model.indicator_constraints())); RETURN_IF_ERROR(AddQuadraticConstraints(input_model.quadratic_constraints())); + RETURN_IF_ERROR(AddSecondOrderConeConstraints( + input_model.second_order_cone_constraints())); return absl::OkStatus(); } absl::Status XpressSolver::AddNewVariables( @@ -1026,8 +1093,11 @@ absl::Status XpressSolver::AddSOS( for (decltype(count) i = 0; i < count; ++i) { auto const& expr = sos.expressions(i); double const weight = has_weight ? sos.weights(i) : (i + 1); - ASSIGN_OR_RETURN(VarId const x, ExtractSingleton(expr, "SOS")); - colind.push_back(variables_map_.at(x)); + // Note: A constant value in an SOS forces all others to zero. At the + // moment we do not support this. We consider this an edge case. + ASSIGN_OR_RETURN(std::optional x, + ExtractSingleton(expr, SingletonForSOS, nullptr)); + colind.push_back(variables_map_.at(x.value())); refval.push_back(weight); } gtl::InsertOrDie(sosmap, sosId, nextId); @@ -1168,7 +1238,7 @@ absl::Status XpressSolver::AddQuadraticConstraints( RETURN_IF_ERROR(xpress_->AddQRow(sense, rhs, rng, lin_colind, lin_coef, quad_col1, quad_col2, quad_coef)); LinearConstraintData& data = - gtl::InsertKeyOrDie(&indicator_map_, ortoolsId); + gtl::InsertKeyOrDie(&quad_constraints_map_, ortoolsId); data.constraint_index = next; data.lower_bound = quad.lower_bound(); data.upper_bound = quad.upper_bound(); @@ -1177,6 +1247,46 @@ absl::Status XpressSolver::AddQuadraticConstraints( return absl::OkStatus(); } +// Extract second order cone constraints. +// Note that we only support +// sum(i in I) x_i^2 <= x_0^2 +absl::Status XpressSolver::AddSecondOrderConeConstraints( + const google::protobuf::Map& constraints) { + std::vector cols; + std::vector coefs; + ASSIGN_OR_RETURN(int next, xpress_->GetIntAttr(XPRS_ORIGINALROWS)); + for (auto const& [ortoolsId, soc] : constraints) { + cols.clear(); + coefs.clear(); + double rhs = 0.0; + auto const& ub = soc.upper_bound(); + double coef; + ASSIGN_OR_RETURN(std::optional const x0, + ExtractSingleton(ub, SingletonForSOCBound, &coef)); + if (x0.has_value()) { + cols.push_back(variables_map_.at(x0.value())); + coefs.push_back(-coef * coef); + } else { + rhs = coef * coef; + } + + for (auto const& arg : soc.arguments_to_norm()) { + ASSIGN_OR_RETURN(std::optional const x, + ExtractSingleton(arg, SingletonForSOCNorm, &coef)); + cols.push_back(variables_map_.at(x.value())); + coefs.push_back(coef * coef); + } + RETURN_IF_ERROR(xpress_->AddQRow('L', rhs, 0.0, {}, {}, cols, cols, coefs)); + LinearConstraintData& data = gtl::InsertKeyOrDie(&soc_map_, ortoolsId); + data.constraint_index = next; + data.lower_bound = kMinusInf; + data.upper_bound = 0.0; + ++next; + } + return absl::OkStatus(); +} + absl::Status XpressSolver::ChangeCoefficients( const SparseDoubleMatrixProto& matrix) { const int num_coefficients = matrix.row_ids().size(); diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 3de3bea1aa6..b44554bd163 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -153,6 +153,9 @@ class XpressSolver : public SolverInterface { absl::Status AddQuadraticConstraints( const google::protobuf::Map& constraints); + absl::Status AddSecondOrderConeConstraints( + const google::protobuf::Map& constraints); absl::Status ChangeCoefficients(const SparseDoubleMatrixProto& matrix); absl::Status LoadModel(const ModelProto& input_model); @@ -190,10 +193,13 @@ class XpressSolver : public SolverInterface { // indicators. gtl::linked_hash_map indicator_map_; - // Internal correspondence from indicator proto IDs to Xpress-numbered - // indicators. + // Internal correspondence from quadratic proto IDs to Xpress-numbered + // rows. gtl::linked_hash_map quad_constraints_map_; + // Internal correspondence from second order cone constraint proto IDs to + // Xpress-numbered rows. + gtl::linked_hash_map soc_map_; int get_model_index(XpressVariableIndex index) const { return index; } int get_model_index(const LinearConstraintData& index) const { diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index a1d4dc56d19..83a6f43e7bf 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -284,7 +284,7 @@ GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(QcDualsTest); SecondOrderConeTestParameters GetXpressSecondOrderConeTestParameters() { return SecondOrderConeTestParameters( SolverType::kXpress, SolveParameters(), - /*supports_soc_constraints=*/false, + /*supports_soc_constraints=*/true, /*supports_incremental_add_and_deletes=*/false); } INSTANTIATE_TEST_SUITE_P( From 712a291165ae96a2facd78cef5dc81c97515676b Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Fri, 17 Oct 2025 10:22:39 +0200 Subject: [PATCH 55/70] Only query "additional" solution for LPs. --- ortools/math_opt/solvers/xpress_solver.cc | 12 ++++++++++++ ortools/math_opt/solvers/xpress_solver.h | 2 ++ ortools/third_party_solvers/xpress_environment.h | 1 + 3 files changed, 15 insertions(+) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 224f40f278a..4f1a06e1b98 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -1371,6 +1371,8 @@ absl::StatusOr XpressSolver::Solve( RETURN_IF_ERROR(xpress_->GetSolution(&primal_sol_avail_, nullptr, 0, -1)); RETURN_IF_ERROR(xpress_->GetDuals(&dual_sol_avail_, nullptr, 0, -1)); ASSIGN_OR_RETURN(algorithm_, xpress_->GetIntAttr(XPRS_ALGORITHM)); + ASSIGN_OR_RETURN(optimizetypeused_, + xpress_->GetIntAttr(XPRS_OPTIMIZETYPEUSED)); RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; ASSIGN_OR_RETURN( @@ -1471,7 +1473,17 @@ absl::Status XpressSolver::AppendSolution( std::vector duals(nCons); std::vector reducedCosts(nVars); + // This is for handling an edge case: + // If an LP solve is interrupted then XPRSgetsolution() and friends will + // return "not available". However, there may still be a current + // primal or dual feasible solution available - depending on the algorithm. + // Users and ortools tests may expect these to be returned in some cases, + // so we try to pick them up. This must be done via XPRSgetlpsol() which + // is designed for exactly this edge case. + // This only applies to LPs. auto hasSolution = + (optimizetypeused_ == + 0) && // 0 = LP, 1 = MIP, 2/3 = nonlin local/global xpress_ ->GetLpSol(absl::MakeSpan(primals), absl::MakeSpan(duals), absl::MakeSpan(reducedCosts)) diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index b44554bd163..e872c170e02 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -223,9 +223,11 @@ class XpressSolver : public SolverInterface { // Results of the last solve int primal_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; int dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; + // Information queried right after a solve and stored for solution reporting int solvestatus_ = XPRS_SOLVESTATUS_UNSTARTED; int solstatus_ = XPRS_SOLSTATUS_NOTFOUND; int algorithm_ = XPRS_ALG_DEFAULT; + int optimizetypeused_ = -1; }; } // namespace operations_research::math_opt diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index 87b233caa15..710457eeff8 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -128,6 +128,7 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); #define XPRS_ORIGINALMIPENTS 1191 #define XPRS_ORIGINALSETS 1194 #define XPRS_ORIGINALINDICATORS 1255 +#define XPRS_OPTIMIZETYPEUSED 1268 #define XPRS_OBJVAL 2118 #define XPRS_BARPRIMALOBJ 4001 #define XPRS_BARDUALOBJ 4002 From cdbbe6d7b2b06b86a2d2125fb88131df9288a9fb Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Fri, 17 Oct 2025 10:23:25 +0200 Subject: [PATCH 56/70] Rename functions. --- ortools/math_opt/solvers/xpress_solver.cc | 37 ++++++++++++----------- ortools/math_opt/solvers/xpress_solver.h | 18 ++++++----- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 4f1a06e1b98..eb58b40ae9d 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -895,8 +895,8 @@ absl::Status XpressSolver::AddNewVariables( XpressSolver::XpressSolver(std::unique_ptr g_xpress) : xpress_(std::move(g_xpress)) {} -void XpressSolver::ParseBounds(double lb, double ub, char& sense, double& rhs, - double& rng) { +void XpressSolver::ExtractBounds(double lb, double ub, char& sense, double& rhs, + double& rng) { sense = XPRS_EQUAL; rhs = 0.0; rng = 0.0; @@ -950,8 +950,8 @@ absl::Status XpressSolver::AddNewLinearConstraints( char sense = XPRS_EQUAL; double rhs = 0.0; double rng = 0.0; - ParseBounds(constraint_data.lower_bound, constraint_data.upper_bound, sense, - rhs, rng); + ExtractBounds(constraint_data.lower_bound, constraint_data.upper_bound, + sense, rhs, rng); constraint_sense.emplace_back(sense); constraint_rhs.emplace_back(rhs); constraint_rng.emplace_back(rng); @@ -1108,9 +1108,9 @@ absl::Status XpressSolver::AddSOS( return absl::OkStatus(); } -void XpressSolver::ParseLinear(SparseDoubleVectorProto const& expr, - std::vector& colind, - std::vector& coef) { +void XpressSolver::ExtractLinear(SparseDoubleVectorProto const& expr, + std::vector& colind, + std::vector& coef) { auto terms = expr.ids_size(); colind.reserve(colind.size() + terms); coef.reserve(coef.size() + terms); @@ -1121,12 +1121,12 @@ void XpressSolver::ParseLinear(SparseDoubleVectorProto const& expr, /** TODO: How do we handle constant terms in expressions? */ } -void XpressSolver::ParseQuadratic(QuadraticConstraintProto const& expr, - std::vector& lin_colind, - std::vector& lin_coef, - std::vector& quad_col1, - std::vector& quad_col2, - std::vector& quad_coef) { +void XpressSolver::ExtractQuadratic(QuadraticConstraintProto const& expr, + std::vector& lin_colind, + std::vector& lin_coef, + std::vector& quad_col1, + std::vector& quad_col2, + std::vector& quad_coef) { auto const& lin = expr.linear_terms(); auto linTerms = lin.ids_size(); lin_colind.reserve(lin_colind.size() + linTerms); @@ -1172,8 +1172,8 @@ absl::Status XpressSolver::AddIndicators( int next = 0; for (auto const& [ortoolsId, indicator] : indicators) { start[next] = colind.size(); - ParseBounds(indicator.lower_bound(), indicator.upper_bound(), sense[next], - rhs[next], rng[next]); + ExtractBounds(indicator.lower_bound(), indicator.upper_bound(), sense[next], + rhs[next], rng[next]); // ortools tests require us to raise an error on ranged indicator // constraints if (sense[next] == XPRS_RANGE) { @@ -1181,7 +1181,7 @@ absl::Status XpressSolver::AddIndicators( << "indicator constraint on ranged constraint"; } - ParseLinear(indicator.expression(), colind, rowcoef); + ExtractLinear(indicator.expression(), colind, rowcoef); i_rowind[next] = oldRows + next; if (indicator.has_indicator_id()) { @@ -1228,13 +1228,14 @@ absl::Status XpressSolver::AddQuadraticConstraints( char sense; double rhs; double rng; - ParseBounds(quad.lower_bound(), quad.upper_bound(), sense, rhs, rng); + ExtractBounds(quad.lower_bound(), quad.upper_bound(), sense, rhs, rng); lin_colind.clear(); lin_coef.clear(); quad_col1.clear(); quad_col2.clear(); quad_coef.clear(); - ParseQuadratic(quad, lin_colind, lin_coef, quad_col1, quad_col2, quad_coef); + ExtractQuadratic(quad, lin_colind, lin_coef, quad_col1, quad_col2, + quad_coef); RETURN_IF_ERROR(xpress_->AddQRow(sense, rhs, rng, lin_colind, lin_coef, quad_col1, quad_col2, quad_coef)); LinearConstraintData& data = diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index e872c170e02..4a2307b8f2a 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -126,14 +126,16 @@ class XpressSolver : public SolverInterface { bool isPrimalFeasible() const; bool isDualFeasible() const; - void ParseBounds(double lb, double ub, char& sense, double& rhs, double& rng); - void ParseLinear(SparseDoubleVectorProto const& expr, - std::vector& colind, std::vector& coef); - void ParseQuadratic(QuadraticConstraintProto const& expr, - std::vector& lin_colind, - std::vector& lin_coef, - std::vector& quad_col1, std::vector& quad_col2, - std::vector& quad_coef); + void ExtractBounds(double lb, double ub, char& sense, double& rhs, + double& rng); + void ExtractLinear(SparseDoubleVectorProto const& expr, + std::vector& colind, std::vector& coef); + void ExtractQuadratic(QuadraticConstraintProto const& expr, + std::vector& lin_colind, + std::vector& lin_coef, + std::vector& quad_col1, + std::vector& quad_col2, + std::vector& quad_coef); absl::StatusOr> GetBasisIfAvailable( const SolveParametersProto& parameters); From 40bf7585c2d20a0ccd96d2a88255216d17ad5171 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Fri, 17 Oct 2025 11:04:35 +0200 Subject: [PATCH 57/70] Use `absl::Span<>` rather than raw arrays. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 80 ++++++++++++++------- ortools/math_opt/solvers/xpress/g_xpress.h | 30 +++++--- ortools/math_opt/solvers/xpress_solver.cc | 29 ++++---- 3 files changed, 92 insertions(+), 47 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 6532288a612..6c89015db35 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -36,6 +36,11 @@ namespace operations_research::math_opt { namespace { bool checkInt32Overflow(const std::size_t value) { return value > INT32_MAX; } +template +/** Forward an optional span to the C API. */ +T* forwardSpan(std::optional> const& span) { + return span.has_value() ? span.value().data() : nullptr; +} } // namespace constexpr int kXpressOk = 0; @@ -422,32 +427,58 @@ absl::StatusOr Xpress::IsMIP() const { return ents != 0; /** TODO: Check for preintsol callback? */ } -absl::Status Xpress::GetDuals(int* p_status, double* duals, int first, - int last) { - return ToStatus(XPRSgetduals(xpress_model_, p_status, duals, first, last)); -} -absl::Status Xpress::GetSolution(int* p_status, double* x, int first, - int last) { - return ToStatus(XPRSgetsolution(xpress_model_, p_status, x, first, last)); +absl::Status Xpress::GetDuals(int* p_status, + std::optional> const& duals, + int first, int last) { + return ToStatus( + XPRSgetduals(xpress_model_, p_status, forwardSpan(duals), first, last)); } -absl::Status Xpress::GetRedCosts(int* p_status, double* dj, int first, - int last) { - return ToStatus(XPRSgetredcosts(xpress_model_, p_status, dj, first, last)); +absl::Status Xpress::GetSolution(int* p_status, + std::optional> const& x, + int first, int last) { + return ToStatus( + XPRSgetsolution(xpress_model_, p_status, forwardSpan(x), first, last)); } - -absl::Status Xpress::AddMIPSol(int len, double const* vals, int const* colind, - char const* name) { - return ToStatus(XPRSaddmipsol(xpress_model_, len, vals, colind, name)); +absl::Status Xpress::GetRedCosts(int* p_status, + std::optional> const& dj, + int first, int last) { + return ToStatus( + XPRSgetredcosts(xpress_model_, p_status, forwardSpan(dj), first, last)); +} + +absl::Status Xpress::AddMIPSol( + absl::Span vals, + std::optional> const& colind, char const* name) { + int len; + if (colind.has_value()) { + if (checkInt32Overflow(colind.value().size())) + return absl::InvalidArgumentError("more start values than columns"); + len = static_cast(colind.has_value()); + } else { + ASSIGN_OR_RETURN(len, GetIntAttr(XPRS_COLS)); + } + return ToStatus(XPRSaddmipsol(xpress_model_, len, vals.data(), + forwardSpan(colind), name)); } -absl::Status Xpress::LoadDelayedRows(int len, int const* rows) { - return ToStatus(XPRSloaddelayedrows(xpress_model_, len, rows)); +absl::Status Xpress::LoadDelayedRows(absl::Span rows) { + if (checkInt32Overflow(rows.size())) + return absl::InvalidArgumentError("more delayed rows than rows"); + return ToStatus(XPRSloaddelayedrows( + xpress_model_, static_cast(rows.size()), rows.data())); } -absl::Status Xpress::LoadDirs(int len, int const* cols, int const* prio, - char const* dir, double const* up, - double const* down) { - return ToStatus(XPRSloaddirs(xpress_model_, len, cols, prio, dir, up, down)); +absl::Status Xpress::LoadDirs( + absl::Span cols, + std::optional> const& prio, + std::optional> const& dir, + std::optional> const& up, + std::optional> const& down) { + if (checkInt32Overflow(cols.size())) + return absl::InvalidArgumentError("more directions than columns"); + return ToStatus(XPRSloaddirs(xpress_model_, static_cast(cols.size()), + cols.data(), forwardSpan(prio), forwardSpan(dir), + forwardSpan(up), forwardSpan(down))); } absl::Status Xpress::SetObjectiveIntControl(int obj, int control, int value) { @@ -458,15 +489,16 @@ absl::Status Xpress::SetObjectiveDoubleControl(int obj, int control, return ToStatus(XPRSsetobjdblcontrol(xpress_model_, obj, control, value)); } absl::StatusOr Xpress::AddObjective(double constant, int ncols, - int const* colind, - double const* objcoef, int priority, - double weight) { + absl::Span colind, + absl::Span objcoef, + int priority, double weight) { ASSIGN_OR_RETURN(int const objs, GetIntAttr(XPRS_OBJECTIVES)); if (objs == INT_MAX) { return util::StatusBuilder(absl::StatusCode::kInvalidArgument) << "too many objectives"; } - int ret = XPRSaddobj(xpress_model_, ncols, colind, objcoef, priority, weight); + int ret = XPRSaddobj(xpress_model_, ncols, colind.data(), objcoef.data(), + priority, weight); if (ret) { return ToStatus(ret); } diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 27e00cecdff..db74d38c11e 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -27,6 +27,7 @@ #include #include +#include #include #include @@ -136,20 +137,31 @@ class Xpress { absl::Status Interrupt(int reason); absl::StatusOr IsMIP() const; - absl::Status GetDuals(int* p_status, double* duals, int first, int last); - absl::Status GetSolution(int* p_status, double* x, int first, int last); - absl::Status GetRedCosts(int* p_status, double* dj, int first, int last); - - absl::Status AddMIPSol(int len, double const* vals, int const* colind, + absl::Status GetDuals(int* p_status, + std::optional> const& duals, + int first, int last); + absl::Status GetSolution(int* p_status, + std::optional> const& x, + int first, int last); + absl::Status GetRedCosts(int* p_status, + std::optional> const& dj, + int first, int last); + + absl::Status AddMIPSol(absl::Span vals, + std::optional> const& colind, char const* name = nullptr); - absl::Status LoadDelayedRows(int len, int const* rows); - absl::Status LoadDirs(int len, int const* cols, int const* prio, - char const* dir, double const* up, double const* down); + absl::Status LoadDelayedRows(absl::Span rows); + absl::Status LoadDirs(absl::Span cols, + std::optional> const& prio, + std::optional> const& dir, + std::optional> const& up, + std::optional> const& down); absl::Status SetObjectiveIntControl(int obj, int control, int value); absl::Status SetObjectiveDoubleControl(int obj, int control, double value); absl::StatusOr AddObjective(double constant, int ncols, - int const* colind, double const* objcoef, + absl::Span colind, + absl::Span objcoef, int priority, double weight); absl::StatusOr CalculateObjectiveN(int objidx, double const* solution); diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index eb58b40ae9d..12611a3640d 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -588,8 +588,7 @@ class ScopedSolverContext { return util::InvalidArgumentErrorBuilder() << "more solution hints than columns"; RETURN_IF_ERROR(shared_ctx.xpress->AddMIPSol( - static_cast(mipStart.size()), mipStart.data(), colind.data(), - absl::StrCat("SolutionHint", cnt).c_str())); + mipStart, colind, absl::StrCat("SolutionHint", cnt).c_str())); ++cnt; } } @@ -614,8 +613,8 @@ class ScopedSolverContext { return util::InvalidArgumentErrorBuilder() << "more branching priorities than columns"; RETURN_IF_ERROR(shared_ctx.xpress->LoadDirs( - static_cast(colind.size()), colind.data(), priority.data(), - nullptr, nullptr, nullptr)); + absl::MakeSpan(colind), absl::MakeSpan(priority), std::nullopt, + std::nullopt, std::nullopt)); } // Objective parameters: primary/single objective @@ -676,8 +675,7 @@ class ScopedSolverContext { return util::InvalidArgumentErrorBuilder() << "more lazy constraints than rows"; - RETURN_IF_ERROR(shared_ctx.xpress->LoadDelayedRows( - static_cast(delayedRows.size()), delayedRows.data())); + RETURN_IF_ERROR(shared_ctx.xpress->LoadDelayedRows(delayedRows)); } return absl::OkStatus(); @@ -1051,8 +1049,8 @@ absl::Status XpressSolver::AddObjective( ASSIGN_OR_RETURN( int const newid, xpress_->AddObjective( - objective.offset(), static_cast(index.size()), index.data(), - objective.linear_coefficients().values().data(), + objective.offset(), static_cast(index.size()), + absl::MakeSpan(index), objective.linear_coefficients().values(), // checked above static_cast(-objective.priority()), weight)); gtl::InsertOrDie(&objectives_map_, objective_id.value(), newid); @@ -1369,8 +1367,9 @@ absl::StatusOr XpressSolver::Solve( // (for example, BARALG to decide whether we need to report barrier or // first order iterations). solveContext.ReraiseException(); - RETURN_IF_ERROR(xpress_->GetSolution(&primal_sol_avail_, nullptr, 0, -1)); - RETURN_IF_ERROR(xpress_->GetDuals(&dual_sol_avail_, nullptr, 0, -1)); + RETURN_IF_ERROR( + xpress_->GetSolution(&primal_sol_avail_, std::nullopt, 0, -1)); + RETURN_IF_ERROR(xpress_->GetDuals(&dual_sol_avail_, std::nullopt, 0, -1)); ASSIGN_OR_RETURN(algorithm_, xpress_->GetIntAttr(XPRS_ALGORITHM)); ASSIGN_OR_RETURN(optimizetypeused_, xpress_->GetIntAttr(XPRS_OPTIMIZETYPEUSED)); @@ -1452,7 +1451,8 @@ absl::Status XpressSolver::AppendSolution( if (is_mip_) { std::vector x(nVars); int avail; - RETURN_IF_ERROR(xpress_->GetSolution(&avail, x.data(), 0, nVars - 1)); + RETURN_IF_ERROR( + xpress_->GetSolution(&avail, absl::MakeSpan(x), 0, nVars - 1)); if (avail != XPRS_SOLAVAILABLE_NOTFOUND) { SolutionProto solution{}; solution.mutable_primal_solution()->set_feasibility_status( @@ -1498,7 +1498,7 @@ absl::Status XpressSolver::AppendSolution( // The preferred methods for obtaining primal information are // XPRSgetsolution() and XPRSgetslacks() (not used here) RETURN_IF_ERROR( - xpress_->GetSolution(nullptr, primals.data(), 0, nVars - 1)); + xpress_->GetSolution(nullptr, absl::MakeSpan(primals), 0, nVars - 1)); solution.mutable_primal_solution()->set_feasibility_status( getPrimalSolutionStatus()); ASSIGN_OR_RETURN(const double primalBound, GetBestPrimalBound()); @@ -1525,9 +1525,10 @@ absl::Status XpressSolver::AppendSolution( if (isDualFeasible()) { // The preferred methods for obtain dual information are XPRSgetduals() // and XPRSgetredcosts(). - RETURN_IF_ERROR(xpress_->GetDuals(nullptr, duals.data(), 0, nCons - 1)); RETURN_IF_ERROR( - xpress_->GetRedCosts(nullptr, reducedCosts.data(), 0, nVars - 1)); + xpress_->GetDuals(nullptr, absl::MakeSpan(duals), 0, nCons - 1)); + RETURN_IF_ERROR(xpress_->GetRedCosts( + nullptr, absl::MakeSpan(reducedCosts), 0, nVars - 1)); solution.mutable_dual_solution()->set_feasibility_status( getDualSolutionStatus()); ASSIGN_OR_RETURN(const double dualBound, GetBestDualBound()); From 7841179af4b9ad1bad6347a0669c45d57b3d2cd1 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Mon, 20 Oct 2025 09:33:55 +0200 Subject: [PATCH 58/70] Cleanup and comments. --- ortools/math_opt/solvers/xpress/g_xpress.cc | 60 +++----- ortools/math_opt/solvers/xpress/g_xpress.h | 5 +- ortools/math_opt/solvers/xpress_solver.cc | 145 ++++++++++++------ ortools/math_opt/solvers/xpress_solver.h | 2 + .../third_party_solvers/xpress_environment.h | 1 + 5 files changed, 129 insertions(+), 84 deletions(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 6c89015db35..bb23ae91d4f 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -115,7 +115,7 @@ void Xpress::initIntControlDefaults() { } } -// All arguments can be empty to indicate "use default values". +// All span arguments can be missing to indicate "use default values". // Default objective value: 0 // Default lower bound: 0 // Default upper bound: infinity @@ -125,7 +125,7 @@ absl::Status Xpress::AddVars(std::size_t count, const absl::Span lb, const absl::Span ub, const absl::Span vtype) { - ASSIGN_OR_RETURN(int const oldCols, GetIntAttr(XPRS_COLS)); + ASSIGN_OR_RETURN(int const oldCols, GetIntAttr(XPRS_ORIGINALCOLS)); if (checkInt32Overflow(count) || checkInt32Overflow(std::size_t(oldCols) + std::size_t(count))) { return absl::InvalidArgumentError( @@ -359,18 +359,6 @@ absl::StatusOr Xpress::GetObjectiveDoubleAttr(int objidx, return result; } -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 Xpress::GetDualStatus() const { int status = 0; double values[1]; @@ -383,8 +371,10 @@ absl::StatusOr Xpress::GetDualStatus() const { absl::Status Xpress::GetBasis(std::vector& rowBasis, std::vector& colBasis) const { - rowBasis.resize(GetNumberOfConstraints()); - colBasis.resize(GetNumberOfVariables()); + ASSIGN_OR_RETURN(int const rows, GetIntAttr(XPRS_ORIGINALROWS)); + ASSIGN_OR_RETURN(int const cols, GetIntAttr(XPRS_ORIGINALCOLS)); + rowBasis.resize(rows); + colBasis.resize(cols); return ToStatus( XPRSgetbasis(xpress_model_, rowBasis.data(), colBasis.data())); } @@ -400,20 +390,20 @@ absl::Status Xpress::SetStartingBasis(std::vector& rowBasis, } absl::StatusOr> Xpress::GetVarLb() const { - int nVars = GetNumberOfVariables(); + ASSIGN_OR_RETURN(int const cols, GetIntAttr(XPRS_ORIGINALCOLS)); std::vector bounds; - bounds.reserve(nVars); + bounds.reserve(cols); RETURN_IF_ERROR( - ToStatus(XPRSgetlb(xpress_model_, bounds.data(), 0, nVars - 1))) + ToStatus(XPRSgetlb(xpress_model_, bounds.data(), 0, cols - 1))) << "Failed to retrieve variable LB from XPRESS"; return bounds; } absl::StatusOr> Xpress::GetVarUb() const { - int nVars = GetNumberOfVariables(); + ASSIGN_OR_RETURN(int const cols, GetIntAttr(XPRS_ORIGINALCOLS)); std::vector bounds; - bounds.reserve(nVars); + bounds.reserve(cols); RETURN_IF_ERROR( - ToStatus(XPRSgetub(xpress_model_, bounds.data(), 0, nVars - 1))) + ToStatus(XPRSgetub(xpress_model_, bounds.data(), 0, cols - 1))) << "Failed to retrieve variable UB from XPRESS"; return bounds; } @@ -446,19 +436,19 @@ absl::Status Xpress::GetRedCosts(int* p_status, XPRSgetredcosts(xpress_model_, p_status, forwardSpan(dj), first, last)); } -absl::Status Xpress::AddMIPSol( - absl::Span vals, - std::optional> const& colind, char const* name) { - int len; - if (colind.has_value()) { - if (checkInt32Overflow(colind.value().size())) - return absl::InvalidArgumentError("more start values than columns"); - len = static_cast(colind.has_value()); - } else { - ASSIGN_OR_RETURN(len, GetIntAttr(XPRS_COLS)); - } - return ToStatus(XPRSaddmipsol(xpress_model_, len, vals.data(), - forwardSpan(colind), name)); +/** Add a mip start that is specified in the original space, i.e., in terms of + * ortools variables. + */ +absl::Status Xpress::AddMIPSol(absl::Span vals, + absl::Span colind, char const* name) { + if (checkInt32Overflow(colind.size())) + return absl::InvalidArgumentError("more start values than columns"); + if (colind.size() != vals.size()) + return absl::InvalidArgumentError("inconsitent data to AddMIPSol()"); + // XPRSaddmipsol() supports colind=nullptr, but we do not support that here + // since we don't need it. + return ToStatus(XPRSaddmipsol(xpress_model_, static_cast(colind.size()), + vals.data(), colind.data(), name)); } absl::Status Xpress::LoadDelayedRows(absl::Span rows) { diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index db74d38c11e..364076c4868 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -128,9 +128,6 @@ class Xpress { absl::Status RemoveCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), void* cbdata = nullptr); - int GetNumberOfConstraints() const; - int GetNumberOfVariables() const; - absl::StatusOr> GetVarLb() const; absl::StatusOr> GetVarUb() const; @@ -148,7 +145,7 @@ class Xpress { int first, int last); absl::Status AddMIPSol(absl::Span vals, - std::optional> const& colind, + absl::Span colind, char const* name = nullptr); absl::Status LoadDelayedRows(absl::Span rows); absl::Status LoadDirs(absl::Span cols, diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 12611a3640d..169267ad28e 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -212,7 +212,7 @@ DEFINE_SCOPED_CB(Checktime, SolveInterrupter const*, int, // Note: we do NOT return non-zero from the callback if the solve was // interrupted. Returning non-zero from the callback is interpreted // as hitting a time limit and we would therefore not map correctly - // the resulting stop status to or tools' termination status. + // the resulting stop status to ortools' termination status. if (cb->or_tools_cb->IsInterrupted()) { cb->Interrupt(XPRS_STOP_USER); } @@ -281,9 +281,7 @@ class ScopedSolverContext { struct OneControl { enum { INT_CONTROL, DBL_CONTROL, STR_CONTROL } type; int id; - int64_t l; - double d; - std::string s; + std::variant value; }; /** Controls to be reset in the destructor. */ std::vector modifiedControls; @@ -297,19 +295,19 @@ class ScopedSolverContext { } absl::Status Set(int id, int64_t const& value) { ASSIGN_OR_RETURN(int64_t old, shared_ctx.xpress->GetIntControl64(id)); - modifiedControls.push_back({OneControl::INT_CONTROL, id, old, 0.0, ""}); + modifiedControls.push_back({OneControl::INT_CONTROL, id, old}); RETURN_IF_ERROR(shared_ctx.xpress->SetIntControl64(id, value)); return absl::OkStatus(); } absl::Status Set(int id, double const& value) { ASSIGN_OR_RETURN(double old, shared_ctx.xpress->GetDblControl(id)); - modifiedControls.push_back({OneControl::DBL_CONTROL, id, 0LL, old, ""}); + modifiedControls.push_back({OneControl::DBL_CONTROL, id, old}); RETURN_IF_ERROR(shared_ctx.xpress->SetDblControl(id, value)); return absl::OkStatus(); } absl::Status Set(int id, std::string const& value) { ASSIGN_OR_RETURN(std::string old, shared_ctx.xpress->GetStrControl(id)); - modifiedControls.push_back({OneControl::STR_CONTROL, id, 0LL, 0.0, old}); + modifiedControls.push_back({OneControl::STR_CONTROL, id, old}); RETURN_IF_ERROR(shared_ctx.xpress->SetStrControl(id, value)); return absl::OkStatus(); } @@ -345,11 +343,13 @@ class ScopedSolverContext { /** Setup model specific parameters. */ absl::Status ApplyParameters(const SolveParametersProto& parameters, MessageCallback message_callback, - std::string* export_model) { + std::string* export_model, + bool* force_postsolve) { std::vector warnings; ASSIGN_OR_RETURN(bool const isMIP, shared_ctx.xpress->IsMIP()); if (parameters.enable_output()) { - // This is considered only if no message callback is set. + // This is considered only if no message callback is set, see the + // ortools specification of the enable_output parameter. if (!message_callback) { RETURN_IF_ERROR( messageCallback.Add(&shared_ctx, stdoutMessageCallback)); @@ -381,7 +381,7 @@ class ScopedSolverContext { } if (parameters.has_objective_limit()) { // In Xpress you can apply MIPABSCUTOFF also to LPs. - // However, or tools applies both cutoff_limit and objective_limit + // However, ortools applies both cutoff_limit and objective_limit // to LPs and distinguishes the two, i.e., expect different return // values depending on what is set. Since we cannot easily make this // distinction, we do not support objective_limit. Users should just @@ -394,7 +394,7 @@ class ScopedSolverContext { if (parameters.has_solution_limit()) { RETURN_IF_ERROR(Set(XPRS_MAXMIPSOL, parameters.solution_limit())); } - if (parameters.has_threads() && parameters.threads() > 1) + if (parameters.has_threads() && parameters.threads() > 0) RETURN_IF_ERROR(Set(XPRS_THREADS, parameters.threads())); if (parameters.has_random_seed()) { RETURN_IF_ERROR(Set(XPRS_RANDOMSEED, parameters.random_seed())); @@ -473,6 +473,7 @@ class ScopedSolverContext { if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) { switch (parameters.heuristics()) { case EMPHASIS_OFF: + RETURN_IF_ERROR(Set(XPRS_HEUREMPHASIS, 0)); break; case EMPHASIS_UNSPECIFIED: break; @@ -491,13 +492,20 @@ class ScopedSolverContext { parameters.xpress().parameters()) { std::string const& name = parameter.name(); std::string const& value = parameter.value(); + int id, type; + int64_t l; + double d; + if (name == "EXPORT_MODEL") { if (export_model) *export_model = value; continue; + } else if (name == "FORCE_POSTSOLVE") { + if (!absl::SimpleAtoi(value, &l)) + return util::InvalidArgumentErrorBuilder() + << "value " << value << " for FORCE_POSTSOLVE" + << " is not an integer"; + if (force_postsolve) *force_postsolve = l != 0; } - int id, type; - int64_t l; - double d; RETURN_IF_ERROR( shared_ctx.xpress->GetControlInfo(name.c_str(), &id, &type)); switch (type) { @@ -546,10 +554,22 @@ class ScopedSolverContext { gtl::linked_hash_map const& objectives_map) { - ASSIGN_OR_RETURN(int const cols, shared_ctx.xpress->GetIntAttr(XPRS_COLS)); - ASSIGN_OR_RETURN(int const rows, shared_ctx.xpress->GetIntAttr(XPRS_ROWS)); + ASSIGN_OR_RETURN(int const cols, + shared_ctx.xpress->GetIntAttr(XPRS_ORIGINALCOLS)); + ASSIGN_OR_RETURN(int const rows, + shared_ctx.xpress->GetIntAttr(XPRS_ORIGINALROWS)); // Set initial basis if (model_parameters.has_initial_basis()) { + // XPRSloadbasis() will raise an error if called on a model in presolved + // state. We still trap this already here so that we can produce a more + // meaningful error message. + ASSIGN_OR_RETURN(int const state, + shared_ctx.xpress->GetIntAttr(XPRS_PRESOLVESTATE)); + if (state & ((1 << 1) | (1 << 2))) { + return util::InvalidArgumentErrorBuilder() + << "cannot set basis for model in presolved space (consider " + "FORCE_POSTSOLVE?)"; + } auto const& basis = model_parameters.initial_basis(); std::vector xpress_var_basis_status(cols); for (const auto [id, value] : MakeView(basis.variable_status())) { @@ -569,7 +589,7 @@ class ScopedSolverContext { } std::vector colind; - // Install solution hints. Xpress does not explicitly have solutions + // Install solution hints. Xpress does not explicitly have solution // hints but it supports partial MIP starts. So we just add each solution // hint as MIP start. if (model_parameters.solution_hints_size() > 0) { @@ -587,6 +607,7 @@ class ScopedSolverContext { if (mipStart.size() > cols) return util::InvalidArgumentErrorBuilder() << "more solution hints than columns"; + // XPRSaddmipsol() expects a solution in the original space RETURN_IF_ERROR(shared_ctx.xpress->AddMIPSol( mipStart, colind, absl::StrCat("SolutionHint", cnt).c_str())); ++cnt; @@ -594,8 +615,6 @@ class ScopedSolverContext { } // Install branching priorities. - // Note that in ortools higher priority takes precedence while in Xpress - // lower priority takes precedence. if (model_parameters.has_branching_priorities()) { auto const& prios = model_parameters.branching_priorities(); colind.clear(); @@ -604,9 +623,14 @@ class ScopedSolverContext { priority.reserve(prios.ids_size()); for (const auto [id, prio] : MakeView(prios)) { colind.push_back(variables_map.at(id)); - /** TODO: Xpress prios must be in [0,1000]. */ + // Xpress only allows priorities in [0,1000]. + // In ortools higher priority takes precedence while in Xpress + // lower priority takes precedence. + if (prio < 0 || prio > 1000) + return util::InvalidArgumentErrorBuilder() + << "Xpress only allows branching priorities in [0,1000]"; priority.push_back( - -prio); // Smaller ids have higher precedence in Xpress! + 1000 - prio); // Smaller prios have higher precedence in Xpress! } if (colind.size() > 0) @@ -696,13 +720,16 @@ class ScopedSolverContext { ++it) { switch (it->type) { case OneControl::INT_CONTROL: - CHECK_OK(shared_ctx.xpress->SetIntControl64(it->id, it->l)); + CHECK_OK(shared_ctx.xpress->SetIntControl64( + it->id, std::get(it->value))); break; case OneControl::DBL_CONTROL: - CHECK_OK(shared_ctx.xpress->SetDblControl(it->id, it->d)); + CHECK_OK(shared_ctx.xpress->SetDblControl( + it->id, std::get(it->value))); break; case OneControl::STR_CONTROL: - CHECK_OK(shared_ctx.xpress->SetStrControl(it->id, it->s.c_str())); + CHECK_OK(shared_ctx.xpress->SetStrControl( + it->id, std::get(it->value).c_str())); break; } } @@ -739,7 +766,8 @@ absl::StatusOr> ExtractSingleton( // A non-zero coefficient does not change anything, so is allowed. if (coef == 0.0) { return util::InvalidArgumentErrorBuilder() - << "Xpress does not support coefficient " << coef << " in SOS"; + << "Xpress does not support coefficient " << coef + << " in SOS (consider using auxiliary variables?)"; } break; case SingletonForSOCBound: // fallthrough @@ -750,7 +778,8 @@ absl::StatusOr> ExtractSingleton( return util::InvalidArgumentErrorBuilder() << "Xpress does not support coefficient " << coef << " in a second order cone constraint " - << (type == SingletonForSOCBound ? "bound" : "norm"); + << (type == SingletonForSOCBound ? "bound" : "norm") + << " (consider using auxiliary variables?)"; } break; } @@ -764,13 +793,15 @@ absl::StatusOr> ExtractSingleton( // Any zero constant would be redundant. // Both are edge cases that we do not support at the moment. return util::InvalidArgumentErrorBuilder() - << "Xpress does not support constant expressions in SOS"; + << "Xpress does not support constant expressions in SOS " + "(consider using auxiliary variables?)"; case SingletonForSOCBound: // We are going to square the bound, so it should not be negative. if (constant < 0.0) { return util::InvalidArgumentErrorBuilder() << "Xpress does not support constant " << constant - << " in a second order cone constraint bound"; + << " in a second order cone constraint bound (consider using " + "auxiliary variables?)"; } break; case SingletonForSOCNorm: @@ -778,7 +809,7 @@ absl::StatusOr> ExtractSingleton( // move them to the right-hand side). return util::InvalidArgumentErrorBuilder() << "Xpress does not support constants in a second order cone " - "constraint norm"; + "constraint norm (consider using auxiliary variables?)"; } if (p_coef) *p_coef = constant; return std::nullopt; @@ -789,7 +820,7 @@ absl::StatusOr> ExtractSingleton( "second order cone constraint norm"}; return util::InvalidArgumentErrorBuilder() << "Xpress does not support general linear expressions in " - << name[type]; + << name[type] << " (consider using auxiliary variables?)"; } } @@ -801,10 +832,10 @@ constexpr SupportedProblemStructures kXpressSupportedStructures = { .quadratic_objectives = SupportType::kSupported, .quadratic_constraints = SupportType::kSupported, // Limitation: We only implemented support for constraints of type - // norm(x1,...,xn) <= x0 + // norm(a1*x1,...,an*xn) <= a0*x0 // General linear expressions in the norm or in the bound are not // supported at the moment. They must be emulated by the caller using - // auxiliary variables. + // auxiliary variables. The right-hand side may be a constant. .second_order_cone_constraints = SupportType::kSupported, // Limitation: We only implemented support for SOS constraints on singleton // variables. General expressions in the SOS are not supported. They must @@ -839,6 +870,8 @@ absl::Status XpressSolver::LoadModel(const ModelProto& input_model) { RETURN_IF_ERROR(AddObjective(input_model.objective(), std::nullopt, !input_model.auxiliary_objectives().empty())); // Tests expect an error on duplicate priorities, so raise one. + // Xpress would otherwise merge objectives with the same objective when it + // starts solving. absl::flat_hash_set prios = { input_model.objective().priority()}; for (auto const& [id, obj] : input_model.auxiliary_objectives()) { @@ -861,7 +894,8 @@ absl::Status XpressSolver::AddNewVariables( const VariablesProto& new_variables) { const int num_new_variables = new_variables.lower_bounds().size(); std::vector variable_type(num_new_variables); - int n_variables = xpress_->GetNumberOfVariables(); + ASSIGN_OR_RETURN(int const n_variables, + xpress_->GetIntAttr(XPRS_ORIGINALCOLS)); bool have_integers = false; for (int j = 0; j < num_new_variables; ++j) { const VarId id = new_variables.ids(j); @@ -937,7 +971,7 @@ absl::Status XpressSolver::AddNewLinearConstraints( constraint_rhs.reserve(num_new_constraints); std::vector constraint_rng; constraint_rng.reserve(num_new_constraints); - int n_constraints = xpress_->GetNumberOfConstraints(); + ASSIGN_OR_RETURN(int n_constraints, xpress_->GetIntAttr(XPRS_ORIGINALROWS)); for (int i = 0; i < num_new_constraints; ++i) { const int64_t id = constraints.ids(i); LinearConstraintData& constraint_data = @@ -968,8 +1002,8 @@ absl::Status XpressSolver::AddObjective( // In ortools smaller priority means more important, in Xpress, // higher priority means more important, so we must invert priorities. // Moreover, in Xpress priorities are 32bit. - // In ortools it seems unspecified what happens to objectives with the - // same priority, in Xpress these are merged. + // Note that ortools does not allow duplicate priorities, this is checked + // by the caller. if (objective.priority() <= INT_MIN || objective.priority() > INT_MAX) { return util::InvalidArgumentErrorBuilder() << "Xpress only supports 32bit signed integers as objective " @@ -1109,6 +1143,9 @@ absl::Status XpressSolver::AddSOS( void XpressSolver::ExtractLinear(SparseDoubleVectorProto const& expr, std::vector& colind, std::vector& coef) { + // Note: Constant terms in expressions are already mixed into the + // right-hand side by ortools, so we don't have to deal with them + // here. auto terms = expr.ids_size(); colind.reserve(colind.size() + terms); coef.reserve(coef.size() + terms); @@ -1116,7 +1153,6 @@ void XpressSolver::ExtractLinear(SparseDoubleVectorProto const& expr, colind.push_back(variables_map_.at(expr.ids(i))); coef.push_back(expr.values(i)); } - /** TODO: How do we handle constant terms in expressions? */ } void XpressSolver::ExtractQuadratic(QuadraticConstraintProto const& expr, @@ -1125,6 +1161,9 @@ void XpressSolver::ExtractQuadratic(QuadraticConstraintProto const& expr, std::vector& quad_col1, std::vector& quad_col2, std::vector& quad_coef) { + // Note: Constant terms in expressions are already mixed into the + // right-hand side by ortools, so we don't have to deal with them + // here. auto const& lin = expr.linear_terms(); auto linTerms = lin.ids_size(); lin_colind.reserve(lin_colind.size() + linTerms); @@ -1307,6 +1346,7 @@ absl::StatusOr XpressSolver::Solve( MessageCallback message_callback, const CallbackRegistrationProto& callback_registration, Callback, const SolveInterrupter* interrupter) { + force_postsolve_ = false; primal_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; solvestatus_ = XPRS_SOLVESTATUS_UNSTARTED; @@ -1338,8 +1378,8 @@ absl::StatusOr XpressSolver::Solve( ScopedSolverContext solveContext(xpress_.get()); RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); std::string export_model = ""; - RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback, - &export_model)); + RETURN_IF_ERROR(solveContext.ApplyParameters( + parameters, message_callback, &export_model, &force_postsolve_)); RETURN_IF_ERROR(solveContext.ApplyModelParameters( model_parameters, variables_map_, linear_constraints_map_, objectives_map_)); @@ -1373,7 +1413,14 @@ absl::StatusOr XpressSolver::Solve( ASSIGN_OR_RETURN(algorithm_, xpress_->GetIntAttr(XPRS_ALGORITHM)); ASSIGN_OR_RETURN(optimizetypeused_, xpress_->GetIntAttr(XPRS_OPTIMIZETYPEUSED)); - RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; + // Do NOT postsolve by default here! + // All functions we use operate in the original space + // and postsolving here is harmful if we want to come back and solve with + // an extended time limit, for example. We defer postsolve until the latest + // point possible. This means we call it in ::Update() and + // ::ComputeInfeasibleSubsystem() + if (force_postsolve_) + RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; ASSIGN_OR_RETURN( SolveResultProto solve_result, @@ -1447,7 +1494,7 @@ absl::Status XpressSolver::AppendSolution( SolveResultProto& solve_result, const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters) { - int const nVars = xpress_->GetNumberOfVariables(); + ASSIGN_OR_RETURN(int const nVars, xpress_->GetIntAttr(XPRS_ORIGINALCOLS)); if (is_mip_) { std::vector x(nVars); int avail; @@ -1469,7 +1516,7 @@ absl::Status XpressSolver::AppendSolution( } } else { // Fetch all results from XPRESS - int const nCons = xpress_->GetNumberOfConstraints(); + ASSIGN_OR_RETURN(int const nCons, xpress_->GetIntAttr(XPRS_ORIGINALROWS)); std::vector primals(nVars); std::vector duals(nCons); std::vector reducedCosts(nVars); @@ -1486,6 +1533,7 @@ absl::Status XpressSolver::AppendSolution( (optimizetypeused_ == 0) && // 0 = LP, 1 = MIP, 2/3 = nonlin local/global xpress_ + // Note: XPRSgetlpsol() returns solution in original space ->GetLpSol(absl::MakeSpan(primals), absl::MakeSpan(duals), absl::MakeSpan(reducedCosts)) .ok(); @@ -1497,6 +1545,7 @@ absl::Status XpressSolver::AppendSolution( if (isPrimalFeasible()) { // The preferred methods for obtaining primal information are // XPRSgetsolution() and XPRSgetslacks() (not used here) + // XPRSgetsolution() returns solution in original space. RETURN_IF_ERROR( xpress_->GetSolution(nullptr, absl::MakeSpan(primals), 0, nVars - 1)); solution.mutable_primal_solution()->set_feasibility_status( @@ -1525,6 +1574,8 @@ absl::Status XpressSolver::AppendSolution( if (isDualFeasible()) { // The preferred methods for obtain dual information are XPRSgetduals() // and XPRSgetredcosts(). + // XPRSgetduals() and XPRSgetredcosts() both return values in the + // original space. RETURN_IF_ERROR( xpress_->GetDuals(nullptr, absl::MakeSpan(duals), 0, nCons - 1)); RETURN_IF_ERROR(xpress_->GetRedCosts( @@ -1617,6 +1668,7 @@ absl::StatusOr> XpressSolver::GetBasisIfAvailable( const SolveParametersProto&) { std::vector xprs_variable_basis_status; std::vector xprs_constraint_basis_status; + // XPRSgetbasis() always returns values in the original space if (!xpress_ ->GetBasis(xprs_constraint_basis_status, xprs_variable_basis_status) .ok()) { @@ -1871,6 +1923,8 @@ absl::StatusOr XpressSolver::ConvertTerminationReason( absl::StatusOr XpressSolver::Update(const ModelUpdateProto&) { // Not implemented yet + // We can only update if problem is not in presolved state. + RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; return false; } @@ -1878,13 +1932,14 @@ absl::StatusOr XpressSolver::ComputeInfeasibleSubsystem(const SolveParametersProto& parameters, MessageCallback message_callback, const SolveInterrupter* interrupter) { + RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; ScopedSolverContext solveContext(xpress_.get()); RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter)); - RETURN_IF_ERROR( - solveContext.ApplyParameters(parameters, message_callback, nullptr)); + RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback, + nullptr, nullptr)); return absl::UnimplementedError( - "XPRESS does not provide a method to compute an infeasible subsystem"); + "XpressSolver does not compute an infeasible subsystem (yet)"); } absl::StatusOr XpressSolver::ListInvertedBounds() const { diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 4a2307b8f2a..a223b4df88d 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -211,6 +211,8 @@ class XpressSolver : public SolverInterface { SolutionStatusProto getDualSolutionStatus() const; absl::StatusOr ListInvertedBounds() const; + /** Whether to force an XPRSpostsolve() after solving. */ + bool force_postsolve_ = false; /** Whether the model has a non-binary indicator variable. * The behavior expected by ortools is that * - we can happily create a model with non-binary indicators diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index 710457eeff8..d93c2bebbbc 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -117,6 +117,7 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); #define XPRS_MINUSINFINITY -1.0e+20 #define XPRS_MAXBANNERLENGTH 512 #define XPVERSION 45 // >= 45 for XPRS_SOLAVAILABLE flags, XPRSgetduals(), etc. +#define XPRS_PRESOLVESTATE 1026 #define XPRS_MIPENTS 1032 #define XPRS_ALGORITHM 1049 #define XPRS_STOPSTATUS 1179 From e35ff0b865416132827fb4167727e6dc1c0a0436 Mon Sep 17 00:00:00 2001 From: Daniel Junglas <50817618+djunglas@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:20:49 +0200 Subject: [PATCH 59/70] Pass small types by value Co-authored-by: Francesco Cavaliere --- ortools/math_opt/solvers/xpress_solver.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 169267ad28e..2d7077fc3b0 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -290,7 +290,7 @@ class ScopedSolverContext { ScopedSolverContext(Xpress* xpress) : removeInterrupterCallback(nullptr) { shared_ctx.xpress = xpress; } - absl::Status Set(int id, int32_t const& value) { + absl::Status Set(int id, int32_t value) { return Set(id, int64_t(value)); } absl::Status Set(int id, int64_t const& value) { From a306b21666a9d0ea563d366b89aea42f2730aab2 Mon Sep 17 00:00:00 2001 From: Daniel Junglas <50817618+djunglas@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:21:19 +0200 Subject: [PATCH 60/70] Pass small types by value Co-authored-by: Francesco Cavaliere --- ortools/math_opt/solvers/xpress_solver.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 2d7077fc3b0..e18b3aff417 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -293,7 +293,7 @@ class ScopedSolverContext { absl::Status Set(int id, int32_t value) { return Set(id, int64_t(value)); } - absl::Status Set(int id, int64_t const& value) { + absl::Status Set(int id, int64_t value) { ASSIGN_OR_RETURN(int64_t old, shared_ctx.xpress->GetIntControl64(id)); modifiedControls.push_back({OneControl::INT_CONTROL, id, old}); RETURN_IF_ERROR(shared_ctx.xpress->SetIntControl64(id, value)); From 3b71120f4d2100bc6f708d3bde5fb0e35f4251dd Mon Sep 17 00:00:00 2001 From: Daniel Junglas <50817618+djunglas@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:21:46 +0200 Subject: [PATCH 61/70] Pass small types by value Co-authored-by: Francesco Cavaliere --- ortools/math_opt/solvers/xpress_solver.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index e18b3aff417..9a578608a3d 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -299,7 +299,7 @@ class ScopedSolverContext { RETURN_IF_ERROR(shared_ctx.xpress->SetIntControl64(id, value)); return absl::OkStatus(); } - absl::Status Set(int id, double const& value) { + absl::Status Set(int id, double value) { ASSIGN_OR_RETURN(double old, shared_ctx.xpress->GetDblControl(id)); modifiedControls.push_back({OneControl::DBL_CONTROL, id, old}); RETURN_IF_ERROR(shared_ctx.xpress->SetDblControl(id, value)); From ea4e1721cd65812d003a3236750e95b64dbebc9f Mon Sep 17 00:00:00 2001 From: Daniel Junglas <50817618+djunglas@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:22:25 +0200 Subject: [PATCH 62/70] Always initialize value Co-authored-by: Francesco Cavaliere --- ortools/math_opt/solvers/xpress/g_xpress.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index bb23ae91d4f..53e436b45cf 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -321,7 +321,7 @@ absl::Status Xpress::SetDblControl(int control, double value) { } absl::StatusOr Xpress::GetStrControl(int control) const { - int nbytes; + int nbytes = 0; RETURN_IF_ERROR( ToStatus(XPRSgetstringcontrol(xpress_model_, control, NULL, 0, &nbytes))); std::vector result(nbytes, From 7353184a828429d3ffa346625ca831426a309589 Mon Sep 17 00:00:00 2001 From: Daniel Junglas <50817618+djunglas@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:23:50 +0200 Subject: [PATCH 63/70] Always initialize Co-authored-by: Francesco Cavaliere --- ortools/math_opt/solvers/xpress/g_xpress.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 53e436b45cf..1793cdcedc0 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -352,7 +352,7 @@ absl::StatusOr Xpress::GetDoubleAttr(int attribute) const { absl::StatusOr Xpress::GetObjectiveDoubleAttr(int objidx, int attribute) const { - double result; + double result = 0.0; RETURN_IF_ERROR( ToStatus(XPRSgetobjdblattrib(xpress_model_, objidx, attribute, &result))) << "Error getting Xpress objective double attribute: " << attribute; From 8b0618f98429a2b7593547aff739b509af9378ad Mon Sep 17 00:00:00 2001 From: Daniel Junglas <50817618+djunglas@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:24:06 +0200 Subject: [PATCH 64/70] Always initialize Co-authored-by: Francesco Cavaliere --- ortools/math_opt/solvers/xpress/g_xpress.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 1793cdcedc0..fc4c7a614a7 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -505,7 +505,7 @@ absl::StatusOr Xpress::AddObjective(double constant, int ncols, absl::StatusOr Xpress::CalculateObjectiveN(int objidx, double const* solution) { - double objval; + double objval = 0.0; int ret = XPRScalcobjn(xpress_model_, objidx, solution, &objval); if (ret) { return ToStatus(ret); From 8398eda07418ad44d96270615bc7b20506ce671f Mon Sep 17 00:00:00 2001 From: Daniel Junglas <50817618+djunglas@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:24:22 +0200 Subject: [PATCH 65/70] Always initialize Co-authored-by: Francesco Cavaliere --- ortools/math_opt/solvers/xpress/g_xpress.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index fc4c7a614a7..2716c6392ff 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -568,7 +568,7 @@ absl::Status Xpress::AddRows(absl::Span rowtype, } absl::StatusOr Xpress::IsBinary(int colidx) { - char ctype; + char ctype = '\0'; RETURN_IF_ERROR( ToStatus(XPRSgetcoltype(xpress_model_, &ctype, colidx, colidx))); if (ctype == XPRS_BINARY) From abef7c6f62f7c8485eaffbc7e423baae0f4e8483 Mon Sep 17 00:00:00 2001 From: Daniel Junglas <50817618+djunglas@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:24:41 +0200 Subject: [PATCH 66/70] Always initialize Co-authored-by: Francesco Cavaliere --- ortools/math_opt/solvers/xpress/g_xpress.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 2716c6392ff..659647e8f3e 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -575,7 +575,7 @@ absl::StatusOr Xpress::IsBinary(int colidx) { return true; else if (ctype != XPRS_INTEGER) return false; - double bnd; + double bnd = 0.0; RETURN_IF_ERROR(ToStatus(XPRSgetlb(xpress_model_, &bnd, colidx, colidx))); if (bnd < 0.0 || bnd > 1.0) return false; RETURN_IF_ERROR(ToStatus(XPRSgetub(xpress_model_, &bnd, colidx, colidx))); From 20bf0ae3c1d095341d350eaeef05767c43af6bf1 Mon Sep 17 00:00:00 2001 From: Daniel Junglas <50817618+djunglas@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:25:12 +0200 Subject: [PATCH 67/70] Use newline character instead of `std::endl` to avoid excessive flushing Co-authored-by: Francesco Cavaliere --- ortools/math_opt/solvers/xpress_solver.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 9a578608a3d..cf770af52d2 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -221,7 +221,7 @@ DEFINE_SCOPED_CB(Checktime, SolveInterrupter const*, int, /** An ortools message callback that prints everything to stdout. */ static void stdoutMessageCallback(std::vector const& lines) { - for (auto& l : lines) std::cout << l << std::endl; + for (auto& l : lines) std::cout << l << '\n'; } inline BasisStatusProto XpressToMathOptBasisStatus(const int status, From 3c9d27fe1fa58fca1e560744e7240330102aa59a Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 21 Oct 2025 14:32:56 +0200 Subject: [PATCH 68/70] Get rid of redundant field. --- ortools/math_opt/solvers/xpress_solver.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index cf770af52d2..0934b37de76 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -279,9 +279,9 @@ class ScopedSolverContext { std::function removeInterrupterCallback; /** A single control that must be reset in the destructor. */ struct OneControl { - enum { INT_CONTROL, DBL_CONTROL, STR_CONTROL } type; int id; std::variant value; + enum { INT_CONTROL, DBL_CONTROL, STR_CONTROL }; // Matches std::variant<>::index; }; /** Controls to be reset in the destructor. */ std::vector modifiedControls; @@ -295,19 +295,19 @@ class ScopedSolverContext { } absl::Status Set(int id, int64_t value) { ASSIGN_OR_RETURN(int64_t old, shared_ctx.xpress->GetIntControl64(id)); - modifiedControls.push_back({OneControl::INT_CONTROL, id, old}); + modifiedControls.push_back({id, old}); RETURN_IF_ERROR(shared_ctx.xpress->SetIntControl64(id, value)); return absl::OkStatus(); } absl::Status Set(int id, double value) { ASSIGN_OR_RETURN(double old, shared_ctx.xpress->GetDblControl(id)); - modifiedControls.push_back({OneControl::DBL_CONTROL, id, old}); + modifiedControls.push_back({id, old}); RETURN_IF_ERROR(shared_ctx.xpress->SetDblControl(id, value)); return absl::OkStatus(); } absl::Status Set(int id, std::string const& value) { ASSIGN_OR_RETURN(std::string old, shared_ctx.xpress->GetStrControl(id)); - modifiedControls.push_back({OneControl::STR_CONTROL, id, old}); + modifiedControls.push_back({id, old}); RETURN_IF_ERROR(shared_ctx.xpress->SetStrControl(id, value)); return absl::OkStatus(); } @@ -718,7 +718,7 @@ class ScopedSolverContext { ~ScopedSolverContext() { for (auto it = modifiedControls.rbegin(); it != modifiedControls.rend(); ++it) { - switch (it->type) { + switch (it->value.index()) { case OneControl::INT_CONTROL: CHECK_OK(shared_ctx.xpress->SetIntControl64( it->id, std::get(it->value))); From 62f2a865ccde299e8ad6d18ac280d0a79bbfd9cc Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 21 Oct 2025 14:38:35 +0200 Subject: [PATCH 69/70] Use enum class rather than plain enum. --- ortools/math_opt/solvers/xpress_solver.cc | 43 ++++++++++++----------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 0934b37de76..5a292557538 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -281,7 +281,11 @@ class ScopedSolverContext { struct OneControl { int id; std::variant value; - enum { INT_CONTROL, DBL_CONTROL, STR_CONTROL }; // Matches std::variant<>::index; + enum { + INT_CONTROL, + DBL_CONTROL, + STR_CONTROL + }; // Matches std::variant<>::index; }; /** Controls to be reset in the destructor. */ std::vector modifiedControls; @@ -290,9 +294,7 @@ class ScopedSolverContext { ScopedSolverContext(Xpress* xpress) : removeInterrupterCallback(nullptr) { shared_ctx.xpress = xpress; } - absl::Status Set(int id, int32_t value) { - return Set(id, int64_t(value)); - } + absl::Status Set(int id, int32_t value) { return Set(id, int64_t(value)); } absl::Status Set(int id, int64_t value) { ASSIGN_OR_RETURN(int64_t old, shared_ctx.xpress->GetIntControl64(id)); modifiedControls.push_back({id, old}); @@ -741,11 +743,11 @@ class ScopedSolverContext { }; /** Different modes for ExtractSingleton(). */ -typedef enum { - SingletonForSOS, /**< SOS constraint. */ - SingletonForSOCBound, /**< Second order cone constraint bound. */ - SingletonForSOCNorm /**< Second order cone constraint norm. */ -} SingletonType; +enum class SingletonType { + SOS, /**< SOS constraint. */ + SOCBound, /**< Second order cone constraint bound. */ + SOCNorm /**< Second order cone constraint norm. */ +}; // ortools supports SOS constraints and second order cone constraints on // expressions. Xpress only supports these constructs on singleton variables. @@ -762,7 +764,7 @@ absl::StatusOr> ExtractSingleton( // We have a single variable in the expression and no constant. double const coef = expr.coefficients(0); switch (type) { - case SingletonForSOS: + case SingletonType::SOS: // A non-zero coefficient does not change anything, so is allowed. if (coef == 0.0) { return util::InvalidArgumentErrorBuilder() @@ -770,15 +772,15 @@ absl::StatusOr> ExtractSingleton( << " in SOS (consider using auxiliary variables?)"; } break; - case SingletonForSOCBound: // fallthrough - case SingletonForSOCNorm: + case SingletonType::SOCBound: // fallthrough + case SingletonType::SOCNorm: // We are going to square the coefficient, so anything non-negative // is allowed. if (coef < 0) { return util::InvalidArgumentErrorBuilder() << "Xpress does not support coefficient " << coef << " in a second order cone constraint " - << (type == SingletonForSOCBound ? "bound" : "norm") + << (type == SingletonType::SOCBound ? "bound" : "norm") << " (consider using auxiliary variables?)"; } break; @@ -788,14 +790,14 @@ absl::StatusOr> ExtractSingleton( } else if (expr.ids_size() == 0) { // The expression is constant. switch (type) { - case SingletonForSOS: + case SingletonType::SOS: // Any non-zero constant would force all other variables to 0. // Any zero constant would be redundant. // Both are edge cases that we do not support at the moment. return util::InvalidArgumentErrorBuilder() << "Xpress does not support constant expressions in SOS " "(consider using auxiliary variables?)"; - case SingletonForSOCBound: + case SingletonType::SOCBound: // We are going to square the bound, so it should not be negative. if (constant < 0.0) { return util::InvalidArgumentErrorBuilder() @@ -804,7 +806,7 @@ absl::StatusOr> ExtractSingleton( "auxiliary variables?)"; } break; - case SingletonForSOCNorm: + case SingletonType::SOCNorm: // Constant entries in the norm are not supported (we would have to // move them to the right-hand side). return util::InvalidArgumentErrorBuilder() @@ -820,7 +822,8 @@ absl::StatusOr> ExtractSingleton( "second order cone constraint norm"}; return util::InvalidArgumentErrorBuilder() << "Xpress does not support general linear expressions in " - << name[type] << " (consider using auxiliary variables?)"; + << name[static_cast(type)] + << " (consider using auxiliary variables?)"; } } @@ -1128,7 +1131,7 @@ absl::Status XpressSolver::AddSOS( // Note: A constant value in an SOS forces all others to zero. At the // moment we do not support this. We consider this an edge case. ASSIGN_OR_RETURN(std::optional x, - ExtractSingleton(expr, SingletonForSOS, nullptr)); + ExtractSingleton(expr, SingletonType::SOS, nullptr)); colind.push_back(variables_map_.at(x.value())); refval.push_back(weight); } @@ -1301,7 +1304,7 @@ absl::Status XpressSolver::AddSecondOrderConeConstraints( auto const& ub = soc.upper_bound(); double coef; ASSIGN_OR_RETURN(std::optional const x0, - ExtractSingleton(ub, SingletonForSOCBound, &coef)); + ExtractSingleton(ub, SingletonType::SOCBound, &coef)); if (x0.has_value()) { cols.push_back(variables_map_.at(x0.value())); coefs.push_back(-coef * coef); @@ -1311,7 +1314,7 @@ absl::Status XpressSolver::AddSecondOrderConeConstraints( for (auto const& arg : soc.arguments_to_norm()) { ASSIGN_OR_RETURN(std::optional const x, - ExtractSingleton(arg, SingletonForSOCNorm, &coef)); + ExtractSingleton(arg, SingletonType::SOCNorm, &coef)); cols.push_back(variables_map_.at(x.value())); coefs.push_back(coef * coef); } From 440c4b82bcc2cd27568b45fdaf9db95b42739c58 Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Tue, 21 Oct 2025 15:12:53 +0200 Subject: [PATCH 70/70] Update reference documentation. --- ortools/math_opt/cpp/model.h | 4 ++++ ortools/math_opt/cpp/parameters.h | 3 ++- ortools/math_opt/solvers/xpress_solver.h | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ortools/math_opt/cpp/model.h b/ortools/math_opt/cpp/model.h index 6cb65ed2564..6fe0866c957 100644 --- a/ortools/math_opt/cpp/model.h +++ b/ortools/math_opt/cpp/model.h @@ -481,6 +481,8 @@ class Model { // The `weights` are an implementation detail in the solver used to order the // `expressions`; see the Gurobi documentation for more detail: // https://www.gurobi.com/documentation/9.5/refman/constraints.html#subsubsection:SOSConstraints + // For Xpress see + // https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/XPRSaddsets.html // // These `weights` must either be empty or the same length as `expressions`. // If it is empty, default weights of 1, 2, ... will be used. @@ -540,6 +542,8 @@ class Model { // The `weights` are an implementation detail in the solver used to order the // `expressions`; see the Gurobi documentation for more detail: // https://www.gurobi.com/documentation/9.5/refman/constraints.html#subsubsection:SOSConstraints + // For Xpress see + // https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/XPRSaddsets.html // // These `weights` must either be empty or the same length as `expressions`. // If it is empty, default weights of 1, 2, ... will be used. diff --git a/ortools/math_opt/cpp/parameters.h b/ortools/math_opt/cpp/parameters.h index f4f432583f6..77e48b52775 100644 --- a/ortools/math_opt/cpp/parameters.h +++ b/ortools/math_opt/cpp/parameters.h @@ -351,7 +351,8 @@ struct SolveParameters { // Solvers will typically not return more solutions than the solution limit, // but this is not enforced by MathOpt, see also b/214041169. // - // Currently supported for Gurobi and SCIP, and for CP-SAT only with value 1. + // Currently supported for Gurobi, Xpress and SCIP, and for CP-SAT only with + // value 1. std::optional solution_limit; // If unset, use the solver default. If set, it must be >= 1. diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index a223b4df88d..227b385f319 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -42,7 +42,6 @@ namespace operations_research::math_opt { // Interface to FICO XPRESS solver -// Largely inspired by the Gurobi interface class XpressSolver : public SolverInterface { public: // Creates the XPRESS solver and loads the model into it