Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
xpress_solver.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cstdint>
18#include <memory>
19#include <optional>
20#include <string>
21#include <type_traits>
22#include <utility>
23#include <vector>
24
25#include "absl/log/check.h"
26#include "absl/memory/memory.h"
27#include "absl/status/status.h"
28#include "absl/status/statusor.h"
29#include "absl/strings/str_cat.h"
30#include "absl/strings/str_join.h"
31#include "absl/time/clock.h"
32#include "absl/time/time.h"
33#include "absl/types/span.h"
48
49namespace operations_research {
50namespace math_opt {
51namespace {
52
53struct SharedSolveContext {
54 Xpress* xpress;
55
57 absl::Mutex mutex;
58
66 std::exception_ptr callbackException;
67};
68
75template <typename ProtoT, typename CbT>
76class ScopedCallback {
77 using proto_type = typename ProtoT::proto_type;
78 SharedSolveContext* ctx;
79
80 ScopedCallback(ScopedCallback const&) = delete;
81 ScopedCallback(ScopedCallback&&) = delete;
82 ScopedCallback& operator=(ScopedCallback const&) = delete;
83 ScopedCallback& operator=(ScopedCallback&&) = delete;
84
85 // We intercept and store any exception throw by a callback defining a static
86 // wrapper function that invokes the callback within a try/carch block. For
87 // this to work, we need to deduce the callback return type and arguments.
88 template <typename FuncPtr>
89 struct ExWrapper;
90
91 // Specialization to deduce the callback return and arguments types
92 template <typename R, typename... Args>
93 struct ExWrapper<R (*)(XPRSprob, void*, Args...)> {
94 // The static function that will be directly invoked by Xpress
95 static auto low_level_cb(XPRSprob prob, void* cbdata, Args... args) try {
96 return ProtoT::glueFn(prob, cbdata, args...);
97 } catch (...) {
98 // Catch any exception and terminate Xpress gracefully
99 ScopedCallback* cb = reinterpret_cast<ScopedCallback*>(cbdata);
100 cb->Interrupt(XPRS_STOP_USER);
101 cb->SetCallbackException(std::current_exception());
102 if constexpr (std::is_convertible_v<R, int>) return static_cast<int>(1);
103 }
104 };
105 const proto_type low_level_cb = ExWrapper<proto_type>::low_level_cb;
106
107 public:
108 CbT or_tools_cb;
109
110 ScopedCallback() : ctx(nullptr) {}
111
112 inline absl::Status Add(SharedSolveContext* context, CbT cb) {
113 ctx = context;
115 ProtoT::Add(ctx->xpress, low_level_cb, reinterpret_cast<void*>(this)));
116 or_tools_cb = cb;
117 return absl::OkStatus();
118 }
119
120 inline void Interrupt(int reason) {
121 CHECK_OK(ctx->xpress->Interrupt(reason));
122 }
123
124 inline void SetCallbackException(std::exception_ptr ex) {
125 const absl::MutexLock lock(&ctx->mutex);
126 if (!ctx->callbackException) ctx->callbackException = ex;
127 }
128
129 ~ScopedCallback() {
130 if (ctx)
131 ProtoT::Remove(ctx->xpress, low_level_cb, reinterpret_cast<void*>(this));
132 }
133};
134
151#define DEFINE_SCOPED_CB(CB_NAME, ORTOOLS_CB, CB_RET_TYPE, ARGS) \
152 CB_RET_TYPE CB_NAME##GlueFn ARGS; \
153 struct CB_NAME##Traits { \
154 using proto_type = CB_RET_TYPE(XPRS_CC*) ARGS; \
155 static constexpr proto_type glueFn = CB_NAME##GlueFn; \
156 static absl::Status Add(Xpress* xpress, proto_type fn, void* data) { \
157 return xpress->AddCb##CB_NAME(fn, data, 0); \
158 } \
159 static void Remove(Xpress* xpress, proto_type fn, void* data) { \
160 CHECK_OK(xpress->RemoveCb##CB_NAME(fn, data)); \
161 } \
162 }; \
163 using CB_NAME##ScopedCb = ScopedCallback<CB_NAME##Traits, ORTOOLS_CB>; \
164 CB_RET_TYPE CB_NAME##GlueFn ARGS
165
169DEFINE_SCOPED_CB(Message, MessageCallback, void,
170 (XPRSprob prob, void* cbdata, char const* msg, int len,
171 int type)) {
172 auto cb = reinterpret_cast<MessageScopedCb*>(cbdata);
173
174 if (type != 1 && // info message
175 type != 3 && // warning message
176 type != 4) { // error message
177 // message type 2 is not used by Xpress, negative values mean "flush"
178 return;
179 }
180
181 if (len == 0) {
182 cb->or_tools_cb(std::vector<std::string>{""});
183 return;
184 }
185
186 std::vector<std::string> lines;
187 int start = 0;
188 // There are a few Xpress messages that span multiple lines.
189 // The MessageCallback contract says that messages must not contain
190 // newlines, so we have to split on newline.
191 while (start <= len) { // <= rather than < to catch message ending in '\n'
192 int end = start;
193 while (end < len && msg[end] != '\n') {
194 ++end;
195 }
196 if (start < len) {
197 lines.emplace_back(msg, start, end - start);
198 } else {
199 lines.push_back("");
200 }
201 start = end + 1;
202 }
203 cb->or_tools_cb(lines);
204}
205
209DEFINE_SCOPED_CB(Checktime, SolveInterrupter const*, int,
210 (XPRSprob prob, void* cbdata)) {
211 auto cb = reinterpret_cast<ChecktimeScopedCb*>(cbdata);
212 // Note: we do NOT return non-zero from the callback if the solve was
213 // interrupted. Returning non-zero from the callback is interpreted
214 // as hitting a time limit and we would therefore not map correctly
215 // the resulting stop status to ortools' termination status.
216 if (cb->or_tools_cb->IsInterrupted()) {
217 cb->Interrupt(XPRS_STOP_USER);
218 }
219 return 0;
220}
221
223static void stdoutMessageCallback(std::vector<std::string> const& lines) {
224 for (auto& l : lines) std::cout << l << '\n';
225}
226
227inline BasisStatusProto XpressToMathOptBasisStatus(const int status,
228 bool isConstraint) {
229 // XPRESS row basis status is that of the slack variable
230 // For example, if the slack variable is at LB, the constraint is at UB
231 switch (status) {
232 case XPRS_BASIC:
233 return BASIS_STATUS_BASIC;
234 case XPRS_AT_LOWER:
235 return isConstraint ? BASIS_STATUS_AT_UPPER_BOUND
237 case XPRS_AT_UPPER:
238 return isConstraint ? BASIS_STATUS_AT_LOWER_BOUND
240 case XPRS_FREE_SUPER:
241 return BASIS_STATUS_FREE;
242 default:
244 }
245}
246
247inline int MathOptToXpressBasisStatus(const BasisStatusProto status,
248 bool isConstraint) {
249 // XPRESS row basis status is that of the slack variable
250 // For example, if the slack variable is at LB, the constraint is at UB
251 switch (status) {
253 return XPRS_BASIC;
255 return isConstraint ? XPRS_AT_UPPER : XPRS_AT_LOWER;
257 return isConstraint ? XPRS_AT_LOWER : XPRS_AT_UPPER;
259 return XPRS_FREE_SUPER;
260 default:
261 return XPRS_FREE_SUPER;
262 }
263}
264
271class ScopedSolverContext {
273 SharedSolveContext shared_ctx;
275 MessageScopedCb messageCallback;
277 ChecktimeScopedCb checktimeCallback;
279 std::function<void()> removeInterrupterCallback;
281 struct OneControl {
282 int id;
283 std::variant<int64_t, double, std::string> value;
284 enum {
285 INT_CONTROL,
286 DBL_CONTROL,
287 STR_CONTROL
288 }; // Matches std::variant<>::index;
289 };
291 std::vector<OneControl> modifiedControls;
292
293 public:
294 ScopedSolverContext(Xpress* xpress) : removeInterrupterCallback(nullptr) {
295 shared_ctx.xpress = xpress;
296 }
297 absl::Status Set(int id, int32_t value) { return Set(id, int64_t(value)); }
298 absl::Status Set(int id, int64_t value) {
299 ASSIGN_OR_RETURN(int64_t old, shared_ctx.xpress->GetIntControl64(id));
300 modifiedControls.push_back({id, old});
301 RETURN_IF_ERROR(shared_ctx.xpress->SetIntControl64(id, value));
302 return absl::OkStatus();
303 }
304 absl::Status Set(int id, double value) {
305 ASSIGN_OR_RETURN(double old, shared_ctx.xpress->GetDblControl(id));
306 modifiedControls.push_back({id, old});
307 RETURN_IF_ERROR(shared_ctx.xpress->SetDblControl(id, value));
308 return absl::OkStatus();
309 }
310 absl::Status Set(int id, std::string const& value) {
311 ASSIGN_OR_RETURN(std::string old, shared_ctx.xpress->GetStrControl(id));
312 modifiedControls.push_back({id, old});
313 RETURN_IF_ERROR(shared_ctx.xpress->SetStrControl(id, value));
314 return absl::OkStatus();
315 }
316
317 absl::Status AddCallbacks(MessageCallback message_callback,
318 const SolveInterrupter* interrupter) {
319 if (message_callback)
320 RETURN_IF_ERROR(messageCallback.Add(&shared_ctx, message_callback));
321 if (interrupter) {
322 /* To be extra safe we add two ways to interrupt Xpress:
323 * 1. We register a checktime callback that polls the interrupter.
324 * 2. We register a callback with the interrupter that will call
325 * XPRSinterrupt().
326 * Eventually we should assess whether the first thing is a performance
327 * hit and if so, remove it.
328 */
329 RETURN_IF_ERROR(checktimeCallback.Add(&shared_ctx, interrupter));
330 SolveInterrupter::CallbackId const id =
331 interrupter->AddInterruptionCallback(
332 [=] { CHECK_OK(shared_ctx.xpress->Interrupt(XPRS_STOP_USER)); });
333 removeInterrupterCallback = [=] {
334 interrupter->RemoveInterruptionCallback(id);
335 };
342 }
343 return absl::OkStatus();
344 }
346 absl::Status ApplyParameters(const SolveParametersProto& parameters,
347 MessageCallback message_callback,
348 std::string* export_model, bool* force_postsolve,
349 bool* stop_after_lp) {
350 std::vector<std::string> warnings;
351 ASSIGN_OR_RETURN(bool const isMIP, shared_ctx.xpress->IsMIP());
352 if (parameters.enable_output()) {
353 // This is considered only if no message callback is set, see the
354 // ortools specification of the enable_output parameter.
355 if (!message_callback) {
357 messageCallback.Add(&shared_ctx, stdoutMessageCallback));
358 }
359 }
360 absl::Duration time_limit = absl::InfiniteDuration();
361 if (parameters.has_time_limit()) {
363 time_limit, util_time::DecodeGoogleApiProto(parameters.time_limit()));
364 }
365 if (time_limit < absl::InfiniteDuration()) {
366 RETURN_IF_ERROR(Set(XPRS_TIMELIMIT, absl::ToDoubleSeconds(time_limit)));
367 }
368 if (parameters.has_iteration_limit()) {
369 if (parameters.lp_algorithm() == LP_ALGORITHM_FIRST_ORDER) {
370 // Iteration limit for PDHG is BARHGMAXRESTARTS
372 Set(XPRS_BARHGMAXRESTARTS, parameters.iteration_limit()));
373 } else {
374 RETURN_IF_ERROR(Set(XPRS_LPITERLIMIT, parameters.iteration_limit()));
375 RETURN_IF_ERROR(Set(XPRS_BARITERLIMIT, parameters.iteration_limit()));
376 }
377 }
378 if (parameters.has_node_limit()) {
379 RETURN_IF_ERROR(Set(XPRS_MAXNODE, parameters.node_limit()));
380 }
381 if (parameters.has_cutoff_limit()) {
382 RETURN_IF_ERROR(Set(XPRS_MIPABSCUTOFF, parameters.cutoff_limit()));
383 }
384 if (parameters.has_objective_limit()) {
385 // In Xpress you can apply MIPABSCUTOFF also to LPs.
386 // However, ortools applies both cutoff_limit and objective_limit
387 // to LPs and distinguishes the two, i.e., expect different return
388 // values depending on what is set. Since we cannot easily make this
389 // distinction, we do not support objective_limit. Users should just
390 // use cutoff_limit with LPs as well.
391 warnings.emplace_back(
392 "XpressSolver does not support objective_limit; use cutoff_limit "
393 "instead");
394 }
395 if (parameters.has_best_bound_limit()) {
396 warnings.emplace_back("XpressSolver does not support best_bound_limit");
397 }
398 if (parameters.has_solution_limit()) {
399 RETURN_IF_ERROR(Set(XPRS_MAXMIPSOL, parameters.solution_limit()));
400 }
401 if (parameters.has_threads() && parameters.threads() > 0)
402 RETURN_IF_ERROR(Set(XPRS_THREADS, parameters.threads()));
403 if (parameters.has_random_seed()) {
404 RETURN_IF_ERROR(Set(XPRS_RANDOMSEED, parameters.random_seed()));
405 }
406 if (parameters.has_absolute_gap_tolerance())
408 Set(XPRS_MIPABSSTOP, parameters.absolute_gap_tolerance()));
409 if (parameters.has_relative_gap_tolerance())
411 Set(XPRS_MIPRELSTOP, parameters.relative_gap_tolerance()));
412 if (parameters.has_solution_pool_size()) {
413 warnings.emplace_back("XpressSolver does not support solution_pool_size");
414 }
415 // According to the documentation, LP algorithm is only for LPs
416 if (!isMIP && parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) {
417 switch (parameters.lp_algorithm()) {
419 RETURN_IF_ERROR(Set(XPRS_LPFLAGS, 1 << 1));
420 break;
422 RETURN_IF_ERROR(Set(XPRS_LPFLAGS, 1 << 0));
423 break;
425 RETURN_IF_ERROR(Set(XPRS_LPFLAGS, 1 << 2));
426 break;
428 RETURN_IF_ERROR(Set(XPRS_LPFLAGS, 1 << 2));
430 break;
431 // Note: Xpress also supports network simplex, but that is not
432 // supported by ortools.
433 }
434 }
435 if (parameters.presolve() != EMPHASIS_UNSPECIFIED) {
436 // default value for XPRS_PRESOLVEPASSES is 1
437 int presolvePasses = -1;
438 switch (parameters.presolve()) {
439 case EMPHASIS_OFF:
440 RETURN_IF_ERROR(Set(XPRS_PRESOLVE, 0)); // Turn presolve off
441 break;
442 case EMPHASIS_LOW:
443 presolvePasses = 2;
444 break;
445 case EMPHASIS_MEDIUM:
446 presolvePasses = 3;
447 break;
448 case EMPHASIS_HIGH:
449 presolvePasses = 4;
450 break;
452 presolvePasses = 5;
453 break;
454 }
455 if (presolvePasses > 0)
456 RETURN_IF_ERROR(Set(XPRS_PRESOLVEPASSES, presolvePasses));
457 }
458 if (parameters.cuts() != EMPHASIS_UNSPECIFIED) {
459 switch (parameters.cuts()) {
460 case EMPHASIS_OFF:
462 break;
463 case EMPHASIS_LOW:
465 break;
466 case EMPHASIS_MEDIUM:
468 break;
469 case EMPHASIS_HIGH:
471 break;
473 RETURN_IF_ERROR(Set(XPRS_CUTSTRATEGY, 3)); // Same as high
474 break;
475 }
476 }
477 if (parameters.heuristics() != EMPHASIS_UNSPECIFIED) {
478 switch (parameters.heuristics()) {
479 case EMPHASIS_OFF:
481 break;
483 break;
484 case EMPHASIS_LOW: // fallthrough
485 case EMPHASIS_MEDIUM:
487 break;
488 case EMPHASIS_HIGH: // fallthrough
491 break;
492 }
493 }
494
495 for (const XpressParametersProto::Parameter& parameter :
496 parameters.xpress().parameters()) {
497 std::string const& name = parameter.name();
498 std::string const& value = parameter.value();
499 int id, type;
500 int64_t l;
501 double d;
502
503 if (name == "EXPORT_MODEL") {
504 if (export_model) *export_model = value;
505 continue;
506 } else if (name == "FORCE_POSTSOLVE") {
507 if (!absl::SimpleAtoi(value, &l))
509 << "value " << value << " for FORCE_POSTSOLVE"
510 << " is not an integer";
511 if (force_postsolve) *force_postsolve = l != 0;
512 continue;
513 } else if (name == "STOP_AFTER_LP") {
514 if (!absl::SimpleAtoi(value, &l))
516 << "value " << value << " for STOP_AFTER_LP"
517 << " is not an integer";
518 if (stop_after_lp) *stop_after_lp = l != 0;
519 continue;
520 }
522 shared_ctx.xpress->GetControlInfo(name.c_str(), &id, &type));
523 switch (type) {
524 case XPRS_TYPE_INT: // fallthrough
525 case XPRS_TYPE_INT64:
526 if (!absl::SimpleAtoi(value, &l))
528 << "value " << value << " for " << name
529 << " is not an integer";
530 if (type == XPRS_TYPE_INT && (l > std::numeric_limits<int>::max() ||
531 l < std::numeric_limits<int>::min()))
533 << "value " << value << " for " << name
534 << " is out of range";
535 RETURN_IF_ERROR(Set(id, l));
536 break;
537 case XPRS_TYPE_DOUBLE:
538 if (!absl::SimpleAtod(value, &d))
540 << "value " << value << " for " << name
541 << " is not a floating pointer number";
542 RETURN_IF_ERROR(Set(id, d));
543 break;
544 case XPRS_TYPE_STRING:
545 RETURN_IF_ERROR(Set(id, value));
546 break;
547 default:
549 << "bad control type for " << name;
550 }
551 }
552
553 if (!warnings.empty()) {
554 return absl::InvalidArgumentError(absl::StrJoin(warnings, "; "));
555 }
556 return absl::OkStatus();
557 }
558 absl::Status ApplyModelParameters(
559 ModelSolveParametersProto const& model_parameters,
560 gtl::linked_hash_map<XpressSolver::VarId,
562 variables_map,
563 gtl::linked_hash_map<XpressSolver::LinearConstraintId,
564 XpressSolver::LinearConstraintData> const&
565 linear_constraints_map,
566 gtl::linked_hash_map<XpressSolver::AuxiliaryObjectiveId,
568 objectives_map) {
569 ASSIGN_OR_RETURN(int const cols,
570 shared_ctx.xpress->GetIntAttr(XPRS_ORIGINALCOLS));
571 ASSIGN_OR_RETURN(int const rows,
572 shared_ctx.xpress->GetIntAttr(XPRS_ORIGINALROWS));
573 // Set initial basis
574 if (model_parameters.has_initial_basis()) {
575 // XPRSloadbasis() will raise an error if called on a model in presolved
576 // state. We still trap this already here so that we can produce a more
577 // meaningful error message.
578 ASSIGN_OR_RETURN(int const state,
579 shared_ctx.xpress->GetIntAttr(XPRS_PRESOLVESTATE));
580 if (state & ((1 << 1) | (1 << 2))) {
582 << "cannot set basis for model in presolved space (consider "
583 "FORCE_POSTSOLVE?)";
584 }
585 auto const& basis = model_parameters.initial_basis();
586 std::vector<int> xpress_var_basis_status(cols);
587 for (const auto [id, value] : MakeView(basis.variable_status())) {
588 xpress_var_basis_status[variables_map.at(id)] =
589 MathOptToXpressBasisStatus(static_cast<BasisStatusProto>(value),
590 false);
591 }
592 std::vector<int> xpress_constr_basis_status(rows);
593 for (const auto [id, value] : MakeView(basis.constraint_status())) {
594 xpress_constr_basis_status[linear_constraints_map.at(id)
595 .constraint_index] =
596 MathOptToXpressBasisStatus(static_cast<BasisStatusProto>(value),
597 true);
598 }
599 RETURN_IF_ERROR(shared_ctx.xpress->SetStartingBasis(
600 xpress_constr_basis_status, xpress_var_basis_status));
601 }
602 std::vector<int> colind;
603
604 // Install solution hints. Xpress does not explicitly have solution
605 // hints but it supports partial MIP starts. So we just add each solution
606 // hint as MIP start.
607 if (model_parameters.solution_hints_size() > 0) {
608 unsigned int cnt = 0;
609 std::vector<double> mipStart;
610 colind.reserve(cols);
611 mipStart.reserve(cols);
612 for (auto const& hint : model_parameters.solution_hints()) {
613 colind.clear();
614 mipStart.clear();
615 for (const auto [id, value] : MakeView(hint.variable_values())) {
616 colind.push_back(variables_map.at(id));
617 mipStart.push_back(value);
618 }
619 if (mipStart.size() > cols)
621 << "more solution hints than columns";
622 // XPRSaddmipsol() expects a solution in the original space
623 RETURN_IF_ERROR(shared_ctx.xpress->AddMIPSol(
624 mipStart, colind, absl::StrCat("SolutionHint", cnt).c_str()));
625 ++cnt;
626 }
627 }
628
629 // Install branching priorities.
630 if (model_parameters.has_branching_priorities()) {
631 auto const& prios = model_parameters.branching_priorities();
632 colind.clear();
633 colind.reserve(prios.ids_size());
634 std::vector<int> priority;
635 priority.reserve(prios.ids_size());
636 for (const auto [id, prio] : MakeView(prios)) {
637 colind.push_back(variables_map.at(id));
638 // Xpress only allows priorities in [0,1000].
639 // In ortools higher priority takes precedence while in Xpress
640 // lower priority takes precedence.
641 if (prio < 0 || prio > 1000)
643 << "Xpress only allows branching priorities in [0,1000]";
644 priority.push_back(
645 1000 - prio); // Smaller prios have higher precedence in Xpress!
646 }
647
648 RETURN_IF_ERROR(shared_ctx.xpress->LoadDirs(
649 absl::MakeSpan(colind), absl::MakeSpan(priority), std::nullopt,
650 std::nullopt, std::nullopt));
651 }
652
653 // Objective parameters: primary/single objective
654 if (model_parameters.has_primary_objective_parameters()) {
655 auto const& p = model_parameters.primary_objective_parameters();
656 // Objective violation tolerances only need to be installed for
657 // multi-objective models. We just set them blindly here. They don't
658 // hurt for a single-objective model.
659 if (p.has_objective_degradation_absolute_tolerance()) {
660 RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl(
662 p.objective_degradation_absolute_tolerance()));
663 }
664 if (p.has_objective_degradation_relative_tolerance()) {
665 RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl(
667 p.objective_degradation_relative_tolerance()));
668 }
669 if (p.has_time_limit()) {
670 // We support a time limit but only if there is one single objective.
671 if (objectives_map.size() > 0) {
673 << "Xpress does not support per-objective time limits";
674 }
675 ASSIGN_OR_RETURN(auto l,
676 util_time::DecodeGoogleApiProto(p.time_limit()));
677
678 RETURN_IF_ERROR(shared_ctx.xpress->SetDblControl(
679 XPRS_TIMELIMIT, absl::ToDoubleSeconds(l)));
680 }
681 }
682 // Objective parameters: auxiliary objectives
683 for (auto const& [id, p] :
684 model_parameters.auxiliary_objective_parameters()) {
685 if (p.has_objective_degradation_absolute_tolerance()) {
686 RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl(
687 objectives_map.at(id), XPRS_OBJECTIVE_ABSTOL,
688 p.objective_degradation_absolute_tolerance()));
689 }
690 if (p.has_objective_degradation_relative_tolerance()) {
691 RETURN_IF_ERROR(shared_ctx.xpress->SetObjectiveDoubleControl(
692 objectives_map.at(id), XPRS_OBJECTIVE_RELTOL,
693 p.objective_degradation_relative_tolerance()));
694 }
695 if (p.has_time_limit()) {
697 << "Xpress does not support per-objective time limits";
698 }
699 }
700
701 if (model_parameters.lazy_linear_constraint_ids_size() > 0) {
702 std::vector<int> delayedRows;
703 delayedRows.reserve(rows);
704 for (auto const& idx : model_parameters.lazy_linear_constraint_ids()) {
705 delayedRows.push_back(linear_constraints_map.at(idx).constraint_index);
706 }
707 if (delayedRows.size() > rows)
709 << "more lazy constraints than rows";
710
711 RETURN_IF_ERROR(shared_ctx.xpress->LoadDelayedRows(delayedRows));
712 }
713
714 return absl::OkStatus();
715 }
717 void Interrupt(int reason) { CHECK_OK(shared_ctx.xpress->Interrupt(reason)); }
718
719 void ReraiseException() {
720 if (shared_ctx.callbackException) {
721 std::exception_ptr ex = shared_ctx.callbackException;
722 shared_ctx.callbackException = nullptr;
723 std::rethrow_exception(ex);
724 }
725 }
726
727 ~ScopedSolverContext() {
728 for (auto it = modifiedControls.rbegin(); it != modifiedControls.rend();
729 ++it) {
730 switch (it->value.index()) {
731 case OneControl::INT_CONTROL:
732 CHECK_OK(shared_ctx.xpress->SetIntControl64(
733 it->id, std::get<int64_t>(it->value)));
734 break;
735 case OneControl::DBL_CONTROL:
736 CHECK_OK(shared_ctx.xpress->SetDblControl(
737 it->id, std::get<double>(it->value)));
738 break;
739 case OneControl::STR_CONTROL:
740 CHECK_OK(shared_ctx.xpress->SetStrControl(
741 it->id, std::get<std::string>(it->value).c_str()));
742 break;
743 }
744 }
745 if (removeInterrupterCallback) removeInterrupterCallback();
746 // If pending callback exception was not reraised yet then do it now
747 if (shared_ctx.callbackException)
748 std::rethrow_exception(shared_ctx.callbackException);
749 }
750};
751
753enum class SingletonType {
754 SOS,
755 SOCBound,
756 SOCNorm
757};
758
759// ortools supports SOS constraints and second order cone constraints on
760// expressions. Xpress only supports these constructs on singleton variables.
761// We could create auxiliary variables here, set each of them equal to one of
762// the expressions and then formulate SOS/SOC on the auxiliary variables.
763// This however seems a bit of overkill at the moment, so we just error out
764// if elements are non-singleton.
765// Returns the variable of the singleton as return value and its coefficient
766// in *p_coef.
767absl::StatusOr<std::optional<XpressSolver::VarId>> ExtractSingleton(
768 LinearExpressionProto const& expr, SingletonType type, double* p_coef) {
769 double const constant = expr.offset();
770 if (expr.ids_size() == 1 && constant == 0.0) {
771 // We have a single variable in the expression and no constant.
772 double const coef = expr.coefficients(0);
773 switch (type) {
774 case SingletonType::SOS:
775 // A non-zero coefficient does not change anything, so is allowed.
776 if (coef == 0.0) {
778 << "Xpress does not support coefficient " << coef
779 << " in SOS (consider using auxiliary variables?)";
780 }
781 break;
782 case SingletonType::SOCBound: // fallthrough
783 case SingletonType::SOCNorm:
784 // We are going to square the coefficient, so anything non-negative
785 // is allowed.
786 if (coef < 0) {
788 << "Xpress does not support coefficient " << coef
789 << " in a second order cone constraint "
790 << (type == SingletonType::SOCBound ? "bound" : "norm")
791 << " (consider using auxiliary variables?)";
792 }
793 break;
794 }
795 if (p_coef) *p_coef = coef;
796 return std::optional<XpressSolver::VarId>(expr.ids(0));
797 } else if (expr.ids_size() == 0) {
798 // The expression is constant.
799 switch (type) {
800 case SingletonType::SOS:
801 // Any non-zero constant would force all other variables to 0.
802 // Any zero constant would be redundant.
803 // Both are edge cases that we do not support at the moment.
805 << "Xpress does not support constant expressions in SOS "
806 "(consider using auxiliary variables?)";
807 case SingletonType::SOCBound:
808 // We are going to square the bound, so it should not be negative.
809 if (constant < 0.0) {
811 << "Xpress does not support constant " << constant
812 << " in a second order cone constraint bound (consider using "
813 "auxiliary variables?)";
814 }
815 break;
816 case SingletonType::SOCNorm:
817 // Constant entries in the norm are not supported (we would have to
818 // move them to the right-hand side).
820 << "Xpress does not support constants in a second order cone "
821 "constraint norm (consider using auxiliary variables?)";
822 }
823 if (p_coef) *p_coef = constant;
824 return std::nullopt;
825 } else {
826 // Multiple coefficients
827 static char const* const name[] = {"SOS",
828 "second order cone constraint bound",
829 "second order cone constraint norm"};
831 << "Xpress does not support general linear expressions in "
832 << name[static_cast<int>(type)]
833 << " (consider using auxiliary variables?)";
834 }
835}
836
840template <typename T>
841struct NameResolver {
842 static std::string const& GetName(T const& container, int i) {
843 return container.names(i);
844 }
845};
846
848template <typename K, typename V>
849struct NameResolver<google::protobuf::Map<K, V>> {
850 static std::string const& GetName(
851 google::protobuf::Map<K, V> const& container,
852 typename google::protobuf::Map<K, V>::const_iterator const& i) {
853 return i->second.name();
854 }
855};
856
861template <typename T, typename I>
862absl::Status AddNames(Xpress* xpress, int type, int offset, I begin, I end,
863 T const& container) {
864 std::vector<char> buffer;
865 int i = 0, start = 0;
866 while (begin != end) {
867 std::string const& name = NameResolver<T>::GetName(container, begin);
868 char const* c_name = name.c_str();
869 buffer.insert(buffer.end(), c_name, c_name + name.size() + 1);
870 // Add names in chunks of 1MB.
871 if (buffer.size() > 1024 * 1024) {
873 xpress->AddNames(type, buffer, offset + start, offset + i));
874 start = i + 1;
875 buffer.clear();
876 }
877 ++i;
878 ++begin;
879 }
880 if (buffer.size()) {
882 xpress->AddNames(type, buffer, offset + start, offset + i - 1));
883 }
884 return absl::OkStatus();
885}
886
887} // namespace
888
890 .integer_variables = SupportType::kSupported,
891 .multi_objectives = SupportType::kSupported,
892 .quadratic_objectives = SupportType::kSupported,
893 .quadratic_constraints = SupportType::kSupported,
894 // Limitation: We only implemented support for constraints of type
895 // norm(a1*x1,...,an*xn) <= a0*x0
896 // General linear expressions in the norm or in the bound are not
897 // supported at the moment. They must be emulated by the caller using
898 // auxiliary variables. The right-hand side may be a constant.
899 .second_order_cone_constraints = SupportType::kSupported,
900 // Limitation: We only implemented support for SOS constraints on singleton
901 // variables. General expressions in the SOS are not supported. They must
902 // be emulated by the caller using auxiliary variables.
903 .sos1_constraints = SupportType::kSupported,
904 .sos2_constraints = SupportType::kSupported,
905 .indicator_constraints = SupportType::kSupported};
906
907absl::StatusOr<std::unique_ptr<XpressSolver>> XpressSolver::New(
908 const ModelProto& model, const InitArgs& init_args) {
910 return absl::InvalidArgumentError("Xpress is not correctly installed.");
911 }
914
915 // We can add here extra checks that are not made in ModelIsSupported
916 // (for example, if XPRESS does not support multi-objective with quad terms)
917
918 ASSIGN_OR_RETURN(auto xpr, Xpress::New(model.name()));
919 bool extract_names = init_args.streamable.has_xpress() &&
920 init_args.streamable.xpress().has_extract_names() &&
921 init_args.streamable.xpress().extract_names();
922 auto xpress_solver =
923 absl::WrapUnique(new XpressSolver(std::move(xpr), extract_names));
924 RETURN_IF_ERROR(xpress_solver->LoadModel(model));
925 return xpress_solver;
926}
927
928absl::Status XpressSolver::LoadModel(const ModelProto& input_model) {
929 CHECK(xpress_ != nullptr);
930 RETURN_IF_ERROR(xpress_->SetProbName(input_model.name()));
931 RETURN_IF_ERROR(AddNewVariables(input_model.variables()));
932 RETURN_IF_ERROR(AddNewLinearConstraints(input_model.linear_constraints()));
933 RETURN_IF_ERROR(ChangeCoefficients(input_model.linear_constraint_matrix()));
934 RETURN_IF_ERROR(AddObjective(input_model.objective(), std::nullopt,
935 !input_model.auxiliary_objectives().empty()));
936 // Tests expect an error on duplicate priorities, so raise one.
937 // Xpress would otherwise merge objectives with the same objective when it
938 // starts solving.
939 absl::flat_hash_set<AuxiliaryObjectiveId> prios = {
940 input_model.objective().priority()};
941 for (auto const& [id, obj] : input_model.auxiliary_objectives()) {
942 auto const prio = obj.priority();
943 if (!prios.insert(prio).second) {
945 << "repeated objective priority: " << prio;
946 }
947 RETURN_IF_ERROR(AddObjective(obj, id, true));
948 }
949 RETURN_IF_ERROR(AddSOS(input_model.sos1_constraints(), true));
950 RETURN_IF_ERROR(AddSOS(input_model.sos2_constraints(), false));
951 RETURN_IF_ERROR(AddIndicators(input_model.indicator_constraints()));
952 RETURN_IF_ERROR(AddQuadraticConstraints(input_model.quadratic_constraints()));
953 RETURN_IF_ERROR(AddSecondOrderConeConstraints(
954 input_model.second_order_cone_constraints()));
955 return absl::OkStatus();
956}
957
958absl::Status XpressSolver::AddNewVariables(
959 const VariablesProto& new_variables) {
960 ASSIGN_OR_RETURN(const int num_old_variables,
961 xpress_->GetIntAttr(XPRS_ORIGINALCOLS));
962 const int num_new_variables = new_variables.lower_bounds().size();
963 std::vector<char> variable_type(num_new_variables);
964 ASSIGN_OR_RETURN(int const n_variables,
965 xpress_->GetIntAttr(XPRS_ORIGINALCOLS));
966 bool have_integers = false;
967 for (int j = 0; j < num_new_variables; ++j) {
968 const VarId id = new_variables.ids(j);
969 gtl::InsertOrDie(&variables_map_, id, j + n_variables);
970 if (new_variables.integers(j)) {
971 // Note: ortools does not distinguish between binary variables and
972 // integer variables in {0,1}
973 variable_type[j] = XPRS_INTEGER;
974 have_integers = true;
975 } else {
976 variable_type[j] = XPRS_CONTINUOUS;
977 }
978 }
979 if (!have_integers) {
980 // There are no integer variables, so we clear variable_type to
981 // save the call to XPRSchgcoltype() in AddVars()
982 variable_type.clear();
983 }
985 xpress_->AddVars(num_new_variables, {}, new_variables.lower_bounds(),
986 new_variables.upper_bounds(), variable_type));
987
988 if (extract_names_) {
989 RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_COLUMN,
990 num_old_variables, 0, num_new_variables,
991 new_variables));
992 }
993
994 return absl::OkStatus();
995}
996
997XpressSolver::XpressSolver(std::unique_ptr<Xpress> g_xpress, bool extract_names)
998 : xpress_(std::move(g_xpress)), extract_names_(extract_names) {}
999
1000void XpressSolver::ExtractBounds(double lb, double ub, char& sense, double& rhs,
1001 double& rng) {
1002 sense = XPRS_EQUAL;
1003 rhs = 0.0;
1004 rng = 0.0;
1005 const bool lb_is_xprs_neg_inf = lb <= kMinusInf;
1006 const bool ub_is_xprs_pos_inf = ub >= kPlusInf;
1007 if (lb_is_xprs_neg_inf && ub_is_xprs_pos_inf) {
1008 // We have a row
1009 // -inf <= expression <= inf
1010 // Xpress has no way to submit this as a ranged constraint. For Xpress
1011 // the upper bound of the constraint is just the ub and the lower bound
1012 // is computed as ub-abs(lb). This would result in inf-inf=nan if you
1013 // use IEEE infinity or XPRS_INFINITY - XPRS_INFINITY = 0. Both are wrong.
1014 // So we explicitly register this as free row.
1015 sense = XPRS_NONBINDING;
1016 rhs = 0.0;
1017 rng = 0.0;
1018 } else if (lb_is_xprs_neg_inf && !ub_is_xprs_pos_inf) {
1019 sense = XPRS_LESS_EQUAL;
1020 rhs = ub;
1021 } else if (!lb_is_xprs_neg_inf && ub_is_xprs_pos_inf) {
1022 sense = XPRS_GREATER_EQUAL;
1023 rhs = lb;
1024 } else if (lb == ub) {
1025 sense = XPRS_EQUAL;
1026 rhs = lb;
1027 } else {
1028 sense = XPRS_RANGE;
1029 rhs = ub;
1030 rng = ub - lb;
1031 }
1032}
1033
1034absl::Status XpressSolver::AddNewLinearConstraints(
1035 const LinearConstraintsProto& constraints) {
1036 // TODO: we might be able to improve performance by setting coefs also
1037 ASSIGN_OR_RETURN(int const num_old_constraints,
1038 xpress_->GetIntAttr(XPRS_ORIGINALROWS));
1039 const int num_new_constraints = constraints.lower_bounds().size();
1040 std::vector<char> constraint_sense;
1041 constraint_sense.reserve(num_new_constraints);
1042 std::vector<double> constraint_rhs;
1043 constraint_rhs.reserve(num_new_constraints);
1044 std::vector<double> constraint_rng;
1045 constraint_rng.reserve(num_new_constraints);
1046 ASSIGN_OR_RETURN(int n_constraints, xpress_->GetIntAttr(XPRS_ORIGINALROWS));
1047 for (int i = 0; i < num_new_constraints; ++i) {
1048 const int64_t id = constraints.ids(i);
1049 LinearConstraintData& constraint_data =
1050 gtl::InsertKeyOrDie(&linear_constraints_map_, id);
1051 constraint_data.lower_bound = constraints.lower_bounds(i);
1052 constraint_data.upper_bound = constraints.upper_bounds(i);
1053 constraint_data.constraint_index = i + n_constraints;
1054 char sense = XPRS_EQUAL;
1055 double rhs = 0.0;
1056 double rng = 0.0;
1057 ExtractBounds(constraint_data.lower_bound, constraint_data.upper_bound,
1058 sense, rhs, rng);
1059 constraint_sense.emplace_back(sense);
1060 constraint_rhs.emplace_back(rhs);
1061 constraint_rng.emplace_back(rng);
1062 }
1063 // Add all constraints in one call.
1065 xpress_->AddConstrs(constraint_sense, constraint_rhs, constraint_rng));
1066 if (extract_names_) {
1067 RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_ROW, num_old_constraints,
1068 0, num_new_constraints, constraints));
1069 }
1070 return absl::OkStatus();
1071}
1072
1073absl::Status XpressSolver::AddObjective(
1074 const ObjectiveProto& objective,
1075 std::optional<AuxiliaryObjectiveId> objective_id, bool multiobj) {
1076 double weight = 1.0;
1077 bool haveId = objective_id.has_value();
1078
1079 if (multiobj) {
1080 // In ortools smaller priority means more important, in Xpress,
1081 // higher priority means more important, so we must invert priorities.
1082 // Moreover, in Xpress priorities are 32bit.
1083 // Note that ortools does not allow duplicate priorities, this is checked
1084 // by the caller.
1085 if (objective.priority() <= INT_MIN || objective.priority() > INT_MAX) {
1087 << "Xpress only supports 32bit signed integers as objective "
1088 "priority, not "
1089 << objective.priority();
1090 }
1091 }
1092
1093 // Set/adjust objective sense.
1094 if (!multiobj) {
1095 // Not a multi-objective model
1096 RETURN_IF_ERROR(xpress_->SetObjectiveSense(objective.maximize()));
1097 } else if (!objective_id.has_value()) {
1098 // First objective in multi-objective.
1099 RETURN_IF_ERROR(xpress_->SetObjectiveSense(objective.maximize()));
1100 is_multiobj_ = true;
1101 } else {
1102 // Auxiliary objective in multi-objective. Xpress does not support
1103 // different objective senses for different objectives. So if the sense
1104 // does not match we set the weight to -1.0 to invert the objective
1105 // coefficients.
1106 ASSIGN_OR_RETURN(double const objsen,
1107 xpress_->GetDoubleAttr(XPRS_OBJSENSE));
1108 if (objective.maximize() != (objsen < 0.0)) {
1109 weight = -1.0;
1110 }
1111 }
1112
1113 // Extract the objective.
1114 // First do quadratic terms since these are illegal for auxiliary objectives
1115 // Quadratic terms
1116 const int num_terms = objective.quadratic_coefficients().row_ids().size();
1117 if (num_terms > 0) {
1118 if (multiobj && objective_id.has_value()) {
1120 << "Xpress does not support quadratic terms in anything but the "
1121 "first objective";
1122 }
1123 std::vector<int> first_var_index(num_terms);
1124 std::vector<int> second_var_index(num_terms);
1125 std::vector<double> coefficients(num_terms);
1126 for (int k = 0; k < num_terms; ++k) {
1127 const int64_t row_id = objective.quadratic_coefficients().row_ids(k);
1128 const int64_t column_id =
1129 objective.quadratic_coefficients().column_ids(k);
1130 first_var_index[k] = variables_map_.at(row_id);
1131 second_var_index[k] = variables_map_.at(column_id);
1132 // XPRESS supposes a 1/2 implicit multiplier to quadratic terms (see doc)
1133 // We have to multiply it by 2 for diagonal terms
1134 double m = first_var_index[k] == second_var_index[k] ? 2 : 1;
1135 coefficients[k] = objective.quadratic_coefficients().coefficients(k) * m;
1136 }
1137 RETURN_IF_ERROR(xpress_->SetQuadraticObjective(
1138 first_var_index, second_var_index, coefficients));
1139 }
1140
1141 // Linear terms
1142 std::vector<int> index;
1143 index.reserve(objective.linear_coefficients().ids_size());
1144 for (const int64_t id : objective.linear_coefficients().ids()) {
1145 index.push_back(variables_map_.at(id));
1146 }
1147
1148 if (multiobj) {
1149 if (!objective_id.has_value()) {
1150 // Primary objective
1151 RETURN_IF_ERROR(xpress_->SetLinearObjective(
1152 objective.offset(), index, objective.linear_coefficients().values()));
1153 RETURN_IF_ERROR(xpress_->SetObjectiveIntControl(
1155 // checked above
1156 static_cast<int>(-objective.priority())));
1158 xpress_->SetObjectiveDoubleControl(0, XPRS_OBJECTIVE_WEIGHT, weight));
1159 } else {
1160 // Auxiliary objective
1162 int const newid,
1163 xpress_->AddObjective(
1164 objective.offset(), static_cast<int>(index.size()),
1165 absl::MakeSpan(index), objective.linear_coefficients().values(),
1166 // checked above
1167 static_cast<int>(-objective.priority()), weight));
1168 gtl::InsertOrDie(&objectives_map_, objective_id.value(), newid);
1169 }
1170 } else {
1171 RETURN_IF_ERROR(xpress_->SetLinearObjective(
1172 objective.offset(), index, objective.linear_coefficients().values()));
1173 }
1174
1175 return absl::OkStatus();
1176}
1177
1199absl::Status XpressSolver::AddSOS(
1200 const google::protobuf::Map<AnyConstraintId, SosConstraintProto>& sets,
1201 bool sos1) {
1202 if (sets.empty()) return absl::OkStatus();
1203 std::vector<XPRSint64> start;
1204 std::vector<int> colind;
1205 std::vector<double> refval;
1206 ASSIGN_OR_RETURN(int nextId, xpress_->GetIntAttr(XPRS_ORIGINALSETS));
1207 int const num_old_sets = nextId;
1208 auto* sosmap = sos1 ? &sos1_map_ : &sos2_map_;
1209 for (auto const& [sosId, sos] : sets) {
1210 start.push_back(colind.size());
1211 auto count = sos.expressions_size();
1212 bool const has_weight = sos.weights_size() > 0;
1213 for (decltype(count) i = 0; i < count; ++i) {
1214 auto const& expr = sos.expressions(i);
1215 double const weight = has_weight ? sos.weights(i) : (i + 1);
1216 // Note: A constant value in an SOS forces all others to zero. At the
1217 // moment we do not support this. We consider this an edge case.
1218 ASSIGN_OR_RETURN(std::optional<VarId> x,
1219 ExtractSingleton(expr, SingletonType::SOS, nullptr));
1220 colind.push_back(variables_map_.at(x.value()));
1221 refval.push_back(weight);
1222 }
1223 gtl::InsertOrDie(sosmap, sosId, nextId);
1224 ++nextId;
1225 }
1226 std::vector<char> settype(start.size(), sos1 ? '1' : '2');
1227 RETURN_IF_ERROR(xpress_->AddSets(settype, start, colind, refval));
1228 if (extract_names_) {
1229 RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_SET, num_old_sets,
1230 sets.begin(), sets.end(), sets));
1231 }
1232 return absl::OkStatus();
1233}
1234
1235void XpressSolver::ExtractLinear(SparseDoubleVectorProto const& expr,
1236 std::vector<int>& colind,
1237 std::vector<double>& coef) {
1238 // Note: Constant terms in expressions are already mixed into the
1239 // right-hand side by ortools, so we don't have to deal with them
1240 // here.
1241 auto terms = expr.ids_size();
1242 colind.reserve(colind.size() + terms);
1243 coef.reserve(coef.size() + terms);
1244 for (decltype(terms) i = 0; i < terms; ++i) {
1245 colind.push_back(variables_map_.at(expr.ids(i)));
1246 coef.push_back(expr.values(i));
1247 }
1248}
1249
1250void XpressSolver::ExtractQuadratic(QuadraticConstraintProto const& expr,
1251 std::vector<int>& lin_colind,
1252 std::vector<double>& lin_coef,
1253 std::vector<int>& quad_col1,
1254 std::vector<int>& quad_col2,
1255 std::vector<double>& quad_coef) {
1256 // Note: Constant terms in expressions are already mixed into the
1257 // right-hand side by ortools, so we don't have to deal with them
1258 // here.
1259 auto const& lin = expr.linear_terms();
1260 auto linTerms = lin.ids_size();
1261 lin_colind.reserve(lin_colind.size() + linTerms);
1262 lin_coef.reserve(lin_coef.size() + linTerms);
1263 for (decltype(linTerms) i = 0; i < linTerms; ++i) {
1264 lin_colind.push_back(variables_map_.at(lin.ids(i)));
1265 lin_coef.push_back(lin.values(i));
1266 }
1267 auto const& quad = expr.quadratic_terms();
1268 auto quadTerms = quad.row_ids_size();
1269 quad_col1.reserve(quad_col1.size() + quadTerms);
1270 quad_col2.reserve(quad_col2.size() + quadTerms);
1271 quad_coef.reserve(quad_coef.size() + quadTerms);
1272 for (decltype(quadTerms) i = 0; i < quadTerms; ++i) {
1273 int const col1 = variables_map_.at(quad.row_ids(i));
1274 int const col2 = variables_map_.at(quad.column_ids(i));
1275 double coef = quad.coefficients(i);
1276 if (col1 != col2) coef *= 0.5;
1277 quad_col1.push_back(col1);
1278 quad_col2.push_back(col2);
1279 quad_coef.push_back(coef);
1280 }
1282}
1283
1284absl::Status XpressSolver::AddIndicators(
1285 const google::protobuf::Map<IndicatorConstraintId,
1286 IndicatorConstraintProto>& indicators) {
1287 if (indicators.empty()) return absl::OkStatus();
1288 // For XPRSaddrows()
1289 size_t count = indicators.size();
1290 std::vector<double> rhs(count);
1291 std::vector<double> rng(count);
1292 std::vector<char> sense(count);
1293 std::vector<XPRSint64> start(count);
1294 std::vector<int> colind;
1295 std::vector<double> rowcoef;
1296 // For XPRSsetindicators()
1297 std::vector<int> i_rowind(count);
1298 std::vector<int> i_colind(count);
1299 std::vector<int> i_complement(count);
1300 ASSIGN_OR_RETURN(int const oldRows, xpress_->GetIntAttr(XPRS_ORIGINALROWS));
1301 int min_icol = std::numeric_limits<int>::max();
1302 int max_icol = std::numeric_limits<int>::min();
1303 bool check_types = false;
1304 int next = 0;
1305 for (auto const& [ortoolsId, indicator] : indicators) {
1306 start[next] = colind.size();
1307 ExtractBounds(indicator.lower_bound(), indicator.upper_bound(), sense[next],
1308 rhs[next], rng[next]);
1309 // ortools tests require us to raise an error on ranged indicator
1310 // constraints
1311 if (sense[next] == XPRS_RANGE) {
1313 << "indicator constraint on ranged constraint";
1314 }
1315
1316 ExtractLinear(indicator.expression(), colind, rowcoef);
1317
1318 i_rowind[next] = oldRows + next;
1319 if (indicator.has_indicator_id()) {
1320 i_colind[next] = variables_map_.at(indicator.indicator_id());
1321 if (i_colind[next] < min_icol) min_icol = i_colind[next];
1322 if (i_colind[next] > max_icol) max_icol = i_colind[next];
1323 i_complement[next] = indicator.activate_on_zero() ? -1 : 1;
1324 check_types = true;
1325 } else {
1326 // By definition, this is an inactive constraint, see
1327 // indicator_constraint.h
1328 i_colind[next] = -1;
1329 i_complement[next] = 0;
1330 sense[next] = XPRS_NONBINDING;
1331 }
1332 LinearConstraintData& data =
1333 gtl::InsertKeyOrDie(&indicator_map_, ortoolsId);
1334 data.constraint_index = oldRows + next;
1335 data.lower_bound = indicator.lower_bound();
1336 data.upper_bound = indicator.upper_bound();
1337 ++next;
1338 }
1339 if (check_types) {
1340 // We have at least one active indicator constraint. We must check types
1341 // of variables. If they are integer but within the range of a binary
1342 // then we must convert them to a binary, otherwise Xpress will reject
1343 // them.
1344 std::vector<double> orig_lb(1 + max_icol - min_icol);
1345 std::vector<double> orig_ub(1 + max_icol - min_icol);
1346 std::vector<char> orig_type(1 + max_icol - min_icol);
1348 xpress_->GetLB(absl::MakeSpan(orig_lb), min_icol, max_icol));
1350 xpress_->GetUB(absl::MakeSpan(orig_ub), min_icol, max_icol));
1352 xpress_->GetColType(absl::MakeSpan(orig_type), min_icol, max_icol));
1353 std::vector<int> colind_bnd;
1354 std::vector<double> new_bds;
1355 std::vector<char> new_bdtype;
1356 std::vector<int> colind_type;
1357 std::vector<char> new_type;
1358 for (auto i : i_colind) {
1359 if (i < 0) continue;
1360 int const idx = i - min_icol;
1361 switch (orig_type[idx]) {
1362 case XPRS_BINARY:
1363 // Ok, nothing to do.
1364 break;
1365 case XPRS_INTEGER:
1366 // Convert to binary if within range.
1367 if (orig_lb[idx] >= 0.0 && orig_lb[idx] <= 1.0 &&
1368 orig_ub[idx] >= 0.0 && orig_ub[idx] <= 1.0) {
1369 double const l = ceil(orig_lb[idx]);
1370 double const u = floor(orig_ub[idx]);
1371 orig_lb[idx] = l; // In case variable is indicator more than once
1372 orig_ub[idx] = u;
1373 // It would require less storage if we performed two calls to
1374 // XPRSchgbounds(): one for changing all lower bounds and one for
1375 // changing all upper bounds. However, this may temporarily result
1376 // in conflicting bounds, which Xpress does not support. So we must
1377 // change all bounds in one single call and collect appropriate data
1378 // for that.
1379 colind_bnd.push_back(i);
1380 colind_bnd.push_back(i);
1381 new_bds.push_back(l);
1382 new_bds.push_back(u);
1383 new_bdtype.push_back('L');
1384 new_bdtype.push_back('U');
1385 colind_type.push_back(i);
1386 new_type.push_back(XPRS_BINARY);
1387 } else {
1388 nonbinary_indicator_ = true;
1389 }
1390 break;
1391 default:
1392 // Anything else cannot be used as indicator variable.
1393 nonbinary_indicator_ = true;
1394 break;
1395 }
1396 }
1397 // Change column type and bounds. Note that we must first change the type
1398 // since changing the type to 'B' will automatically change bounds to [0,1].
1399 // After that we can fix up the bounds.
1400 if (colind_type.size()) {
1401 RETURN_IF_ERROR(xpress_->ChgColType(colind_type, new_type));
1402 }
1403 if (colind_bnd.size()) {
1404 RETURN_IF_ERROR(xpress_->ChgBounds(colind_bnd, new_bdtype, new_bds));
1405 }
1406 }
1407 RETURN_IF_ERROR(xpress_->AddRows(sense, rhs, rng, start, colind, rowcoef));
1408 RETURN_IF_ERROR(xpress_->SetIndicators(i_rowind, i_colind, i_complement));
1409 return absl::OkStatus();
1410}
1411
1412absl::Status XpressSolver::AddQuadraticConstraints(
1413 const google::protobuf::Map<QuadraticConstraintId,
1414 QuadraticConstraintProto>& constraints) {
1415 std::vector<int> lin_colind;
1416 std::vector<double> lin_coef;
1417 std::vector<int> quad_col1;
1418 std::vector<int> quad_col2;
1419 std::vector<double> quad_coef;
1420 ASSIGN_OR_RETURN(int next, xpress_->GetIntAttr(XPRS_ORIGINALROWS));
1421 for (const auto& [ortoolsId, quad] : constraints) {
1422 // Xpress has no function to add multiple quadratic rows in one shot, so we
1423 // add the linear part one by one as well.
1424 char sense;
1425 double rhs;
1426 double rng;
1427 ExtractBounds(quad.lower_bound(), quad.upper_bound(), sense, rhs, rng);
1428 lin_colind.clear();
1429 lin_coef.clear();
1430 quad_col1.clear();
1431 quad_col2.clear();
1432 quad_coef.clear();
1433 ExtractQuadratic(quad, lin_colind, lin_coef, quad_col1, quad_col2,
1434 quad_coef);
1435 RETURN_IF_ERROR(xpress_->AddQRow(sense, rhs, rng, lin_colind, lin_coef,
1436 quad_col1, quad_col2, quad_coef));
1437 LinearConstraintData& data =
1438 gtl::InsertKeyOrDie(&quad_constraints_map_, ortoolsId);
1439 data.constraint_index = next;
1440 data.lower_bound = quad.lower_bound();
1441 data.upper_bound = quad.upper_bound();
1442 ++next;
1443 }
1444 return absl::OkStatus();
1445}
1446
1447// Extract second order cone constraints.
1448// Note that we only support
1449// sum(i in I) x_i^2 <= x_0^2
1450absl::Status XpressSolver::AddSecondOrderConeConstraints(
1451 const google::protobuf::Map<SecondOrderConeConstraintId,
1452 SecondOrderConeConstraintProto>& constraints) {
1453 std::vector<int> cols;
1454 std::vector<double> coefs;
1455 ASSIGN_OR_RETURN(int next, xpress_->GetIntAttr(XPRS_ORIGINALROWS));
1456 for (auto const& [ortoolsId, soc] : constraints) {
1457 cols.clear();
1458 coefs.clear();
1459 double rhs = 0.0;
1460 auto const& ub = soc.upper_bound();
1461 double coef;
1462 ASSIGN_OR_RETURN(std::optional<VarId> const x0,
1463 ExtractSingleton(ub, SingletonType::SOCBound, &coef));
1464 if (x0.has_value()) {
1465 cols.push_back(variables_map_.at(x0.value()));
1466 coefs.push_back(-coef * coef);
1467 } else {
1468 rhs = coef * coef;
1469 }
1470
1471 for (auto const& arg : soc.arguments_to_norm()) {
1472 ASSIGN_OR_RETURN(std::optional<VarId> const x,
1473 ExtractSingleton(arg, SingletonType::SOCNorm, &coef));
1474 cols.push_back(variables_map_.at(x.value()));
1475 coefs.push_back(coef * coef);
1476 }
1477 RETURN_IF_ERROR(xpress_->AddQRow('L', rhs, 0.0, {}, {}, cols, cols, coefs));
1478 LinearConstraintData& data = gtl::InsertKeyOrDie(&soc_map_, ortoolsId);
1479 data.constraint_index = next;
1480 data.lower_bound = kMinusInf;
1481 data.upper_bound = 0.0;
1482 ++next;
1483 }
1484 return absl::OkStatus();
1485}
1486
1487absl::Status XpressSolver::ChangeCoefficients(
1488 const SparseDoubleMatrixProto& matrix) {
1489 const int num_coefficients = matrix.row_ids().size();
1490 std::vector<int> row_index;
1491 row_index.reserve(num_coefficients);
1492 std::vector<int> col_index;
1493 col_index.reserve(num_coefficients);
1494 for (int k = 0; k < num_coefficients; ++k) {
1495 row_index.push_back(
1496 linear_constraints_map_.at(matrix.row_ids(k)).constraint_index);
1497 col_index.push_back(variables_map_.at(matrix.column_ids(k)));
1498 }
1499 return xpress_->ChgCoeffs(row_index, col_index, matrix.coefficients());
1500}
1501
1502absl::StatusOr<SolveResultProto> XpressSolver::Solve(
1503 const SolveParametersProto& parameters,
1504 const ModelSolveParametersProto& model_parameters,
1505 MessageCallback message_callback,
1506 const CallbackRegistrationProto& callback_registration, Callback,
1507 const SolveInterrupter* interrupter) {
1508 force_postsolve_ = false;
1509 primal_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND;
1510 dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND;
1511 solvestatus_ = XPRS_SOLVESTATUS_UNSTARTED;
1512 solstatus_ = XPRS_SOLSTATUS_NOTFOUND;
1513 algorithm_ = XPRS_ALG_DEFAULT;
1515 model_parameters, kXpressSupportedStructures, "XPRESS"));
1516 ASSIGN_OR_RETURN(is_mip_, xpress_->IsMIP());
1517 const absl::Time start = absl::Now();
1518
1519 RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration,
1520 /*supported_events=*/{}));
1521
1522 // Check that bounds are not inverted just before solve
1523 // XPRESS returns "infeasible" when bounds are inverted
1524 {
1525 ASSIGN_OR_RETURN(const InvertedBounds inverted_bounds,
1526 ListInvertedBounds());
1527 RETURN_IF_ERROR(inverted_bounds.ToStatus());
1528 }
1529 // Check that we don't have non-binary indicator variables
1530 if (nonbinary_indicator_) {
1532 << "indicator variable is not binary";
1533 }
1534
1535 // Register callbacks and create scoped context to automatically if an
1536 // exception has been thrown during optimization.
1537 ScopedSolverContext solveContext(xpress_.get());
1538 RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter));
1539 std::string export_model = "";
1540 RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback,
1541 &export_model, &force_postsolve_,
1542 &stop_after_lp_));
1543 RETURN_IF_ERROR(solveContext.ApplyModelParameters(
1544 model_parameters, variables_map_, linear_constraints_map_,
1545 objectives_map_));
1546
1547 // We are ready to solve the problem. If we are asked to export the
1548 // problem, then do that now. Depending on the file name extension we
1549 // either create a save file or an LP/MPS file.
1550 if (export_model.length() > 0) {
1551 if (export_model.length() >= 4 &&
1552 export_model.compare(export_model.length() - 4, 4, ".svf") == 0) {
1553 RETURN_IF_ERROR(xpress_->SaveAs(export_model.c_str()));
1554 } else {
1555 RETURN_IF_ERROR(xpress_->WriteProb(export_model.c_str()));
1556 }
1557 }
1558
1559 // Solve. We use the generic XPRSoptimize() and let Xpress decide what is
1560 // the best algorithm. Note that we do not pass flags to the function
1561 // either. We assume that algorithms are configured via controls like
1562 // LPFLAGS.
1564 xpress_->Optimize(stop_after_lp_ ? "l" : "", &solvestatus_, &solstatus_));
1565 // Reraise any exception now. Note that we cannot just limit the scope of
1566 // solveContext since its destructor will restore controls settings.
1567 // On the other hand, when fetching results we need to check some controls
1568 // (for example, BARALG to decide whether we need to report barrier or
1569 // first order iterations).
1570 solveContext.ReraiseException();
1572 xpress_->GetSolution(&primal_sol_avail_, std::nullopt, 0, -1));
1573 RETURN_IF_ERROR(xpress_->GetDuals(&dual_sol_avail_, std::nullopt, 0, -1));
1574 ASSIGN_OR_RETURN(algorithm_, xpress_->GetIntAttr(XPRS_ALGORITHM));
1575 ASSIGN_OR_RETURN(optimizetypeused_,
1576 xpress_->GetIntAttr(XPRS_OPTIMIZETYPEUSED));
1577 // Do NOT postsolve by default here!
1578 // All functions we use operate in the original space
1579 // and postsolving here is harmful if we want to come back and solve with
1580 // an extended time limit, for example. We defer postsolve until the latest
1581 // point possible. This means we call it in ::Update() and
1582 // ::ComputeInfeasibleSubsystem()
1583 if (force_postsolve_)
1584 RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed";
1585
1587 SolveResultProto solve_result,
1588 ExtractSolveResultProto(start, model_parameters, parameters));
1589
1590 // Other solvers reset/clear model_parameters here.
1591 // We don't do this. Consider for example branching priorities. These
1592 // can only be changed if the model is not in presolved state (see the
1593 // reference documentation of XPRSloaddirs()).
1594 // If the solve terminated due to a resource limit then the model will still
1595 // be in presolved state. In order to clear branching priorities we would
1596 // have to XPRSpostsolve() the problem. That would mean that the next call
1597 // to Solve() would solve the model from scratch. Since this would make
1598 // incremental solves impossible, we don't clear model_parameters here.
1599
1600 return solve_result;
1601}
1602
1603absl::StatusOr<SolveResultProto> XpressSolver::ExtractSolveResultProto(
1604 absl::Time start, const ModelSolveParametersProto& model_parameters,
1605 const SolveParametersProto& solve_parameters) {
1606 SolveResultProto result;
1607 RETURN_IF_ERROR(AppendSolution(result, model_parameters, solve_parameters));
1608 ASSIGN_OR_RETURN(*result.mutable_solve_stats(), GetSolveStats(start));
1609 ASSIGN_OR_RETURN(const double best_primal_bound, GetBestPrimalBound());
1610 ASSIGN_OR_RETURN(const double best_dual_bound, GetBestDualBound());
1612 *result.mutable_termination(),
1613 ConvertTerminationReason(best_primal_bound, best_dual_bound));
1614 return result;
1615}
1616
1617absl::StatusOr<double> XpressSolver::GetBestPrimalBound() const {
1618 if (is_mip_) {
1619 return xpress_->GetDoubleAttr(XPRS_OBJVAL);
1620 } else if (primal_sol_avail_ == XPRS_SOLAVAILABLE_OPTIMAL ||
1621 primal_sol_avail_ == XPRS_SOLAVAILABLE_FEASIBLE) {
1622 return xpress_->GetDoubleAttr(XPRS_OBJVAL);
1623 }
1624 // No primal bound available, return infinity.
1625 ASSIGN_OR_RETURN(double const objsen, xpress_->GetDoubleAttr(XPRS_OBJSENSE));
1626 return objsen * kPlusInf;
1627}
1628
1629absl::StatusOr<double> XpressSolver::GetBestDualBound() const {
1630 if (is_mip_) {
1631 return xpress_->GetDoubleAttr(XPRS_BESTBOUND);
1632 }
1633 // Xpress does not have an attribute to report the best dual bound from
1634 // simplex
1635 else {
1636 ASSIGN_OR_RETURN(int const alg, xpress_->GetIntAttr(XPRS_ALGORITHM));
1637 if (alg == XPRS_ALG_BARRIER)
1638 return xpress_->GetDoubleAttr(XPRS_BARDUALOBJ);
1639 else if (primal_sol_avail_ == XPRS_SOLAVAILABLE_OPTIMAL)
1640 return xpress_->GetDoubleAttr(XPRS_OBJVAL);
1641 }
1642 // No dual bound available, return infinity.
1643 ASSIGN_OR_RETURN(double const objsen, xpress_->GetDoubleAttr(XPRS_OBJSENSE));
1644 return objsen * kMinusInf;
1645}
1646
1650absl::Status XpressSolver::ExtendWithMultiobj(SolutionProto& solution) {
1651 // We may not have solved for all objectives, so make sure we query only
1652 // those that were solved.
1653 ASSIGN_OR_RETURN(int const nSolved, xpress_->GetIntAttr(XPRS_SOLVEDOBJS));
1654 auto* objvals =
1655 solution.mutable_primal_solution()->mutable_auxiliary_objective_values();
1656 for (auto const& [ortoolsId, xpressId] : objectives_map_) {
1657 ASSIGN_OR_RETURN(double const thisobj,
1658 xpress_->CalculateObjectiveN(xpressId, nullptr));
1659 (*objvals)[ortoolsId] = thisobj;
1660 }
1661 return absl::OkStatus();
1662}
1663
1664absl::Status XpressSolver::AppendSolution(
1665 SolveResultProto& solve_result,
1666 const ModelSolveParametersProto& model_parameters,
1667 const SolveParametersProto& solve_parameters) {
1668 ASSIGN_OR_RETURN(int const nVars, xpress_->GetIntAttr(XPRS_ORIGINALCOLS));
1669 if (is_mip_) {
1670 std::vector<double> x(nVars);
1671 int avail;
1673 xpress_->GetSolution(&avail, absl::MakeSpan(x), 0, nVars - 1));
1674 if (avail != XPRS_SOLAVAILABLE_NOTFOUND) {
1675 SolutionProto solution{};
1676 solution.mutable_primal_solution()->set_feasibility_status(
1677 getPrimalSolutionStatus());
1678 ASSIGN_OR_RETURN(const double objval,
1679 xpress_->GetDoubleAttr(XPRS_OBJVAL));
1680 solution.mutable_primal_solution()->set_objective_value(objval);
1681 RETURN_IF_ERROR(ExtendWithMultiobj(solution));
1682 XpressVectorToSparseDoubleVector(
1683 x, variables_map_,
1684 *solution.mutable_primal_solution()->mutable_variable_values(),
1685 model_parameters.variable_values_filter());
1686 *solve_result.add_solutions() = std::move(solution);
1687 }
1688 } else {
1689 // Fetch all results from XPRESS
1690 ASSIGN_OR_RETURN(int const nCons, xpress_->GetIntAttr(XPRS_ORIGINALROWS));
1691 std::vector<double> primals(nVars);
1692 std::vector<double> duals(nCons);
1693 std::vector<double> reducedCosts(nVars);
1694
1695 // This is for handling an edge case:
1696 // If an LP solve is interrupted then XPRSgetsolution() and friends will
1697 // return "not available". However, there may still be a current
1698 // primal or dual feasible solution available - depending on the algorithm.
1699 // Users and ortools tests may expect these to be returned in some cases,
1700 // so we try to pick them up. This must be done via XPRSgetlpsol() which
1701 // is designed for exactly this edge case.
1702 // This only applies to LPs.
1703 auto hasSolution =
1704 (optimizetypeused_ ==
1705 0) && // 0 = LP, 1 = MIP, 2/3 = nonlin local/global
1706 xpress_
1707 // Note: XPRSgetlpsol() returns solution in original space
1708 ->GetLpSol(absl::MakeSpan(primals), absl::MakeSpan(duals),
1709 absl::MakeSpan(reducedCosts))
1710 .ok();
1711
1712 SolutionProto solution{};
1713 bool storeSolutions = (solvestatus_ == XPRS_SOLVESTATUS_STOPPED ||
1714 solvestatus_ == XPRS_SOLVESTATUS_COMPLETED);
1715
1716 if (isPrimalFeasible()) {
1717 // The preferred methods for obtaining primal information are
1718 // XPRSgetsolution() and XPRSgetslacks() (not used here)
1719 // XPRSgetsolution() returns solution in original space.
1721 xpress_->GetSolution(nullptr, absl::MakeSpan(primals), 0, nVars - 1));
1722 solution.mutable_primal_solution()->set_feasibility_status(
1723 getPrimalSolutionStatus());
1724 ASSIGN_OR_RETURN(const double primalBound, GetBestPrimalBound());
1725 solution.mutable_primal_solution()->set_objective_value(primalBound);
1726 XpressVectorToSparseDoubleVector(
1727 primals, variables_map_,
1728 *solution.mutable_primal_solution()->mutable_variable_values(),
1729 model_parameters.variable_values_filter());
1730 RETURN_IF_ERROR(ExtendWithMultiobj(solution));
1731 } else if (storeSolutions) {
1732 // Even if we are not primal feasible, store the results we obtained
1733 // from XPRSgetlpsolution(). The feasibility status of this vector
1734 // is undetermined, though.
1735 solution.mutable_primal_solution()->set_feasibility_status(
1737 ASSIGN_OR_RETURN(const double primalBound, GetBestPrimalBound());
1738 solution.mutable_primal_solution()->set_objective_value(primalBound);
1739 XpressVectorToSparseDoubleVector(
1740 primals, variables_map_,
1741 *solution.mutable_primal_solution()->mutable_variable_values(),
1742 model_parameters.variable_values_filter());
1743 }
1744
1745 if (isDualFeasible()) {
1746 // The preferred methods for obtain dual information are XPRSgetduals()
1747 // and XPRSgetredcosts().
1748 // XPRSgetduals() and XPRSgetredcosts() both return values in the
1749 // original space.
1751 xpress_->GetDuals(nullptr, absl::MakeSpan(duals), 0, nCons - 1));
1752 RETURN_IF_ERROR(xpress_->GetRedCosts(
1753 nullptr, absl::MakeSpan(reducedCosts), 0, nVars - 1));
1754 solution.mutable_dual_solution()->set_feasibility_status(
1755 getDualSolutionStatus());
1756 ASSIGN_OR_RETURN(const double dualBound, GetBestDualBound());
1757 solution.mutable_dual_solution()->set_objective_value(dualBound);
1758 XpressVectorToSparseDoubleVector(
1759 duals, linear_constraints_map_,
1760 *solution.mutable_dual_solution()->mutable_dual_values(),
1761 model_parameters.dual_values_filter());
1762 XpressVectorToSparseDoubleVector(
1763 reducedCosts, variables_map_,
1764 *solution.mutable_dual_solution()->mutable_reduced_costs(),
1765 model_parameters.reduced_costs_filter());
1766 } else if (storeSolutions) {
1767 // Even if we are not dual feasible, store the results we obtained from
1768 // XPRSgetlpsolution(). The feasibility status of this vector
1769 // is undetermined, though.
1770 solution.mutable_dual_solution()->set_feasibility_status(
1772 ASSIGN_OR_RETURN(const double dualBound, GetBestDualBound());
1773 solution.mutable_dual_solution()->set_objective_value(dualBound);
1774 XpressVectorToSparseDoubleVector(
1775 duals, linear_constraints_map_,
1776 *solution.mutable_dual_solution()->mutable_dual_values(),
1777 model_parameters.dual_values_filter());
1778 XpressVectorToSparseDoubleVector(
1779 reducedCosts, variables_map_,
1780 *solution.mutable_dual_solution()->mutable_reduced_costs(),
1781 model_parameters.reduced_costs_filter());
1782 }
1783
1784 // Get basis
1785 ASSIGN_OR_RETURN(auto basis, GetBasisIfAvailable(solve_parameters));
1786 if (basis.has_value()) {
1787 *solution.mutable_basis() = std::move(*basis);
1788 }
1789 *solve_result.add_solutions() = std::move(solution);
1790 }
1791 return absl::OkStatus();
1792}
1793
1794bool XpressSolver::isPrimalFeasible() const {
1795 return primal_sol_avail_ == XPRS_SOLAVAILABLE_FEASIBLE ||
1796 primal_sol_avail_ == XPRS_SOLAVAILABLE_OPTIMAL;
1797}
1798
1799bool XpressSolver::isDualFeasible() const {
1801 return dual_sol_avail_ == XPRS_SOLAVAILABLE_FEASIBLE ||
1802 dual_sol_avail_ == XPRS_SOLAVAILABLE_OPTIMAL;
1803}
1804
1805SolutionStatusProto XpressSolver::getPrimalSolutionStatus() const {
1806 switch (solvestatus_) {
1809 case XPRS_SOLVESTATUS_STOPPED: // fallthrough
1810 case XPRS_SOLVESTATUS_FAILED: // fallthrough
1811 case XPRS_SOLVESTATUS_COMPLETED: // fallthrough
1812 break;
1813 }
1814 switch (solstatus_) {
1817 case XPRS_SOLSTATUS_OPTIMAL: // fallthrough
1824 }
1826}
1827
1828SolutionStatusProto XpressSolver::getDualSolutionStatus() const {
1829 if (dual_sol_avail_ == XPRS_SOLAVAILABLE_OPTIMAL ||
1830 dual_sol_avail_ == XPRS_SOLAVAILABLE_FEASIBLE)
1836}
1837
1838absl::StatusOr<std::optional<BasisProto>> XpressSolver::GetBasisIfAvailable(
1839 const SolveParametersProto&) {
1840 std::vector<int> xprs_variable_basis_status;
1841 std::vector<int> xprs_constraint_basis_status;
1842 // XPRSgetbasis() always returns values in the original space
1843 if (!xpress_
1844 ->GetBasis(xprs_constraint_basis_status, xprs_variable_basis_status)
1845 .ok()) {
1846 return std::nullopt;
1847 }
1848
1849 BasisProto basis;
1850 // Variable basis
1851 for (auto [variable_id, xprs_variable_index] : variables_map_) {
1852 basis.mutable_variable_status()->add_ids(variable_id);
1853 const BasisStatusProto variable_status = XpressToMathOptBasisStatus(
1854 xprs_variable_basis_status[xprs_variable_index], false);
1855 if (variable_status == BASIS_STATUS_UNSPECIFIED) {
1856 return absl::InternalError(
1857 absl::StrCat("Invalid Xpress variable basis status: ",
1858 xprs_variable_basis_status[xprs_variable_index]));
1859 }
1860 basis.mutable_variable_status()->add_values(variable_status);
1861 }
1862
1863 // Constraint basis
1864 for (auto [constraint_id, xprs_ct_index] : linear_constraints_map_) {
1865 basis.mutable_constraint_status()->add_ids(constraint_id);
1866 const BasisStatusProto status = XpressToMathOptBasisStatus(
1867 xprs_constraint_basis_status[xprs_ct_index.constraint_index], true);
1868 if (status == BASIS_STATUS_UNSPECIFIED) {
1869 return absl::InternalError(absl::StrCat(
1870 "Invalid Xpress constraint basis status: ",
1871 xprs_constraint_basis_status[xprs_ct_index.constraint_index]));
1872 }
1873 basis.mutable_constraint_status()->add_values(status);
1874 }
1875
1876 // Dual basis
1877 basis.set_basic_dual_feasibility(
1879 return basis;
1880}
1881
1882absl::StatusOr<SolveStatsProto> XpressSolver::GetSolveStats(
1883 absl::Time start) const {
1884 SolveStatsProto solve_stats;
1885 CHECK_OK(util_time::EncodeGoogleApiProto(absl::Now() - start,
1886 solve_stats.mutable_solve_time()));
1887
1888 int simplex_iters = 0;
1889 int barrier_iters = 0;
1890 int first_order_iters = 0;
1891 if (algorithm_ == XPRS_ALG_DEFAULT) {
1892 // Could be concurrent, so capture simplex and barrier iterations
1893 ASSIGN_OR_RETURN(simplex_iters, xpress_->GetIntAttr(XPRS_SIMPLEXITER));
1894 ASSIGN_OR_RETURN(barrier_iters, xpress_->GetIntAttr(XPRS_BARITER));
1895 } else if (algorithm_ == XPRS_ALG_DUAL || algorithm_ == XPRS_ALG_PRIMAL ||
1896 algorithm_ == XPRS_ALG_NETWORK) {
1897 // Definitely simplex
1898 ASSIGN_OR_RETURN(simplex_iters, xpress_->GetIntAttr(XPRS_SIMPLEXITER));
1899 } else if (algorithm_ == XPRS_ALG_BARRIER) {
1900 // Barrier or first order
1901 ASSIGN_OR_RETURN(const int baralg, xpress_->GetIntControl(XPRS_BARALG));
1902 if (baralg == 4) {
1903 ASSIGN_OR_RETURN(first_order_iters, xpress_->GetIntAttr(XPRS_BARITER));
1904 } else {
1905 ASSIGN_OR_RETURN(barrier_iters, xpress_->GetIntAttr(XPRS_BARITER));
1906 }
1907 }
1908 solve_stats.set_simplex_iterations(simplex_iters);
1909 solve_stats.set_barrier_iterations(barrier_iters);
1910 solve_stats.set_first_order_iterations(first_order_iters);
1911 if (is_mip_) {
1912 ASSIGN_OR_RETURN(const int nodes, xpress_->GetIntAttr(XPRS_NODES));
1913 solve_stats.set_node_count(nodes);
1914 }
1915 return solve_stats;
1916}
1917
1918template <typename T>
1919void XpressSolver::XpressVectorToSparseDoubleVector(
1920 absl::Span<const double> xpress_values, const T& map,
1922 const SparseVectorFilterProto& filter) const {
1923 SparseVectorFilterPredicate predicate(filter);
1924 for (auto [id, xpress_data] : map) {
1925 const double value = xpress_values[get_model_index(xpress_data)];
1926 if (predicate.AcceptsAndUpdate(id, value)) {
1927 result.add_ids(id);
1928 result.add_values(value);
1929 }
1930 }
1931}
1932
1933absl::StatusOr<TerminationProto> XpressSolver::ConvertTerminationReason(
1934 double best_primal_bound, double best_dual_bound) const {
1935 ASSIGN_OR_RETURN(double const objsen, xpress_->GetDoubleAttr(XPRS_OBJSENSE));
1936 ASSIGN_OR_RETURN(int const stopStatus, xpress_->GetIntAttr(XPRS_STOPSTATUS));
1937 bool const isMax = objsen < 0.0;
1938 bool checkSolStatus = false;
1939
1940 if (!is_mip_) {
1941 // Handle some special LP termination reasons.
1942 ASSIGN_OR_RETURN(int const lpstatus, xpress_->GetIntAttr(XPRS_LPSTATUS));
1943 switch (lpstatus) {
1944 case XPRS_LP_UNSTARTED:
1945 break;
1946 case XPRS_LP_OPTIMAL:
1947 break;
1948 case XPRS_LP_INFEAS:
1949 break;
1950 case XPRS_LP_CUTOFF:
1951 // This can happen if you set MIPABSCUTOFF for an LP
1953 isMax, LIMIT_CUTOFF, std::nullopt, "Objective limit (LP_CUTOFF)");
1954 break;
1955 case XPRS_LP_UNFINISHED:
1956 break;
1957 case XPRS_LP_UNBOUNDED:
1958 break;
1960 // This can happen if you set MIPABSCUTOFF for an LP
1962 isMax, LIMIT_CUTOFF, std::nullopt,
1963 "Objective limit (LP_CUTOFF_IN_DUAL)");
1964 case XPRS_LP_UNSOLVED:
1965 break;
1966 case XPRS_LP_NONCONVEX:
1968 "Problem contains quadratic data, which is "
1969 "not convex (XPRS_LP_NONCONVEX)");
1970 }
1971 }
1972
1973 // First check how far the solve actually got.
1974 switch (solvestatus_) {
1977 "Problem solve has not started");
1978 break;
1980 checkSolStatus = true;
1981 break;
1983 switch (stopStatus) {
1986 "Generic error");
1988 // This can actually not happen since despite its name, this is
1989 // not an error but indicates hitting a user defined memory limit
1991 "Memory error");
1994 "License lost");
1997 "Numerical issues");
1998 default:
2000 "Problem solve failed");
2001 }
2002 break;
2004 checkSolStatus = true;
2005 break;
2006 }
2007 if (checkSolStatus) {
2008 // Algorithm finished or was stopped on purpose
2009 switch (solstatus_) {
2011 switch (stopStatus) {
2014 isMax, LIMIT_TIME, best_dual_bound, "Time limit hit");
2015 case XPRS_STOP_CTRLC: // fallthrough
2016 case XPRS_STOP_USER:
2018 isMax, LIMIT_INTERRUPTED, best_dual_bound, "Interrupted");
2021 isMax, LIMIT_NODE, best_dual_bound, "Node limit hit");
2024 isMax, LIMIT_ITERATION, best_dual_bound, "Node limit hit");
2027 isMax, LIMIT_OTHER, best_dual_bound, "Work limit hit");
2029 // Despite its name, MEMORYERROR is not actually an error
2030 // but instead indicates that we hit a user defined memory
2031 // limit.
2033 isMax, LIMIT_MEMORY, best_dual_bound, "Memory limit hit");
2034 default:
2035 if (stop_after_lp_) {
2036 // Stopping after the LP relaxation is treated as special
2037 // node limit
2039 isMax, LIMIT_NODE, best_dual_bound,
2040 "Stopped after LP relaxation");
2041 } else {
2042 return TerminateForReason(isMax,
2044 }
2045 }
2046 break;
2048 return OptimalTerminationProto(best_primal_bound, best_dual_bound);
2049 break;
2051 switch (stopStatus) {
2054 best_primal_bound, best_dual_bound,
2055 "Time limit hit");
2056 case XPRS_STOP_CTRLC: // fallthrough
2057 case XPRS_STOP_USER:
2059 best_primal_bound, best_dual_bound,
2060 "Interrupted");
2063 best_primal_bound, best_dual_bound,
2064 "Node limit hit");
2067 best_primal_bound, best_dual_bound,
2068 "Node limit hit");
2071 best_primal_bound, best_dual_bound,
2072 "Work limit hit");
2074 // Despite its name, MEMORYERROR is not actually an error
2075 // but instead indicates that we hit a user defined memory
2076 // limit.
2078 best_primal_bound, best_dual_bound,
2079 "Memory limit hit");
2080 default:
2081 if (stop_after_lp_) {
2082 // Stopping after the LP relaxation is treated as special
2083 // node limit
2085 isMax, LIMIT_NODE, best_primal_bound, best_dual_bound,
2086 "Stopped after LP relaxation");
2087 } else {
2089 best_primal_bound,
2090 best_dual_bound);
2091 }
2092 }
2093 break;
2096 isMax, isDualFeasible() ? FEASIBILITY_STATUS_FEASIBLE
2099 return UnboundedTerminationProto(isMax);
2100 }
2101 }
2103 "Unknown error");
2104}
2105
2106absl::StatusOr<bool> XpressSolver::Update(const ModelUpdateProto&) {
2107 // Not implemented yet
2108 // We can only update if problem is not in presolved state.
2109 RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed";
2110 return false;
2111}
2112
2113absl::StatusOr<ComputeInfeasibleSubsystemResultProto>
2115 MessageCallback message_callback,
2116 const SolveInterrupter* interrupter) {
2117 RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed";
2118 ScopedSolverContext solveContext(xpress_.get());
2119 RETURN_IF_ERROR(solveContext.AddCallbacks(message_callback, interrupter));
2120 RETURN_IF_ERROR(solveContext.ApplyParameters(parameters, message_callback,
2121 nullptr, nullptr, nullptr));
2122
2123 return absl::UnimplementedError(
2124 "XpressSolver does not compute an infeasible subsystem (yet)");
2125}
2126
2127absl::StatusOr<InvertedBounds> XpressSolver::ListInvertedBounds() const {
2128 InvertedBounds inverted_bounds;
2129 {
2130 ASSIGN_OR_RETURN(const std::vector<double> var_lbs, xpress_->GetVarLb());
2131 ASSIGN_OR_RETURN(const std::vector<double> var_ubs, xpress_->GetVarUb());
2132 for (const auto& [id, index] : variables_map_) {
2133 if (var_lbs[index] > var_ubs[index]) {
2134 inverted_bounds.variables.push_back(id);
2135 }
2136 }
2137 }
2138 // We could have used XPRSgetrhsrange to check if one is negative. However,
2139 // XPRSaddrows ignores the sign of the RHS range and takes the absolute value
2140 // in all cases. So we need to do the following checks on the internal model.
2141 for (const auto& [id, cstr_data] : linear_constraints_map_) {
2142 if (cstr_data.lower_bound > cstr_data.upper_bound) {
2143 inverted_bounds.linear_constraints.push_back(id);
2144 }
2145 }
2146 // Above code have inserted ids in non-stable order.
2147 std::sort(inverted_bounds.variables.begin(), inverted_bounds.variables.end());
2148 std::sort(inverted_bounds.linear_constraints.begin(),
2149 inverted_bounds.linear_constraints.end());
2150 return inverted_bounds;
2151}
2152
2154} // namespace math_opt
2155} // namespace operations_research
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
const ::operations_research::math_opt::VariablesProto & variables() const
Definition model.pb.h:4464
const ::google::protobuf::Map<::int64_t, ::operations_research::math_opt::QuadraticConstraintProto > & quadratic_constraints() const
Definition model.pb.h:4886
const ::operations_research::math_opt::LinearConstraintsProto & linear_constraints() const
Definition model.pb.h:4694
const ::std::string & name() const
Definition model.pb.h:4389
const ::google::protobuf::Map<::int64_t, ::operations_research::math_opt::SosConstraintProto > & sos1_constraints() const
Definition model.pb.h:4950
const ::google::protobuf::Map<::int64_t, ::operations_research::math_opt::SosConstraintProto > & sos2_constraints() const
Definition model.pb.h:4982
const ::google::protobuf::Map<::int64_t, ::operations_research::math_opt::SecondOrderConeConstraintProto > & second_order_cone_constraints() const
Definition model.pb.h:4918
const ::operations_research::math_opt::ObjectiveProto & objective() const
Definition model.pb.h:4563
const ::operations_research::math_opt::SparseDoubleMatrixProto & linear_constraint_matrix() const
Definition model.pb.h:4787
const ::google::protobuf::Map<::int64_t, ::operations_research::math_opt::ObjectiveProto > & auxiliary_objectives() const
Definition model.pb.h:4662
const ::google::protobuf::Map<::int64_t, ::operations_research::math_opt::IndicatorConstraintProto > & indicator_constraints() const
Definition model.pb.h:5014
::operations_research::math_opt::TerminationProto *PROTOBUF_NONNULL mutable_termination()
Definition result.pb.h:2739
::operations_research::math_opt::SolveStatsProto *PROTOBUF_NONNULL mutable_solve_stats()
Definition result.pb.h:2988
const ::operations_research::math_opt::XpressInitializerProto & xpress() const
std::function< void(const std::vector< std::string > &)> MessageCallback
std::function< absl::StatusOr< CallbackResultProto >( const CallbackDataProto &)> Callback
XpressParametersProto_Parameter Parameter
Definition xpress.pb.h:630
absl::StatusOr< SolveResultProto > Solve(const SolveParametersProto &parameters, const ModelSolveParametersProto &model_parameters, MessageCallback message_cb, const CallbackRegistrationProto &callback_registration, Callback cb, const SolveInterrupter *interrupter) override
absl::StatusOr< bool > Update(const ModelUpdateProto &model_update) override
absl::StatusOr< ComputeInfeasibleSubsystemResultProto > ComputeInfeasibleSubsystem(const SolveParametersProto &parameters, MessageCallback message_cb, const SolveInterrupter *interrupter) override
static absl::StatusOr< std::unique_ptr< XpressSolver > > New(const ModelProto &input_model, const SolverInterface::InitArgs &init_args)
static absl::StatusOr< std::unique_ptr< Xpress > > New(absl::string_view model_name)
Definition g_xpress.cc:67
void InsertOrDie(Collection *const collection, const typename Collection::value_type &value)
Definition map_util.h:159
auto & InsertKeyOrDie(Collection *const collection, const typename Collection::value_type::first_type &key)
Definition map_util.h:178
TerminationProto TerminateForReason(const TerminationReasonProto reason, const absl::string_view detail)
absl::Status ModelIsSupported(const ModelProto &model, const SupportedProblemStructures &support_menu, const absl::string_view solver_name)
constexpr SupportedProblemStructures kXpressSupportedStructures
absl::Status ModelSolveParametersAreSupported(const ModelSolveParametersProto &model_parameters, const SupportedProblemStructures &support_menu, const absl::string_view solver_name)
TerminationProto OptimalTerminationProto(const double finite_primal_objective, const double dual_objective, const absl::string_view detail)
TerminationProto FeasibleTerminationProto(const bool is_maximize, const LimitProto limit, const double primal_objective, const std::optional< double > optional_dual_objective, const absl::string_view detail)
std::function< void(const std::vector< std::string > &)> MessageCallback
TerminationProto NoSolutionFoundTerminationProto(const bool is_maximize, const LimitProto limit, const std::optional< double > optional_dual_objective, const absl::string_view detail)
absl::Status CheckRegisteredCallbackEvents(const CallbackRegistrationProto &registration, const absl::flat_hash_set< CallbackEventProto > &supported_events)
SparseVectorView< T > MakeView(absl::Span< const int64_t > ids, const Collection &values)
ElementId< ElementType::kQuadraticConstraint > QuadraticConstraintId
Definition elements.h:83
TerminationProto InfeasibleTerminationProto(bool is_maximize, const FeasibilityStatusProto dual_feasibility_status, const absl::string_view detail)
ElementId< ElementType::kIndicatorConstraint > IndicatorConstraintId
Definition elements.h:84
TerminationProto UnboundedTerminationProto(const bool is_maximize, const absl::string_view detail)
OR-Tools root namespace.
Select next search node to expand Select next item_i to add this new search node to the search Generate a new search node where item_i is not in the knapsack Check validity of this new partial solution(using propagators) - If valid
ClosedInterval::Iterator end(ClosedInterval interval)
ClosedInterval::Iterator begin(ClosedInterval interval)
inline ::absl::StatusOr< absl::Duration > DecodeGoogleApiProto(const google::protobuf::Duration &proto)
Definition protoutil.h:42
inline ::absl::StatusOr< google::protobuf::Duration > EncodeGoogleApiProto(absl::Duration d)
Definition protoutil.h:27
StatusBuilder InvalidArgumentErrorBuilder()
#define MATH_OPT_REGISTER_SOLVER(solver_type, solver_factory)
#define XPRS_SOLAVAILABLE_FEASIBLE
#define XPRS_OBJECTIVE_WEIGHT
#define XPRS_SIMPLEXITER
#define XPRS_STOP_GENERICERROR
#define XPRS_SOLVESTATUS_FAILED
#define XPRS_LPSTATUS
#define XPRS_NAMES_SET
#define XPRS_TYPE_INT
#define XPRS_LP_CUTOFF_IN_DUAL
#define XPRS_LP_INFEAS
#define XPRS_SOLVESTATUS_COMPLETED
#define XPRS_ORIGINALROWS
#define XPRS_ORIGINALSETS
#define XPRS_OBJECTIVE_RELTOL
#define XPRS_NAMES_ROW
#define XPRS_OBJECTIVE_PRIORITY
#define XPRS_RANDOMSEED
#define XPRS_BASIC
#define XPRS_ORIGINALCOLS
#define XPRS_MIPABSSTOP
#define XPRS_STOP_ITERLIMIT
#define XPRS_BARHGMAXRESTARTS
#define XPRS_LP_NONCONVEX
#define XPRS_SOLAVAILABLE_OPTIMAL
#define XPRS_FREE_SUPER
#define XPRS_TYPE_INT64
#define XPRS_ALG_BARRIER
#define XPRS_MIPRELSTOP
struct xo_prob_struct * XPRSprob
#define XPRS_MAXMIPSOL
#define XPRS_PRESOLVEPASSES
#define XPRS_GREATER_EQUAL
#define XPRS_TYPE_STRING
#define XPRS_LP_CUTOFF
#define XPRS_OPTIMIZETYPEUSED
#define XPRS_PRESOLVE
#define XPRS_BESTBOUND
#define XPRS_EQUAL
#define XPRS_LESS_EQUAL
#define XPRS_SOLSTATUS_FEASIBLE
#define XPRS_AT_LOWER
#define XPRS_LP_UNFINISHED
#define XPRS_TYPE_DOUBLE
#define XPRS_STOP_LICENSELOST
#define XPRS_STOP_CTRLC
#define XPRS_NODES
#define XPRS_LP_OPTIMAL
#define XPRS_LP_UNSTARTED
#define XPRS_LPFLAGS
#define XPRS_STOP_NUMERICALERROR
#define XPRS_PRESOLVESTATE
#define XPRS_SOLSTATUS_UNBOUNDED
#define XPRS_RANGE
#define XPRS_NONBINDING
#define XPRS_THREADS
#define XPRS_STOP_NODELIMIT
#define XPRS_CUTSTRATEGY
#define XPRS_BARALG
#define XPRS_SOLSTATUS_INFEASIBLE
#define XPRS_LPITERLIMIT
#define XPRS_STOP_TIMELIMIT
#define XPRS_AT_UPPER
#define XPRS_SOLVESTATUS_STOPPED
#define XPRS_HEUREMPHASIS
#define XPRS_BARITERLIMIT
#define XPRS_STOP_MEMORYERROR
#define XPRS_BARDUALOBJ
#define XPRS_BARITER
#define XPRS_STOP_WORKLIMIT
#define XPRS_ALG_DEFAULT
#define XPRS_ALGORITHM
#define XPRS_LP_UNBOUNDED
#define XPRS_SOLSTATUS_OPTIMAL
#define XPRS_STOP_USER
#define XPRS_TIMELIMIT
#define XPRS_ALG_NETWORK
#define XPRS_STOPSTATUS
#define XPRS_OBJECTIVE_ABSTOL
#define XPRS_BINARY
#define XPRS_SOLAVAILABLE_NOTFOUND
#define XPRS_ALG_PRIMAL
#define XPRS_SOLVESTATUS_UNSTARTED
#define XPRS_MAXNODE
#define XPRS_MIPABSCUTOFF
#define XPRS_OBJSENSE
#define XPRS_NAMES_COLUMN
#define XPRS_OBJVAL
#define XPRS_SOLSTATUS_NOTFOUND
#define XPRS_SOLVEDOBJS
#define XPRS_ALG_DUAL
#define XPRS_LP_UNSOLVED
#define XPRS_CONTINUOUS
#define XPRS_INTEGER
#define DEFINE_SCOPED_CB(CB_NAME, ORTOOLS_CB, CB_RET_TYPE, ARGS)