From 59b64cb2c0c6ff0bd585ebf696dc40069552ed3e Mon Sep 17 00:00:00 2001 From: Daniel Junglas Date: Thu, 18 Jun 2026 15:00:39 +0200 Subject: [PATCH] Implement callback support for Xpress We implement full support for the callback in Xpress. The Xpress implementation supports all events and all features of the ortools callbacks. We also added a "solution source" attribute to the callback data in case of the MIP_SOLUTION event because we think this can be useful. In general the Xpress callback is much richer than what the ortools callback offers but we did not want to add too many things that could be Xpress-specific and might not be available with other solvers. Co-authored-by: Francesco Cavaliere --- ortools/math_opt/callback.proto | 46 +- ortools/math_opt/cpp/callback.cc | 23 + ortools/math_opt/cpp/callback.h | 57 +- .../math_opt/solver_tests/callback_tests.cc | 97 +- .../math_opt/solver_tests/callback_tests.h | 15 +- .../solver_tests/logical_constraint_tests.cc | 34 + .../solver_tests/multi_objective_tests.cc | 26 +- .../solver_tests/second_order_cone_tests.cc | 11 + ortools/math_opt/solver_tests/test_models.cc | 18 + ortools/math_opt/solvers/xpress/g_xpress.cc | 100 +- ortools/math_opt/solvers/xpress/g_xpress.h | 47 +- ortools/math_opt/solvers/xpress_solver.cc | 1202 +++++++++++++++-- ortools/math_opt/solvers/xpress_solver.h | 4 + .../math_opt/solvers/xpress_solver_test.cc | 61 +- .../third_party_solvers/xpress_environment.cc | 48 + .../third_party_solvers/xpress_environment.h | 28 + 16 files changed, 1619 insertions(+), 198 deletions(-) diff --git a/ortools/math_opt/callback.proto b/ortools/math_opt/callback.proto index bdbbeab4550..76b5ce3d85c 100644 --- a/ortools/math_opt/callback.proto +++ b/ortools/math_opt/callback.proto @@ -36,28 +36,32 @@ enum CallbackEventProto { // The solver is currently running presolve. // - // This event is supported by SOLVER_TYPE_GUROBI only. + // This event is supported by SOLVER_TYPE_GUROBI and SOLVER_TYPE_XPRESS only. CALLBACK_EVENT_PRESOLVE = 1; // The solver is currently running the simplex method. // - // This event is supported by SOLVER_TYPE_GUROBI only. + // This event is supported by SOLVER_TYPE_GUROBI and SOLVER_TYPE_XPRESS only. CALLBACK_EVENT_SIMPLEX = 2; // The solver is in the MIP loop (called periodically before starting a new // node). Useful for early termination. Note that this event does not provide // information on LP relaxations nor about new incumbent solutions. // - // This event is fully supported for MIP models by SOLVER_TYPE_GUROBI only. If + // This event is fully supported for MIP models by SOLVER_TYPE_GUROBI and + // SOLVER_TYPE_XPRESS only. If // used with SOLVER_TYPE_CP_SAT, it is called when the dual bound is improved. CALLBACK_EVENT_MIP = 3; // Called every time a new MIP incumbent is found. // - // This event is fully supported for MIP models by SOLVER_TYPE_GUROBI. + // This event is fully supported for MIP models by SOLVER_TYPE_GUROBI and + // SOLVER_TYPE_XPRESS. // SOLVER_TYPE_CP_SAT has partial support: you can view the solutions and // request termination, but you cannot add lazy constraints. Other solvers // don't support this event. + // It is solver specific whether terminating from this event still collects + // the solution for which the event was triggered or not. CALLBACK_EVENT_MIP_SOLUTION = 4; // Called inside a MIP node. Note that there is no guarantee that the @@ -68,15 +72,32 @@ enum CallbackEventProto { // being called and/or adding cuts at this event, the behavior is solver // specific. // - // This event is supported for MIP models by SOLVER_TYPE_GUROBI only. + // This event is supported for MIP models by SOLVER_TYPE_GUROBI and + // SOLVER_TYPE_XPRESS only. CALLBACK_EVENT_MIP_NODE = 5; // Called in each iterate of an interior point/barrier method. // - // This event is supported for SOLVER_TYPE_GUROBI only. + // This event is supported for SOLVER_TYPE_GUROBI and SOLVER_TYPE_XPRESS only. CALLBACK_EVENT_BARRIER = 6; } +// Where a solution for a CALLBACK_EVENT_MIP_SOLUTION came from. +enum CallbackSolutionSourceProto { + CALLBACK_SOLUTION_SOURCE_UNSPECIFIED = 0; + + // The solution came from an LP relaxation that happened to be integer + // feasible. + CALLBACK_SOLUTION_SOURCE_INTEGRAL = 1; + + // The solution came from a heuristic. + CALLBACK_SOLUTION_SOURCE_HEURISTIC = 2; + + // The solution came from a solution vector provided by the user. + // This may include solutions the solver had to "repair". + CALLBACK_SOLUTION_SOURCE_USER = 3; +}; + // The callback function input data. // Note that depending on the event, some information might be unavailable. message CallbackDataProto { @@ -139,6 +160,10 @@ message CallbackDataProto { optional int64 simplex_iterations = 5; optional int32 number_of_solutions_found = 6; optional int32 cutting_planes_in_lp = 7; + // Only for CALLBACK_EVENT_MIP_SOLUTION, specifies where the solution + // came from. See CallbackSolutionSource. + // Only implemented for SOLVER_TYPE_XPRESS. + optional int32 solution_source = 8; } MipStats mip_stats = 7; } @@ -179,16 +204,19 @@ message CallbackResultProto { // Dynamically generated linear constraints to add to the MIP. See // GeneratedLinearConstraint::is_lazy for details. + // All constraints must be globally valid (and not only valid for the subtree + // rooted at the search tree node for which the event was triggered). repeated GeneratedLinearConstraint cuts = 4; // Use only for CALLBACK_EVENT_MIP_NODE or CALLBACK_EVENT_MIP_SOLUTION. // - // Note that some solvers (e.g. Gurobi) support partially-defined solutions. + // Note that some solvers (e.g. Gurobi or Xpress) support partially-defined + // solutions. // The most common use case is to specify a value for each variable in the // model. If a variable is not present in the primal solution, its value is // taken to be undefined, and is up to the underlying solver to deal with it. - // For example, Gurobi will try to solve a Sub-MIP to get a fully feasible - // solution if necessary. + // For example, Gurobi or Xpress will try to solve a Sub-MIP to get a fully + // feasible solution if necessary. repeated SparseDoubleVectorProto suggested_solutions = 5; } diff --git a/ortools/math_opt/cpp/callback.cc b/ortools/math_opt/cpp/callback.cc index c01e349fe9c..4059a77e535 100644 --- a/ortools/math_opt/cpp/callback.cc +++ b/ortools/math_opt/cpp/callback.cc @@ -66,6 +66,29 @@ absl::Span Enum::AllValues() { return absl::MakeConstSpan(kCallbackEventValues); } +std::optional Enum::ToOptString( + CallbackSolutionSource value) { + switch (value) { + case CallbackSolutionSource::kIntegral: + return "integral"; + case CallbackSolutionSource::kHeuristic: + return "heuristic"; + case CallbackSolutionSource::kUser: + return "user"; + } + return std::nullopt; +} + +absl::Span +Enum::AllValues() { + static constexpr CallbackSolutionSource kCallbackSolutionSourceValues[] = { + CallbackSolutionSource::kIntegral, + CallbackSolutionSource::kHeuristic, + CallbackSolutionSource::kUser, + }; + return absl::MakeConstSpan(kCallbackSolutionSourceValues); +} + CallbackData::CallbackData(const CallbackEvent event, const absl::Duration runtime) : event(event), runtime(runtime) {} diff --git a/ortools/math_opt/cpp/callback.h b/ortools/math_opt/cpp/callback.h index 1671b32afd1..f2c79035d80 100644 --- a/ortools/math_opt/cpp/callback.h +++ b/ortools/math_opt/cpp/callback.h @@ -45,7 +45,7 @@ // std::vector> solutions; // auto cb = [&solutions](const CallbackData& cb_data) { // // NOTE: this assumes the callback is always called from the same thread. -// // Gurobi always does this, multi-threaded SCIP does not. +// // Gurobi always does this, multi-threaded SCIP or Xpress do not. // solutions.push_back(*cb_data.solution); // return CallbackResult(); // }; @@ -62,8 +62,8 @@ // CHECK fail). Some solvers do not support callbacks or certain events, in this // case the callback is ignored. TODO(b/180617976): change this behavior. // -// Some solvers may call callback from multiple threads (SCIP will, Gurobi will -// not). You should either solve with one thread (see +// Some solvers may call callback from multiple threads (SCIP and Xpress will, +// Gurobi will not). You should either solve with one thread (see // solver_parameters.threads), write a threadsafe callback, or consult // the documentation of your underlying solver. #ifndef ORTOOLS_MATH_OPT_CPP_CALLBACK_H_ @@ -97,26 +97,30 @@ using Callback = std::function; enum class CallbackEvent { // The solver is currently running presolve. // - // This event is supported for SolverType::kGurobi only. + // This event is supported for SolverType::kGurobi or SolverType::kXpress + // only. kPresolve = CALLBACK_EVENT_PRESOLVE, // The solver is currently running the simplex method. // - // This event is supported for SolverType::kGurobi only. + // This event is supported for SolverType::kGurobi or SolverType::kXpress + // only. kSimplex = CALLBACK_EVENT_SIMPLEX, // The solver is in the MIP loop (called periodically before starting a new // node). Useful for early termination. Note that this event does not provide // information on LP relaxations nor about new incumbent solutions. // - // This event is fully supported for MIP models with SolverType::kGurobi only. + // This event is fully supported for MIP models with SolverType::kGurobi or + // SolverType::kXpress only. // If used with SolverType::kCpSat, it is called when the dual bound is // improved. kMip = CALLBACK_EVENT_MIP, // Called every time a new MIP incumbent is found. // - // This event is fully supported for MIP models by SolverType::kGurobi. + // This event is fully supported for MIP models by SolverType::kGurobi or + // SolverType::kXpress only. // SolverType::kCpSat has partial support: you can view the solutions and // request termination, but you cannot add lazy constraints. Other solvers // don't support this event. @@ -130,17 +134,37 @@ enum class CallbackEvent { // being called and/or adding cuts at this event, the behavior is solver // specific. // - // This event is supported for MIP models with SolverType::kGurobi only. + // This event is supported for MIP models with SolverType::kGurobi or + // SolverType::kXpress only. + // For Xpress disabling cuts will prevent this event. To disable cuts + // and still get this event called for Xpress, disable cuts by setting + // COVERCUTS, GOMCUTS, TREECOVERCUTS, TREEGOMCUTS to 0. kMipNode = CALLBACK_EVENT_MIP_NODE, // Called in each iterate of an interior point/barrier method. // - // This event is supported for SolverType::kGurobi only. + // This event is supported for SolverType::kGurobi or SolverType::kXpress + // only. kBarrier = CALLBACK_EVENT_BARRIER, }; MATH_OPT_DEFINE_ENUM(CallbackEvent, CALLBACK_EVENT_UNSPECIFIED); +// Where a solution for a CALLBACK_EVENT_MIP_SOLUTION came from. +enum class CallbackSolutionSource { + // The solution came from an LP relaxation that happened to be integer + // feasible. + kIntegral = CALLBACK_SOLUTION_SOURCE_INTEGRAL, + // The solution came from a heuristic. + kHeuristic = CALLBACK_SOLUTION_SOURCE_HEURISTIC, + // The solution came from a solution vector provided by the user. + // This may include solutions the solver had to "repair". + kUser = CALLBACK_SOLUTION_SOURCE_USER, +}; + +MATH_OPT_DEFINE_ENUM(CallbackSolutionSource, + CALLBACK_SOLUTION_SOURCE_UNSPECIFIED); + // Provided with a callback at the start of a Solve() to inform the solver: // * what information the callback needs, // * how the callback might alter the solve process. @@ -255,13 +279,17 @@ struct CallbackResult { }; // Adds a "user cut," a linear constraint that excludes the current LP - // solution but does not cut off any integer points. Use only for - // CallbackEvent::kMipNode. + // solution but does not cut off any integer points. + // The constraint must be globally valid (and not only valid for the subtree + // rooted at the MIP search node at which the event was triggered). + // Use only for CallbackEvent::kMipNode. void AddUserCut(BoundedLinearExpression linear_constraint) { new_constraints.push_back({std::move(linear_constraint), false}); } // Adds a "lazy constraint," a linear constraint that excludes integer points. + // The constraint must be globally valid (and not only valid for the subtree + // rooted at the MIP search node at which the event was triggered). // Use only for CallbackEvent::kMipNode and CallbackEvent::kMipSolution. void AddLazyConstraint(BoundedLinearExpression linear_constraint) { new_constraints.push_back({std::move(linear_constraint), true}); @@ -299,12 +327,13 @@ struct CallbackResult { // The user cuts and lazy constraints added. Prefer AddUserCut() and // AddLazyConstraint() to modifying this directly. + // All constraints are assumed to be globally valid. std::vector new_constraints; // A list of solutions (or partially defined solutions) to suggest to the - // solver. Some solvers (e.g. gurobi) will try and convert a partial solution - // into a full solution. Use only for CallbackEvent::kMipNode or - // CallbackEvent::kMipSolution. + // solver. Some solvers (e.g. gurobi or Xpress) will try and convert a + // partial solution into a full solution. Use only for + // CallbackEvent::kMipNode or CallbackEvent::kMipSolution. std::vector> suggested_solutions; }; diff --git a/ortools/math_opt/solver_tests/callback_tests.cc b/ortools/math_opt/solver_tests/callback_tests.cc index cdb5eba3e82..c22c3ae1d6a 100644 --- a/ortools/math_opt/solver_tests/callback_tests.cc +++ b/ortools/math_opt/solver_tests/callback_tests.cc @@ -79,14 +79,16 @@ CallbackTestParams::CallbackTestParams( const bool add_lazy_constraints, const bool add_cuts, absl::flat_hash_set supported_events, std::optional all_solutions, - std::optional reaches_cut_callback) + std::optional reaches_cut_callback, + std::optional solve_parameters) : solver_type(solver_type), model_class(model_class), add_lazy_constraints(add_lazy_constraints), add_cuts(add_cuts), supported_events(std::move(supported_events)), all_solutions(std::move(all_solutions)), - reaches_cut_callback(std::move(reaches_cut_callback)) {} + reaches_cut_callback(std::move(reaches_cut_callback)), + solve_parameters(std::move(solve_parameters)) {} bool CallbackTestParams::uses_integer_variables() const { return model_class == TestModelClass::kIp; @@ -139,6 +141,8 @@ using ::testing::AnyOf; using ::testing::Each; using ::testing::HasSubstr; using ::testing::IsEmpty; +using ::testing::IsFalse; +using ::testing::IsTrue; using ::testing::Pair; using ::testing::UnorderedElementsAre; using ::testing::status::IsOkAndHolds; @@ -305,7 +309,8 @@ TEST_P(CallbackTest, EventPresolve) { model.AddVariable(0, 3.0, GetParam().uses_integer_variables(), "y"); model.AddLinearConstraint(y <= 1.0); model.Maximize(2.0 * x + y); - SolveArguments args = { + SolveArguments args{ + .parameters = GetParam().solve_parameters.value_or(SolveParameters()), .callback_registration = {.events = {CallbackEvent::kPresolve}}}; absl::Mutex mutex; std::optional last_presolve_data; // Guarded by mutex. @@ -335,17 +340,27 @@ TEST_P(CallbackTest, EventSimplex) { Variable x3 = model.AddVariable(0, 4.0, false, "x3"); model.Maximize(x1 - x2 + x3); - SolveArguments args; + SolveArguments args = { + .parameters = GetParam().solve_parameters.value_or(SolveParameters())}; args.parameters.presolve = Emphasis::kOff; args.parameters.lp_algorithm = LPAlgorithm::kPrimalSimplex; // Note: we solve and then change the objective to so that on our second // solve, we know the starting basis. It would be simpler to set the starting // basis, once this is supported. + Basis startingBasis; + // Xpress currently is a non-incremental solver, so we must explicitly + // transfer the starting basis. + // Once Xpress supports incremental solves, we can remove that. + bool useStartingBasis = GetParam().solver_type == SolverType::kXpress; + ASSERT_OK_AND_ASSIGN(const std::unique_ptr solver, NewIncrementalSolver(&model, GetParam().solver_type)); { ASSERT_OK_AND_ASSIGN(const SolveResult result, solver->Solve(args)); ASSERT_THAT(result, IsOptimal(6.0)); + + ASSERT_THAT(result.solutions[0].basis.has_value(), IsTrue()); + startingBasis = result.solutions[0].basis.value(); } // We know that from the previous optimal solution, we should take 3 pivots. @@ -359,6 +374,9 @@ TEST_P(CallbackTest, EventSimplex) { stats.push_back(callback_data.simplex_stats); return CallbackResult(); }; + if (useStartingBasis) { + args.model_parameters.initial_basis = startingBasis; + } ASSERT_OK_AND_ASSIGN(const SolveResult result, solver->Solve(args)); ASSERT_THAT(result, IsOptimal(3.0)); // It should take at least 3 pivots to move from (2, 0, 4) to (0, 3, 0) @@ -370,7 +388,13 @@ TEST_P(CallbackTest, EventSimplex) { } // We should begin dual infeasible. EXPECT_EQ(stats[0].iteration_count(), 0); - EXPECT_GT(stats[0].dual_infeasibility(), 0.0); + if (GetParam().solver_type == SolverType::kXpress) { + // Xpress does not report dual infeasibiltiy + /** TODO: Instead report NUMBER of dual infeasibilities and test that. */ + ASSERT_THAT(stats[0].has_dual_infeasibility(), IsFalse()); + } else { + EXPECT_GT(stats[0].dual_infeasibility(), 0.0); + } EXPECT_NEAR(stats[0].objective_value(), -6.0, kTolerance); EXPECT_GE(stats.back().iteration_count(), 3); @@ -387,9 +411,20 @@ TEST_P(CallbackTest, EventBarrier) { const std::unique_ptr model = SmallModel(GetParam().uses_integer_variables()); - SolveArguments args; + double optimalObjective = GetParam().uses_integer_variables() ? 9.0 : 12.0; + SolveArguments args = { + .parameters = GetParam().solve_parameters.value_or(SolveParameters())}; args.parameters.presolve = Emphasis::kOff; args.parameters.lp_algorithm = LPAlgorithm::kBarrier; + // There is a flaw in this test: + // According to the documentation, lp_algorithm is only for LP, so it + // does not/should not apply if GetParam().integer_variables is set and the + // test fails. It seems Xpress is the only solver that invokes this test + // with GetParam().integer_variables=true, so we do some extra stuff here. + if (GetParam().uses_integer_variables() && + GetParam().solver_type == SolverType::kXpress) { + args.parameters.xpress.param_values["LPFLAGS"] = "4"; + } args.callback_registration.events.insert(CallbackEvent::kBarrier); absl::Mutex mutex; std::vector stats; // Guarded-by mutex. @@ -400,7 +435,7 @@ TEST_P(CallbackTest, EventBarrier) { }; ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(*model, GetParam().solver_type, args)); - ASSERT_THAT(result, IsOptimal(12.0)); + ASSERT_THAT(result, IsOptimal(optimalObjective)); // TODO(b/196035470): test more data from the stats. ASSERT_GE(stats.size(), 1); @@ -423,6 +458,7 @@ TEST_P(CallbackTest, EventSolutionAlwaysCalled) { model.Maximize(x + 2 * y); SolveArguments args = { + .parameters = GetParam().solve_parameters.value_or(SolveParameters()), .callback_registration = {.events = {CallbackEvent::kMipSolution}}}; absl::Mutex mutex; std::atomic cb_called = false; @@ -465,17 +501,35 @@ TEST_P(CallbackTest, EventSolutionInterrupt) { // A model where we will not prove optimality immediately. const std::unique_ptr model = DenseIndependentSet(/*integer=*/true); - const SolveArguments args = { + SolveArguments args = { // Don't prove optimality in presolve. - .parameters = {.presolve = Emphasis::kOff}, + .parameters = GetParam().solve_parameters.value_or(SolveParameters()), .callback_registration = {.events = {CallbackEvent::kMipSolution}}, .callback = [&](const CallbackData& /*callback_data*/) { return CallbackResult{.terminate = true}; }}; + args.parameters.presolve = Emphasis::kOff; ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(*model, GetParam().solver_type, args)); - EXPECT_THAT(result, TerminatesWithReasonFeasible(Limit::kInterrupted)); - EXPECT_TRUE(result.has_primal_feasible_solution()); + // When terminating from a kMipSolution event it is solver dependent + // whether the solver accepts the solution. + // A kMipSolution event allows the user to do two things: + // 1. Reject the candidate solution + // 2. Terminate the solve. + // Due to 1, the candidate solution cannot be marked as incumbent before + // the callback is invoked. + // After return from the callback, it is solver-dependent whether the + // termination request or accepting the solution is handled first. + // Xpress first handles the termination request (and thus discards the + // feasible solution), other solvers do it the other way around. + // If you want to stop right after the first solution in Xpress, use the + // MAXMIPSOL control. + if (GetParam().solver_type == SolverType::kXpress) { + EXPECT_THAT(result, TerminatesWith(TerminationReason::kNoSolutionFound)); + } else { + EXPECT_THAT(result, TerminatesWithReasonFeasible(Limit::kInterrupted)); + EXPECT_TRUE(result.has_primal_feasible_solution()); + } } TEST_P(CallbackTest, EventSolutionCalledMoreThanOnce) { @@ -557,6 +611,7 @@ TEST_P(CallbackTest, EventSolutionLazyConstraint) { model.Maximize(x + 2 * y); SolveArguments args = { + .parameters = GetParam().solve_parameters.value_or(SolveParameters()), .callback_registration = {.events = {CallbackEvent::kMipSolution}, .add_lazy_constraints = true}}; // Add the constraint x+y <= 1 if it is violated by the current solution. @@ -607,6 +662,7 @@ TEST_P(CallbackTest, EventSolutionLazyConstraintWithLinearConstraints) { model.AddLinearConstraint(x + y + z >= 1.0); SolveArguments args = { + .parameters = GetParam().solve_parameters.value_or(SolveParameters()), .callback_registration = {.events = {CallbackEvent::kMipSolution}, .add_lazy_constraints = true}}; // Add the constraint x+y <= 1 if it is violated by the current solution. @@ -651,9 +707,11 @@ TEST_P(CallbackTest, EventSolutionFilter) { model.AddLinearConstraint(x + y <= 1); model.Maximize(x + 2 * y); - SolveArguments args = {.callback_registration = { - .events = {CallbackEvent::kMipSolution}, - .mip_solution_filter = MakeKeepKeysFilter({y})}}; + SolveArguments args = { + .parameters = GetParam().solve_parameters.value_or(SolveParameters()), + .callback_registration = { + .events = {CallbackEvent::kMipSolution}, + .mip_solution_filter = MakeKeepKeysFilter({y})}}; absl::Mutex mutex; std::atomic cb_called = false; std::atomic cb_called_on_optimal = false; @@ -781,9 +839,11 @@ TEST_P(CallbackTest, EventNodeFilter) { const Variable x0 = variables[0]; const Variable x2 = variables[2]; - SolveArguments args = {.callback_registration = { - .events = {CallbackEvent::kMipNode}, - .mip_node_filter = MakeKeepKeysFilter({x0, x2})}}; + SolveArguments args = { + .parameters = GetParam().solve_parameters.value_or(SolveParameters()), + .callback_registration = { + .events = {CallbackEvent::kMipNode}, + .mip_node_filter = MakeKeepKeysFilter({x0, x2})}}; absl::Mutex mutex; std::vector> solutions; int empty_solution_count = 0; @@ -825,6 +885,7 @@ TEST_P(CallbackTest, EventMip) { std::atomic best_dual_bound = -std::numeric_limits::infinity(); const SolveArguments args = { + .parameters = GetParam().solve_parameters.value_or(SolveParameters()), .callback_registration = {.events = {CallbackEvent::kMip}}, .callback = [&](const CallbackData& callback_data) { CHECK_EQ(callback_data.event, CallbackEvent::kMip); @@ -865,6 +926,7 @@ TEST_P(CallbackTest, StatusPropagation) { model.Maximize(x + 2 * y); SolveArguments args = { + .parameters = GetParam().solve_parameters.value_or(SolveParameters()), .callback_registration = {.events = {CallbackEvent::kMipSolution}, .add_lazy_constraints = true}}; absl::Mutex mutex; @@ -895,6 +957,7 @@ TEST_P(CallbackTest, UnsupportedEvents) { SCOPED_TRACE(absl::StrCat("event: ", EnumToString(event))); const SolveArguments args = { + .parameters = GetParam().solve_parameters.value_or(SolveParameters()), .callback_registration = {.events = {event}}, .callback = [](const CallbackData&) { return CallbackResult{}; }}; diff --git a/ortools/math_opt/solver_tests/callback_tests.h b/ortools/math_opt/solver_tests/callback_tests.h index 70b7506e29b..740f65a8087 100644 --- a/ortools/math_opt/solver_tests/callback_tests.h +++ b/ortools/math_opt/solver_tests/callback_tests.h @@ -78,11 +78,13 @@ class MessageCallbackTest // Parameters for CallbackTest. struct CallbackTestParams { - CallbackTestParams(SolverType solver_type, TestModelClass model_class, - bool add_lazy_constraints, bool add_cuts, - absl::flat_hash_set supported_events, - std::optional all_solutions, - std::optional reaches_cut_callback); + CallbackTestParams( + SolverType solver_type, TestModelClass model_class, + bool add_lazy_constraints, bool add_cuts, + absl::flat_hash_set supported_events, + std::optional all_solutions, + std::optional reaches_cut_callback, + std::optional solve_parameters = std::nullopt); // The solver to test. SolverType solver_type; @@ -111,6 +113,9 @@ struct CallbackTestParams { // Returns true if model_class uses integer variables (i.e., is `kIp`). bool uses_integer_variables() const; + // Parameters that are set for every solve. + std::optional solve_parameters; + friend std::ostream& operator<<(std::ostream& out, const CallbackTestParams& params); }; diff --git a/ortools/math_opt/solver_tests/logical_constraint_tests.cc b/ortools/math_opt/solver_tests/logical_constraint_tests.cc index 886558e5846..453ea2fdec3 100644 --- a/ortools/math_opt/solver_tests/logical_constraint_tests.cc +++ b/ortools/math_opt/solver_tests/logical_constraint_tests.cc @@ -208,6 +208,10 @@ TEST_P(SimpleLogicalConstraintTest, Sos1WithExpressions) { if (!GetParam().supports_sos1) { GTEST_SKIP() << no_sos1_support_message; } + if (GetParam().solver_type == SolverType::kXpress) { + // see https://github.com/google/or-tools/issues/5084 + GTEST_SKIP() << "skipped since SOS on expressions are not supported"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -229,6 +233,10 @@ TEST_P(SimpleLogicalConstraintTest, Sos2WithExpressions) { if (!GetParam().supports_sos2) { GTEST_SKIP() << no_sos2_support_message; } + if (GetParam().solver_type == SolverType::kXpress) { + // see https://github.com/google/or-tools/issues/5084 + GTEST_SKIP() << "skipped since SOS on expressions are not supported"; + } Model model; const Variable x = model.AddContinuousVariable(-1.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(-1.0, 1.0, "y"); @@ -251,6 +259,11 @@ TEST_P(SimpleLogicalConstraintTest, Sos1VariableInMultipleTerms) { if (!GetParam().supports_sos1) { GTEST_SKIP() << no_sos2_support_message; } + if (GetParam().solver_type == SolverType::kXpress) { + // Xpress does not support SOSs like { x, x } (see also xpress_solver.cc). + GTEST_SKIP() << "Xpress does not support the same variable appearing " + "multiple times in an SOS"; + } Model model; const Variable x = model.AddContinuousVariable(-1.0, 1.0, "x"); model.Minimize(x); @@ -271,6 +284,11 @@ TEST_P(SimpleLogicalConstraintTest, Sos2VariableInMultipleTerms) { if (!GetParam().supports_sos2) { GTEST_SKIP() << no_sos2_support_message; } + if (GetParam().solver_type == SolverType::kXpress) { + // Xpress does not support SOSs like { x, x } (see also xpress_solver.cc). + GTEST_SKIP() << "Xpress does not support the same variable appearing " + "multiple times in an SOS"; + } Model model; const Variable x = model.AddContinuousVariable(-1.0, 1.0, "x"); model.Minimize(x); @@ -405,6 +423,10 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesSos1Constraint) { if (!GetParam().supports_sos1) { GTEST_SKIP() << no_sos1_support_message; } + if (GetParam().solver_type == SolverType::kXpress) { + // see https://github.com/google/or-tools/issues/5084 + GTEST_SKIP() << "skipped since SOS on expressions are not supported"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -449,6 +471,10 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesSos2Constraint) { if (!GetParam().supports_sos2) { GTEST_SKIP() << no_sos1_support_message; } + if (GetParam().solver_type == SolverType::kXpress) { + // see https://github.com/google/or-tools/issues/5084 + GTEST_SKIP() << "skipped since SOS on expressions are not supported"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -498,6 +524,10 @@ TEST_P(IncrementalLogicalConstraintTest, if (!GetParam().supports_sos1) { GTEST_SKIP() << no_sos1_support_message; } + if (GetParam().solver_type == SolverType::kXpress) { + // see https://github.com/google/or-tools/issues/5084 + GTEST_SKIP() << "skipped since SOS on expressions are not supported"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -546,6 +576,10 @@ TEST_P(IncrementalLogicalConstraintTest, if (!GetParam().supports_sos2) { GTEST_SKIP() << no_sos2_support_message; } + if (GetParam().solver_type == SolverType::kXpress) { + // see https://github.com/google/or-tools/issues/5084 + GTEST_SKIP() << "skipped since SOS on expressions are not supported"; + } 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/solver_tests/multi_objective_tests.cc b/ortools/math_opt/solver_tests/multi_objective_tests.cc index df205f36d6d..d8661e508b2 100644 --- a/ortools/math_opt/solver_tests/multi_objective_tests.cc +++ b/ortools/math_opt/solver_tests/multi_objective_tests.cc @@ -385,11 +385,6 @@ TEST_P(SimpleMultiObjectiveTest, .objective_parameters = { {aux_obj, {.time_limit = absl::Milliseconds(1)}}}}}; const auto result = Solve(*model, GetParam().solver_type, args); - if (GetParam().solver_type == SolverType::kXpress) { - EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, - HasSubstr("per-objective time limits"))); - return; - } if (!GetParam().supports_auxiliary_objectives) { EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("multiple objectives"))); @@ -410,6 +405,17 @@ TEST_P(SimpleMultiObjectiveTest, if (!GetParam().supports_integer_variables) { GTEST_SKIP() << kNoIntegerVariableSupportMessage; } + if (GetParam().solver_type == SolverType::kXpress) { + // Xpress behaves very differently from what this test expects: + // If the first objective hits a time limit without finding a solution + // (this is what happens here) then Xpress leaves the first solution + // unfixed and continues with the second solution. Since the second + // solution solves to optimality, Xpress will report "optimal" in the end. + // The rationale for this behavior is that a time limit on everything but + // the first objective is interpreted as "if you can solve this then great, + // if not, move on". + GTEST_SKIP() << "skipped since Xpress behaves differently in this context"; + } ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, Load23588MiplibInstance()); model->AddMaximizationObjective(0, /*priority=*/1); @@ -419,11 +425,6 @@ TEST_P(SimpleMultiObjectiveTest, .objective_parameters = {{model->primary_objective(), {.time_limit = absl::Milliseconds(1)}}}}}; const auto result = Solve(*model, GetParam().solver_type, args); - if (GetParam().solver_type == SolverType::kXpress) { - EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, - HasSubstr("per-objective time limits"))); - return; - } if (!GetParam().supports_auxiliary_objectives) { EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("multiple objectives"))); @@ -485,11 +486,6 @@ TEST_P(SimpleMultiObjectiveTest, {.time_limit = absl::Seconds(10)}}}}}; args.parameters.time_limit = absl::Milliseconds(1); const auto result = Solve(*model, GetParam().solver_type, args); - if (GetParam().solver_type == SolverType::kXpress) { - EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument, - HasSubstr("per-objective time limits"))); - return; - } ASSERT_OK(result); EXPECT_THAT(*result, TerminatesWithLimit(Limit::kTime)); // Solvers do not stop very precisely, use a large number to avoid flaky 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 68fd24a034b..5faecafaebb 100644 --- a/ortools/math_opt/solver_tests/second_order_cone_tests.cc +++ b/ortools/math_opt/solver_tests/second_order_cone_tests.cc @@ -335,6 +335,10 @@ TEST_P(IncrementalSecondOrderConeTest, if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } + if (GetParam().solver_type == SolverType::kXpress) { + GTEST_SKIP() + << "Xpress does not support second order cone with general expressions"; + } Model model; const Variable x = model.AddContinuousVariable(0.0, 2.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -377,6 +381,9 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesVariableThatIsAnArgument) { if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } + if (GetParam().solver_type == SolverType::kXpress) { + GTEST_SKIP() << "Xpress does not support second order cone with constants"; + } Model model; const Variable x = model.AddContinuousVariable(1.0, 1.0, "x"); const Variable y = model.AddContinuousVariable(0.0, 1.0, "y"); @@ -419,6 +426,10 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesVariableInAnArgument) { if (!GetParam().supports_soc_constraints) { GTEST_SKIP() << kNoSocSupportMessage; } + if (GetParam().solver_type == SolverType::kXpress) { + GTEST_SKIP() + << "Xpress does not support second order cone with general expressions"; + } 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/solver_tests/test_models.cc b/ortools/math_opt/solver_tests/test_models.cc index c403a2c7882..c013231d3e3 100644 --- a/ortools/math_opt/solver_tests/test_models.cc +++ b/ortools/math_opt/solver_tests/test_models.cc @@ -95,6 +95,24 @@ std::unique_ptr NontrivialModel(const TestModelClass model_class, return model; } +// Create model +// Maximize +// obj: 3x_1 + 2y_1 + 3x_2 + 2y_2 + 3x_3 + 2y_3 +// Subject To +// c1: x_1 + y_1 <= 1.5 +// c2: x_2 + y_2 <= 1.5 +// c3: x_3 + y_3 <= 1.5 +// Bounds +// 0 <= x_1 <= 1 +// 0 <= y_1 <= 1 +// 0 <= x_2 <= 1 +// 0 <= y_2 <= 1 +// 0 <= x_3 <= 1 +// 0 <= y_3 <= 1 +// End +// If `integer` is true then all variables are marked integer. +// integer=false: optimal solution: 12.0 (x_i=1.0, y_i=0.5) +// integer=true: optimal solution: 9.0 (x_i=1.0, y_i=0.0) std::unique_ptr SmallModel(const bool integer) { auto model = std::make_unique("small_model"); const Variable x_1 = model->AddVariable(0.0, 1.0, integer, "x_1"); diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 6422b190afb..c99cbb398d3 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -47,21 +47,38 @@ T* forwardSpan(std::optional> const& span) { constexpr int kXpressOk = 0; -absl::Status Xpress::ToStatus(const int xprs_err, - const absl::StatusCode code) const { +std::string Xpress::GetLastError(XPRSprob const& prob, int xprs_err) { + char errmsg[512]; + if (XPRSgetlasterror(prob, errmsg) != kXpressOk) { + std::snprintf(errmsg, sizeof(errmsg), + "Xpress error code: %d (message could not be fetched)", + xprs_err); + } + return errmsg; +} + +absl::Status Xpress::ToStatus(XPRSprob prob, int xprs_err, + absl::StatusCode code) { if (xprs_err == kXpressOk) { return absl::OkStatus(); } - char errmsg[512]; - int status = XPRSgetlasterror(xpress_model_, errmsg); - if (status == kXpressOk) { - return absl::StatusBuilder(code) - << "Xpress error code: " << xprs_err << ", message: " << errmsg; + if (prob) { + char errmsg[512]; + int status = XPRSgetlasterror(prob, errmsg); + if (status == kXpressOk) { + return absl::StatusBuilder(code) + << "Xpress error code: " << xprs_err << ", message: " << errmsg; + } } return absl::StatusBuilder(code) << "Xpress error code: " << xprs_err << " (message could not be fetched)"; } +absl::Status Xpress::ToStatus(const int xprs_err, + const absl::StatusCode code) const { + return ToStatus(xpress_model_, xprs_err, code); +} + Xpress::Xpress(XPRSprob& model) : xpress_model_(ABSL_DIE_IF_NULL(model)) { initIntControlDefaults(); } @@ -83,15 +100,11 @@ 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(MessageCallback cb, 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(MessageCallback cb, void* cbdata) { return ToStatus(XPRSremovecbmessage(xpress_model_, cb, cbdata)); } @@ -104,11 +117,68 @@ absl::Status Xpress::RemoveCbChecktime(ChecktimeCallback cb, void* cbdata) { return ToStatus(XPRSremovecbchecktime(xpress_model_, cb, cbdata)); } +absl::Status Xpress::AddCbBarlog(BarlogCallback cb, void* cbdata, int prio) { + return ToStatus(XPRSaddcbbarlog(xpress_model_, cb, cbdata, prio)); +} +absl::Status Xpress::RemoveCbBarlog(BarlogCallback cb, void* cbdata) { + return ToStatus(XPRSremovecbbarlog(xpress_model_, cb, cbdata)); +} + +absl::Status Xpress::AddCbLplog(LplogCallback cb, void* cbdata, int prio) { + return ToStatus(XPRSaddcblplog(xpress_model_, cb, cbdata, prio)); +} +absl::Status Xpress::RemoveCbLplog(LplogCallback cb, void* cbdata) { + return ToStatus(XPRSremovecblplog(xpress_model_, cb, cbdata)); +} + +absl::Status Xpress::AddCbPresolve(PresolveCallback cb, void* cbdata, + int prio) { + return ToStatus(XPRSaddcbpresolve(xpress_model_, cb, cbdata, prio)); +} +absl::Status Xpress::RemoveCbPresolve(PresolveCallback cb, void* cbdata) { + return ToStatus(XPRSremovecbpresolve(xpress_model_, cb, cbdata)); +} + +absl::Status Xpress::AddCbPreIntSol(PreintsolCallback cb, void* cbdata, + int prio) { + return ToStatus(XPRSaddcbpreintsol(xpress_model_, cb, cbdata, prio)); +} +absl::Status Xpress::RemoveCbPreIntSol(PreintsolCallback cb, void* cbdata) { + return ToStatus(XPRSremovecbpreintsol(xpress_model_, cb, cbdata)); +} + +absl::Status Xpress::AddCbOptNode(OptnodeCallback cb, void* cbdata, int prio) { + return ToStatus(XPRSaddcboptnode(xpress_model_, cb, cbdata, prio)); +} +absl::Status Xpress::RemoveCbOptNode(OptnodeCallback cb, void* cbdata) { + return ToStatus(XPRSremovecboptnode(xpress_model_, cb, cbdata)); +} + +absl::Status Xpress::AddCbPreNode(PrenodeCallback cb, void* cbdata, int prio) { + return ToStatus(XPRSaddcbprenode(xpress_model_, cb, cbdata, prio)); +} +absl::Status Xpress::RemoveCbPreNode(PrenodeCallback cb, void* cbdata) { + return ToStatus(XPRSremovecbprenode(xpress_model_, cb, cbdata)); +} + +absl::Status Xpress::AddCbCutRound(CutroundCallback cb, void* cbdata, + int prio) { + return ToStatus(XPRSaddcbcutround(xpress_model_, cb, cbdata, prio)); +} +absl::Status Xpress::RemoveCbCutRound(CutroundCallback cb, void* cbdata) { + return ToStatus(XPRSremovecbcutround(xpress_model_, cb, cbdata)); +} + Xpress::~Xpress() { CHECK_EQ(kXpressOk, XPRSdestroyprob(xpress_model_)); CHECK_EQ(kXpressOk, XPRSfree()); } +absl::Status Xpress::GetVersionNumbers(int* p_major, int* p_minor, + int* p_build) { + return ToStatus(nullptr, XPRSgetversionnumbers(p_major, p_minor, p_build)); +} + void Xpress::initIntControlDefaults() { std::vector controls = {XPRS_LPITERLIMIT, XPRS_BARITERLIMIT}; for (auto control : controls) { @@ -389,10 +459,6 @@ absl::Status Xpress::GetBasis(std::vector& rowBasis, absl::Status Xpress::SetStartingBasis(std::vector& rowBasis, std::vector& colBasis) const { - if (rowBasis.size() != colBasis.size()) { - return absl::InvalidArgumentError( - "Row basis and column basis must be of same size."); - } return ToStatus( XPRSloadbasis(xpress_model_, rowBasis.data(), colBasis.data())); } diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index c6cc30501b9..05358b77da0 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -52,6 +52,9 @@ class Xpress { ~Xpress(); + static absl::Status GetVersionNumbers(int* p_major, int* p_minor, + int* p_build); + absl::Status GetControlInfo(char const* name, int* p_id, int* p_type) const; absl::StatusOr GetIntControl(int control) const; @@ -130,17 +133,43 @@ 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); + using MessageCallback = void(XPRS_CC*)(XPRSprob, void*, char const*, int, + int); + absl::Status AddCbMessage(MessageCallback cb, void* cbdata, int prio = 0); + absl::Status RemoveCbMessage(MessageCallback cb, void* cbdata = nullptr); using ChecktimeCallback = int(XPRS_CC*)(XPRSprob, void*); absl::Status AddCbChecktime(ChecktimeCallback cb, void* cbdata, int prio = 0); absl::Status RemoveCbChecktime(ChecktimeCallback cb, void* cbdata = nullptr); + using BarlogCallback = int(XPRS_CC*)(XPRSprob, void*); + absl::Status AddCbBarlog(BarlogCallback cb, void* cbdata, int prio = 0); + absl::Status RemoveCbBarlog(BarlogCallback cb, void* cbdata = nullptr); + + using LplogCallback = int(XPRS_CC*)(XPRSprob, void*); + absl::Status AddCbLplog(LplogCallback cb, void* cbdata, int prio = 0); + absl::Status RemoveCbLplog(LplogCallback cb, void* cbdata = nullptr); + + using PresolveCallback = void(XPRS_CC*)(XPRSprob, void*); + absl::Status AddCbPresolve(PresolveCallback cb, void* cbdata, int prio = 0); + absl::Status RemoveCbPresolve(PresolveCallback cb, void* cbdata = nullptr); + + using PreintsolCallback = void(XPRS_CC*)(XPRSprob, void*, int, int*, double*); + absl::Status AddCbPreIntSol(PreintsolCallback cb, void* cbdata, int prio = 0); + absl::Status RemoveCbPreIntSol(PreintsolCallback cb, void* cbdata = nullptr); + + using OptnodeCallback = void(XPRS_CC*)(XPRSprob, void*, int*); + absl::Status AddCbOptNode(OptnodeCallback cb, void* cbdata, int prio = 0); + absl::Status RemoveCbOptNode(OptnodeCallback cb, void* cbdata = nullptr); + + using PrenodeCallback = void(XPRS_CC*)(XPRSprob, void*, int*); + absl::Status AddCbPreNode(PrenodeCallback cb, void* cbdata, int prio = 0); + absl::Status RemoveCbPreNode(PrenodeCallback cb, void* cbdata = nullptr); + + using CutroundCallback = void(XPRS_CC*)(XPRSprob, void*, int, int*); + absl::Status AddCbCutRound(CutroundCallback cb, void* cbdata, int prio = 0); + absl::Status RemoveCbCutRound(CutroundCallback cb, void* cbdata = nullptr); + absl::StatusOr> GetVarLb() const; absl::StatusOr> GetVarUb() const; @@ -197,6 +226,12 @@ class Xpress { std::string const& flags = ""); absl::Status SaveAs(std::string const& filename); + static std::string GetLastError(XPRSprob const& prob, int xprs_err); + + static absl::Status ToStatus( + XPRSprob prob, int xprs_err, + absl::StatusCode code = absl::StatusCode::kInvalidArgument); + private: XPRSprob xpress_model_; diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index f10b3d8df45..54b33902eba 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -64,12 +64,50 @@ namespace operations_research { namespace math_opt { + +// The callback events that Xpress supports. +// For Xpress it is not a problem to listen to MIP events when solving an LP, +// they will just never be triggered. However, the testsuite expects that we +// raise an error if someone attempts to register MIP events for a non-MIP +// solve, so we have to support different callbacks depending on the problem +// type. + +absl::flat_hash_set const XpressSolver::SupportedMIPEvents_( + { + CALLBACK_EVENT_PRESOLVE, + CALLBACK_EVENT_SIMPLEX, + CALLBACK_EVENT_MIP, + CALLBACK_EVENT_MIP_SOLUTION, + CALLBACK_EVENT_MIP_NODE, + CALLBACK_EVENT_BARRIER, + }); +absl::flat_hash_set const XpressSolver::SupportedLPEvents_({ + CALLBACK_EVENT_PRESOLVE, + CALLBACK_EVENT_SIMPLEX, + CALLBACK_EVENT_BARRIER, +}); + namespace { -struct SharedSolveContext { - Xpress* xpress; +/** Map an ortools variable or objective id to an Xpress column index. + * This will raise an exception if the ortools id does not exist in the map. + */ +int or2xprs(absl::linked_hash_map const& varMap, int64_t orId) { + return varMap.at(orId); // raises exception if no such key +} + +/** Map an ortools constraint id to an Xpress row index. + * This will raise an exception if the ortools id does not exist in the map. + */ +XpressSolver::XpressLinearConstraintIndex or2xprs( + absl::linked_hash_map const& conMap, + XpressSolver::LinearConstraintId orId) { + return conMap.at(orId).constraint_index; // raises exception if no such key +} - /** Mutex for accessing callback_status_. */ +class SharedSolveContext { + /** Mutex for accessing callback_exception or callback_status. */ absl::Mutex mutex; /** Capturing of exceptions in callbacks. @@ -79,7 +117,86 @@ struct SharedSolveContext { * of the solver. So we must capture exceptions, convert them to Status, * interrupt the solve and handle the status once the solver returned. */ + std::exception_ptr callback_exception ABSL_GUARDED_BY(mutex) = nullptr; + + /** Capturing of errors in callbacks. + * This allows us to report errors that occur in a callback back to + * the user once the solve is complete. + */ absl::Status callback_status ABSL_GUARDED_BY(mutex) = absl::OkStatus(); + + public: + /** The Xpress instance we use for adding and removing callbacks, + * for querying attributes and setting controls, etc. + */ + Xpress* const xpress; + + SharedSolveContext(Xpress* xprs) : xpress(xprs) {} + + ~SharedSolveContext() { + // If pending callback exception was not re-raised yet then do it now. + // Raising exceptions from a destructor is usually a bad idea. We do it + // only for RAII purposes and as the very last thing in the destructor. + // Note that instances of this class are only ever allocated on the stack, + // so an exception will not bypass memory deallocation. + if (callback_exception) std::rethrow_exception(callback_exception); + } + + inline void SetCallbackException(std::exception_ptr ex) { + absl::MutexLock const lock(mutex); + if (!callback_exception) callback_exception = ex; + } + + inline void SetCallbackStatus(absl::Status const& status) { + absl::MutexLock const lock(mutex); + callback_status.Update(status); + } + + /** Handle errors that occured during callbacks. + * This is supposed to be called after a solve completes. If there was an + * exception in a callback then this function re-raises the exception. + * If there was an error during any callback then this function returns that + * error. + */ + absl::Status HandleCallbackProblems() { + const absl::MutexLock lock(mutex); + // If callbacks raised an exception then re-raise that now. + if (callback_exception) { + std::rethrow_exception(std::exchange(callback_exception, nullptr)); + } + // If callbacks produced an error then return that now. + return std::exchange(callback_status, absl::OkStatus()); + } +}; + +/** Base class for scoped callbacks. + * This provides everything the ScopedCallback class requires that does not + * depend on template arguments. + */ +class ScopedCallbackBase { + ScopedCallbackBase(ScopedCallbackBase const&) = delete; + ScopedCallbackBase(ScopedCallbackBase&&) = delete; + ScopedCallbackBase& operator=(ScopedCallbackBase const&) = delete; + ScopedCallbackBase& operator=(ScopedCallbackBase&&) = delete; + + protected: + SharedSolveContext* ctx; + ScopedCallbackBase() : ctx(nullptr) {} + + public: + /** Store an exception that case raised during a callback. + * Only the first such exception will be remembered. + */ + inline void SetCallbackException(std::exception_ptr ex) { + ctx->SetCallbackException(ex); + } + + /** Store an error status that occurred during a callback. + * Only the first such error will be remembered. + */ + inline void SetCallbackStatus(absl::Status const& status) { + ctx->SetCallbackStatus(status); + } }; /** Registered callback that is auto-removed in the destructor. @@ -89,7 +206,7 @@ struct SharedSolveContext { * capture exceptions from user code and return them as Status. */ template -class ScopedCallback { +class ScopedCallback : public ScopedCallbackBase { using proto_type = typename ProtoT::proto_type; SharedSolveContext* ctx_; @@ -106,28 +223,27 @@ class ScopedCallback { template struct ExWrapper { // The static function that will be directly invoked by Xpress - static auto low_level_cb(XPRSprob prob, void* cbdata, Args... args) { + // Note: Xpress callbacks return either void or int. All callbacks + // that return int treat zero as "continue" and non-zero as "stop" + // (either explicit stop request or error). + static R low_level_cb(XPRSprob prob, void* cbdata, Args... args) { + ScopedCallback* cb = reinterpret_cast(cbdata); #ifdef ABSL_HAVE_EXCEPTIONS try { #endif - return ProtoT::glueFn(prob, cbdata, args...); + absl::Status status = ProtoT::glueFn(prob, cbdata, args...); + if (status.ok()) return static_cast(0); // void-cast ignores value + XPRSinterrupt(prob, XPRS_STOP_GENERICERROR); + cb->SetCallbackStatus(status); #ifdef ABSL_HAVE_EXCEPTIONS - } catch (const std::exception& e) { - // Catch any exception and terminate Xpress gracefully - ScopedCallback* cb = reinterpret_cast(cbdata); - cb->Interrupt(XPRS_STOP_USER); - cb->SetCallbackStatus(absl::UnknownError(e.what())); - if constexpr (std::is_convertible_v) return static_cast(1); } catch (...) { // Catch any exception and terminate Xpress gracefully - ScopedCallback* cb = reinterpret_cast(cbdata); - cb->Interrupt(XPRS_STOP_USER); - cb->SetCallbackStatus(absl::UnknownError( - "a C++ exception that is not a std::exception occurred in " - "Xpress callback")); - if constexpr (std::is_convertible_v) return static_cast(1); + XPRSinterrupt(prob, XPRS_STOP_USER); + cb->SetCallbackException(std::current_exception()); } #endif + // We get here only if an error or an exception occurred. + return static_cast(1); // void-cast ignores value } }; const proto_type low_level_cb_ = ExWrapper::low_level_cb; @@ -135,7 +251,7 @@ class ScopedCallback { public: CbT or_tools_cb_; - ScopedCallback() : ctx_(nullptr) {} + ScopedCallback() : ScopedCallbackBase(), ctx_(nullptr) {} inline absl::Status Add(SharedSolveContext* context, CbT cb) { ctx_ = context; @@ -150,8 +266,7 @@ class ScopedCallback { } inline void SetCallbackStatus(const absl::Status& status) { - const absl::MutexLock lock(ctx_->mutex); - ctx_->callback_status.Update(status); + ctx_->SetCallbackStatus(status); } ~ScopedCallback() { @@ -177,20 +292,21 @@ class 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; \ - 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 DEFINE_SCOPED_CB(CB_NAME, ORTOOLS_CB, CB_RET_TYPE, ARGS) \ + absl::Status CB_NAME##GlueFn ARGS; \ + struct CB_NAME##Traits { \ + using proto_type = CB_RET_TYPE(XPRS_CC*) ARGS; \ + static constexpr absl::Status(XPRS_CC* glueFn) ARGS = CB_NAME##GlueFn; \ + static absl::Status Add(Xpress* xpress, proto_type fn, void* data, \ + int prio = 0) { \ + return xpress->AddCb##CB_NAME(fn, data, prio); \ + } \ + static void Remove(Xpress* xpress, proto_type fn, void* data) { \ + CHECK_OK(xpress->RemoveCb##CB_NAME(fn, data)); \ + } \ + }; \ + using CB_NAME##ScopedCb = ScopedCallback; \ + absl::Status CB_NAME##GlueFn ARGS /** Define the message callback. * This forwards messages from Xpress to an ortools message callback. @@ -207,12 +323,12 @@ DEFINE_SCOPED_CB(Message, SolverInterface::MessageCallback, void, break; default: // message type 2 is not used by Xpress, negative values mean "flush" - return; + return absl::OkStatus(); } if (len == 0) { cb->or_tools_cb_(std::vector{""}); - return; + return absl::OkStatus(); } std::vector lines; @@ -233,13 +349,14 @@ DEFINE_SCOPED_CB(Message, SolverInterface::MessageCallback, void, start = end + 1; } cb->or_tools_cb_(lines); + return absl::OkStatus(); } /** Define the checktime callback. * This callbacks checks an interrupter for whether the solve was interrupted. */ DEFINE_SCOPED_CB(Checktime, SolveInterrupter const*, int, - (XPRSprob /*prob*/, void* cbdata)) { + (XPRSprob prob, void* cbdata)) { auto cb = reinterpret_cast(cbdata); // Note: we do NOT return non-zero from the callback if the solve was // interrupted. Returning non-zero from the callback is interpreted @@ -247,8 +364,716 @@ DEFINE_SCOPED_CB(Checktime, SolveInterrupter const*, int, // the resulting stop status to ortools' termination status. if (cb->or_tools_cb_->IsInterrupted()) { cb->Interrupt(XPRS_STOP_USER); + ABSL_RETURN_IF_ERROR( + Xpress::ToStatus(prob, XPRSinterrupt(prob, XPRS_STOP_USER))); + } + return absl::OkStatus(); +} + +/** This is passed as user data to the callback. */ +struct OrtoolsCallbackContext { + /** Storage for solutions that cannot be injected in the callback in which + * the user returns them. + * This is needed since not all Xpress callbacks allow injection of solutions + * in all situations. + */ + struct SolStore { + std::vector ind; + std::vector val; + }; + /** Storage for cuts or lazy constraints that cannot be injected in the + * callback in which the user returns them. + * This is needed since not all Xpress callbacks allow injection of cuts or + * lazy constraints in all situations. + */ + struct CutStore { + std::vector start; + std::vector ind; + std::vector val; + std::vector sense; + std::vector rhs; + + /** Add the cuts stored in this instance as managed cuts to prob. + * The function assumes that all cuts stored in this instance are stated + * in the original space. + */ + absl::Status AddManagedCuts(XPRSprob prob) { + if (start.empty()) return absl::OkStatus(); + constexpr int const globallyValid = + 1; // Cuts must always be globally valid. + start.push_back(ind.size()); + int const xprs_err = XPRSaddmanagedcuts64( + prob, globallyValid, rhs.size(), sense.data(), rhs.data(), + start.data(), ind.data(), val.data()); + start.clear(); + ind.clear(); + val.clear(); + sense.clear(); + rhs.clear(); + return Xpress::ToStatus(prob, xprs_err); + } + + /** Add the cuts stored in this instance as lazy constraints to prob. + * The function assumes that the cuts stored are already presolved. + */ + absl::Status AddLazyConstraints(XPRSprob prob) { + if (start.empty()) return absl::OkStatus(); + start.push_back(ind.size()); + std::vector cuttype(rhs.size()); // Cannot be null. + int const xprs_err = + XPRSaddcuts64(prob, rhs.size(), cuttype.data(), sense.data(), + rhs.data(), start.data(), ind.data(), val.data()); + start.clear(); + ind.clear(); + val.clear(); + sense.clear(); + rhs.clear(); + return Xpress::ToStatus(prob, xprs_err); + } + }; + /** ortools callback function. */ + SolverInterface::Callback const cb_; + /** Maps ortools variable ids to Xpress column indices in the original + * space. This is a reference to XpressSolver::variables_map_. + */ + absl::linked_hash_map const& varMap_; + /** The solution filter specified by the user for CALLBACK_EVENT_MIP_SOLUTION. + */ + SparseVectorFilterProto const& mip_solution_filter_; + /** The solution filter specified by the user for CALLBACK_EVENT_MIP_NODE. */ + SparseVectorFilterProto const& mip_node_filter_; + + private: + /** Mutex for accessing the maps below. */ + absl::Mutex mutex; + /** Lazy constraints that could not be injected at the time they were + * separated. */ + absl::linked_hash_map delayedLazyConstraints_; + /** User cuts that could not be injected at the time they were separated. */ + absl::linked_hash_map delayedCuts_; + /** Feasible solutions that could not be injected at the time they were + * provided. */ + absl::linked_hash_map> delayedSols_; + /** The time at which the solve started. */ + absl::Time const startTime_; + + public: + OrtoolsCallbackContext(SolverInterface::Callback const& cb, + CallbackRegistrationProto const& callback_registration, + absl::linked_hash_map const& varMap) + : cb_(cb), + varMap_(varMap), + mip_solution_filter_(callback_registration.mip_solution_filter()), + mip_node_filter_(callback_registration.mip_node_filter()), + startTime_(absl::Now()) {} + + template + ElemT* GetDelayedEntity_(int threadID, bool create, + absl::linked_hash_map& map) { + absl::MutexLock const lock(mutex); + if (!create) { + auto it = map.find(threadID); + return it != map.end() ? &it->second : nullptr; + } + auto res = map.try_emplace(threadID); + return &res.first->second; + } + + public: + /** Get the store for delayed lazy constraints for the specified thread. + * @param threadID The thread for which we should get the store. + * @param create If true then the store will be created if it does not + * yet exist. + * @return The requested store or nullptr if that does not exist and + * create was false. + */ + CutStore* GetDelayedLazyConstraints(int threadID, bool create) { + return GetDelayedEntity_(threadID, create, delayedLazyConstraints_); + } + + /** Get the store for delayed cuts for the specified thread. + * @param threadID The thread for which we should get the store. + * @param create If true then the store will be created if it does not + * yet exist. + * @return The requested store or nullptr if that does not exist and + * create was false. + */ + CutStore* GetDelayedCuts(int threadID, bool create) { + return GetDelayedEntity_(threadID, create, delayedCuts_); + } + + /** Get the store for delayed solutions for the specified thread. + * @param threadID The thread for which we should get the store. + * @param create If true then the store will be created if it does not + * yet exist. + * @return The requested store or nullptr if that does not exist and + * create was false. + */ + std::vector* GetDelayedSolutions(int threadID, bool create) { + return GetDelayedEntity_(threadID, create, delayedSols_); + } + + /** Flush any delayed info from a callback for the current thread. + * @param prob The problem instance that was passed into the callback. + * @param flushCuts Whether we are allowed to flush user cuts. + */ + absl::Status FlushDelayedInfo(XPRSprob prob, bool flushCuts) { + int threadID = -1; + ABSL_RETURN_IF_ERROR(Xpress::ToStatus( + prob, XPRSgetintattrib(prob, XPRS_MIPTHREADID, &threadID))); + + CutStore* store = nullptr; + if (flushCuts) { + if ((store = GetDelayedCuts(threadID, false))) { + ABSL_RETURN_IF_ERROR(store->AddManagedCuts(prob)); + } + } + if ((store = GetDelayedLazyConstraints(threadID, false))) { + ABSL_RETURN_IF_ERROR(store->AddLazyConstraints(prob)); + } + std::vector* sols = GetDelayedSolutions(threadID, false); + if (sols) { + int xprs_err = 0; + for (auto const& s : *sols) { + xprs_err = XPRSaddmipsol(prob, s.ind.size(), s.val.data(), s.ind.data(), + nullptr); + if (xprs_err != 0) break; + } + sols->clear(); + ABSL_RETURN_IF_ERROR(Xpress::ToStatus(prob, xprs_err)); + } + return absl::OkStatus(); + } + + /** Set the elapsed time in callback data. + */ + absl::Status SetElapsed(XPRSprob prob, CallbackDataProto& cbdata) { + ABSL_RETURN_IF_ERROR(util_time::EncodeGoogleApiProto( + absl::Now() - startTime_, cbdata.mutable_runtime())); + return absl::OkStatus(); + } + + /** Apply a solutionn filter to reduce the number of non-zeros in a + * solution. + */ + SparseDoubleVectorProto FilterSolution( + absl::Span dense, const SparseVectorFilterProto& filter) { + SparseVectorFilterPredicate predicate(filter); + SparseDoubleVectorProto result; + for (auto const [id, idx] : varMap_) { + const double val = dense[idx]; + if (predicate.AcceptsAndUpdate(id, val)) { + result.add_ids(id); + result.add_values(val); + } + } + return result; + } +}; + +// Query attributes from callbacks. +// Since we do not want to create an Xpress instance for every callback +// invocation, we just directly call XPRSgetintattrib() and friends from +// callbacks. +absl::Status GetAttr(XPRSprob prob, int attr, int* value) { + return Xpress::ToStatus(prob, XPRSgetintattrib(prob, attr, value)); +} +absl::Status GetAttr(XPRSprob prob, int attr, int64_t* value) { + XPRSint64 xval; + int err = XPRSgetintattrib64(prob, attr, &xval); + *value = xval; + return Xpress::ToStatus(prob, err); +} +absl::Status GetAttr(XPRSprob prob, int attr, double* value) { + return Xpress::ToStatus(prob, XPRSgetdblattrib(prob, attr, value)); +} + +/** Set an attribute for a CallbackDataProto. + * The macro assumes that an XPRSprob `prob` is in the current scope. + * @param stats The field of CallbackDataProto to be set. + * @param orattr The attribute to set in stats. + * @param xattr The XPRS_FOO attribute to query. + */ +#define CALLBACK_SET_ATTRIBUTE(stats, orattr, xattr) \ + do { \ + decltype(stats->orattr()) value_; \ + ABSL_RETURN_IF_ERROR(GetAttr(prob, xattr, &value_)); \ + stats->set_##orattr(value_); \ + } while (0) + +/** Initialize simplex statistics in cbdata. + * @param prob Problem passed into callback. + * @param cbdata Data that will be passed to the ortools callback. + */ +absl::Status InitSimplexStats(XPRSprob prob, CallbackDataProto& cbdata) { + CallbackDataProto::SimplexStats* const s = cbdata.mutable_simplex_stats(); + CALLBACK_SET_ATTRIBUTE(s, iteration_count, XPRS_SIMPLEXITER); + /* This is not available in Xpress + CALLBACK_SET_ATTRIBUTE(s, is_perturbed, ); + */ + CALLBACK_SET_ATTRIBUTE(s, objective_value, XPRS_LPOBJVAL); + CALLBACK_SET_ATTRIBUTE(s, primal_infeasibility, XPRS_SUMPRIMALINF); + /* Xpress only has XPRS_SUMPRIMALINF, not XPRS_SUMDUALINF + CALLBACK_SET_ATTRIBUTE(s, dual_infeasibility, ); + */ + return absl::OkStatus(); +} + +/** Initialize barrier statistics in cbdata. + * @param prob Problem passed into callback. + * @param cbdata Data that will be passed to the ortools callback. + */ +absl::Status InitBarrierStats(XPRSprob prob, CallbackDataProto& cbdata) { + CallbackDataProto::BarrierStats* const s = cbdata.mutable_barrier_stats(); + + CALLBACK_SET_ATTRIBUTE(s, iteration_count, XPRS_BARITER); + CALLBACK_SET_ATTRIBUTE(s, primal_objective, XPRS_BARPRIMALOBJ); + CALLBACK_SET_ATTRIBUTE(s, dual_objective, XPRS_BARDUALOBJ); + CALLBACK_SET_ATTRIBUTE(s, complementarity, XPRS_BARCGAP); + CALLBACK_SET_ATTRIBUTE(s, primal_infeasibility, XPRS_BARPRIMALINF); + CALLBACK_SET_ATTRIBUTE(s, dual_infeasibility, XPRS_BARDUALINF); + return absl::OkStatus(); +} + +/** Initialize presolve statistics in cbdata. + * @param prob Problem passed into callback. + * @param cbdata Data that will be passed to the ortools callback. + */ +absl::Status InitPresolveStats(XPRSprob prob, CallbackDataProto& cbdata) { + int cols, origCols, rows, origRows; + ABSL_RETURN_IF_ERROR( + Xpress::ToStatus(prob, XPRSgetintattrib(prob, XPRS_COLS, &cols))); + ABSL_RETURN_IF_ERROR(Xpress::ToStatus( + prob, XPRSgetintattrib(prob, XPRS_ORIGINALCOLS, &origCols))); + ABSL_RETURN_IF_ERROR( + Xpress::ToStatus(prob, XPRSgetintattrib(prob, XPRS_ROWS, &rows))); + ABSL_RETURN_IF_ERROR(Xpress::ToStatus( + prob, XPRSgetintattrib(prob, XPRS_ORIGINALROWS, &origRows))); + + CallbackDataProto::PresolveStats* const s = cbdata.mutable_presolve_stats(); + s->set_removed_variables(origCols - cols); + s->set_removed_constraints(origRows - rows); + /* These two are not available in Xpress. + s->set_bound_changes() + s->set_coefficient_changes() + */ + return absl::OkStatus(); +} + +/** Initialize MIP statistics in cbdata. + * @param prob Problem passed into callback. + * @param cbdata Data that will be passed to the ortools callback. + */ +absl::Status InitMipStats(XPRSprob prob, CallbackDataProto& cbdata) { + CallbackDataProto::MipStats* const s = cbdata.mutable_mip_stats(); + + CALLBACK_SET_ATTRIBUTE(s, primal_bound, XPRS_MIPBESTOBJVAL); + CALLBACK_SET_ATTRIBUTE(s, dual_bound, XPRS_BESTBOUND); + CALLBACK_SET_ATTRIBUTE(s, explored_nodes, XPRS_NODES); + CALLBACK_SET_ATTRIBUTE(s, open_nodes, XPRS_ACTIVENODES); + // Note that in multi-threading SIMPLEXITER gives the iterations per worker, + // not the global grand total. That will only be reported after the solve. + CALLBACK_SET_ATTRIBUTE(s, simplex_iterations, XPRS_SIMPLEXITER); + CALLBACK_SET_ATTRIBUTE(s, number_of_solutions_found, XPRS_MIPSOLS); + CALLBACK_SET_ATTRIBUTE(s, cutting_planes_in_lp, XPRS_CUTS); + return absl::OkStatus(); +} + +/** Invoke the ortools callback. + * The function also handles all actions requested by the callback, such as + * termination requests, injected solutions, add cuts/lazy constraints. + * @param ctx Global callback context. + * @param prob The XPRSprob passed into the Xpress callback. + * @param data Data passed into the ortools callback. + * Everything but the runtime field must be setup. + * @param allowCuts If this is true then the callback is allowed to + * add cuts or lazy constraints. + * @param modifiableNode True if the current node can be modified, i.e., + * we can add solutions, cuts, lazy constraints. + * @param hadLazy Set to true if we had any lazy constraint. + */ +absl::Status InvokeOrtoolsCallback(OrtoolsCallbackContext* ctx, XPRSprob prob, + CallbackDataProto& data, bool allowCuts, + bool modifiableNode, bool* hadLazy) { + ABSL_RETURN_IF_ERROR(ctx->SetElapsed(prob, data)); + ABSL_ASSIGN_OR_RETURN(CallbackResultProto result, ctx->cb_(data)); + // The variables below are needed to presolve lazy constraints. They are + // only initialized when we have to presolve the first lazy constraint. + int origCols = -1, cols = -1, threadID = -1; + std::vector origInd, preInd; + std::vector origVal, preVal; + + ABSL_RETURN_IF_ERROR(Xpress::ToStatus( + prob, XPRSgetintattrib(prob, XPRS_MIPTHREADID, &threadID))); + + // Setup stores for cuts/lazy constraints, depending on whether we can + // add them directly or need to cache them. + OrtoolsCallbackContext::CutStore cutsToCommit; + OrtoolsCallbackContext::CutStore* cutStore = &cutsToCommit; + OrtoolsCallbackContext::CutStore* lazyStore = nullptr; + std::vector* solStore = nullptr; + if (!modifiableNode) { + cutStore = ctx->GetDelayedCuts(threadID, true); + lazyStore = ctx->GetDelayedLazyConstraints(threadID, true); + solStore = ctx->GetDelayedSolutions(threadID, true); + } + + // Go through all linear constraints returned by the callback. + for (CallbackResultProto::GeneratedLinearConstraint const& cut : + result.cuts()) { + if (!allowCuts) { + return absl::StatusBuilder(absl::StatusCode::kInvalidArgument) + << " Callback " << data.event() + << " is not allowed to generate cuts or lazy constraints"; + } + + // Since we cannot add ranged cuts, we must add ranged cuts as two + // different cuts, one for each direction. + char senseToAdd[2]; + double rhsToAdd[2]; + int numToAdd = 0; + double const lb = cut.lower_bound(); + double const ub = cut.upper_bound(); + + // Now process the cut/lazy constraint + if (lb <= -1e20 && ub >= 1e20) { + // Ignore free rows. + continue; + } else if (lb == ub) { + // Equality constraint + senseToAdd[0] = 'E'; + rhsToAdd[0] = lb; + numToAdd = 1; + } else { + if (lb <= -1e20) { + // <= constraint + senseToAdd[0] = 'L'; + rhsToAdd[0] = ub; + numToAdd = 1; + } else if (ub >= 1e20) { + // >= constraint + senseToAdd[0] = 'G'; + rhsToAdd[0] = lb; + numToAdd = 1; + } else { + // Range constraint, must insert the cut twice, once for each direction + senseToAdd[0] = 'L'; + rhsToAdd[0] = ub; + senseToAdd[1] = 'G'; + rhsToAdd[1] = lb; + numToAdd = 2; + } + } + + for (int i = 0; i < numToAdd; ++i) { + if (cut.is_lazy()) { + // Lazy constraints are special. They must be provided in the + // presolved space, so we must presolve them. + *hadLazy = true; + if (cols < 0) { + // First lazy constraint. Initialize the buffers. + ABSL_RETURN_IF_ERROR(Xpress::ToStatus( + prob, XPRSgetintattrib(prob, XPRS_ORIGINALCOLS, &origCols))); + ABSL_RETURN_IF_ERROR( + Xpress::ToStatus(prob, XPRSgetintattrib(prob, XPRS_COLS, &cols))); + origInd.reserve(origCols); + origVal.reserve(origCols); + preInd.resize(cols); + preVal.resize(cols); + } + origInd.clear(); + origVal.clear(); + for (auto const [id, value] : MakeView(cut.linear_expression())) { + origInd.push_back(or2xprs(ctx->varMap_, id)); + origVal.push_back(value); + } + int const cuttype = 0; + int ncoefs, status; + double preRhs; + /* Note: For the second iteration for ranged rows, we cannot just + * reuse results from the first iteration and change direction + * of the constraints: constants on the left-hand side may have + * to be factored into the right-hand side. + */ + ABSL_RETURN_IF_ERROR(Xpress::ToStatus( + prob, + XPRSpresolverow(prob, senseToAdd[i], origInd.size(), origInd.data(), + origVal.data(), rhsToAdd[i], cols, &ncoefs, + preInd.data(), preVal.data(), &preRhs, &status))); + if (status != 0) { + // Presolving a row may fail depending on which presolve reductions + // have been carried out on the problem. Hedge against this. + return absl::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "Failed to presolve a lazy constraint, status = " << status; + } + if (lazyStore) { + // We cannot apply the lazy constraints directly, we must buffer + // them. + lazyStore->start.push_back(lazyStore->ind.size()); + lazyStore->ind.insert(lazyStore->ind.end(), preInd.begin(), + preInd.begin() + ncoefs); + lazyStore->val.insert(lazyStore->val.end(), preVal.begin(), + preVal.begin() + ncoefs); + lazyStore->sense.push_back(senseToAdd[i]); + lazyStore->rhs.push_back(preRhs); + } else { + // Apply lazy constraints one by one, there is not really a point + // in buffering since presolving the row already is a significant + // overhead. + XPRSint64 const prestart[] = {0, + static_cast(preInd.size())}; + ABSL_RETURN_IF_ERROR(Xpress::ToStatus( + prob, XPRSaddcuts64(prob, 1, &cuttype, &senseToAdd[i], &preRhs, + prestart, preInd.data(), preVal.data()))); + } + } else { + // A regular cut. + cutStore->start.push_back(cutStore->ind.size()); + for (auto const [id, value] : MakeView(cut.linear_expression())) { + cutStore->ind.push_back(or2xprs(ctx->varMap_, id)); + cutStore->val.push_back(value); + } + cutStore->sense.push_back(senseToAdd[i]); + cutStore->rhs.push_back(rhsToAdd[i]); + } + } + } + + if (!cutsToCommit.start.empty()) + ABSL_RETURN_IF_ERROR(cutsToCommit.AddManagedCuts(prob)); + + // Process any solutions that were added. + for (SparseDoubleVectorProto const& solution_vector : + result.suggested_solutions()) { + std::vector ids; + std::vector vals; + for (auto const [id, value] : MakeView(solution_vector)) { + ids.push_back(or2xprs(ctx->varMap_, id)); + vals.push_back(value); + } + if (solStore) { + solStore->push_back({ids, vals}); + } else { + ABSL_RETURN_IF_ERROR(Xpress::ToStatus( + prob, + XPRSaddmipsol(prob, ids.size(), vals.data(), ids.data(), nullptr))); + } + } + // If we are asked to terminate then do that now. + if (result.terminate()) { + ABSL_RETURN_IF_ERROR( + Xpress::ToStatus(prob, XPRSinterrupt(prob, XPRS_STOP_USER))); + } + return absl::OkStatus(); +} + +// Below we define various callbacks that we need in order to implement +// the ortools callback. +// ortools only has a single callback that is called with different events. +// Xpress on the other side has a separate callback for each event. So we +// need multiple Xpress callbacks that all fire the same ortools callback but +// with different events. + +/** Barrier Logging callback. + * This is used to implemented CALLBACK_EVENT_BARRIER. + * Specification of this event from math_opt/cpp/callback.h: + * Called in each iterate of an interior point/barrier method. + * @return non-zero to stop the solve. + */ +DEFINE_SCOPED_CB(Barlog, OrtoolsCallbackContext*, int, + (XPRSprob prob, void* cbdata)) { + BarlogScopedCb* cb = reinterpret_cast(cbdata); + OrtoolsCallbackContext* ctx = cb->or_tools_cb_; + CallbackDataProto cbargs; + cbargs.set_event(CALLBACK_EVENT_BARRIER); + ABSL_RETURN_IF_ERROR(InitBarrierStats(prob, cbargs)); + ABSL_RETURN_IF_ERROR( + InvokeOrtoolsCallback(ctx, prob, cbargs, false, false, nullptr)); + return absl::OkStatus(); +} + +/** LP Logging callback. + * This is used to implemented CALLBACK_EVENT_SIMPLEX. + * Specification of this event from math_opt/cpp/callback.h: + * The solver is currently running the simplex method. + * @return non-zero to stop the solve. + */ +DEFINE_SCOPED_CB(Lplog, OrtoolsCallbackContext*, int, + (XPRSprob prob, void* cbdata)) { + LplogScopedCb* cb = reinterpret_cast(cbdata); + OrtoolsCallbackContext* ctx = cb->or_tools_cb_; + CallbackDataProto cbargs; + cbargs.set_event(CALLBACK_EVENT_SIMPLEX); + ABSL_RETURN_IF_ERROR(InitSimplexStats(prob, cbargs)); + ABSL_RETURN_IF_ERROR( + InvokeOrtoolsCallback(ctx, prob, cbargs, false, false, nullptr)); + return absl::OkStatus(); +} + +/** Presolve callback. + * This is used to implement CALLBACK_EVENT_PRESOLVE. + * Specification of this event from math_opt/cpp/callback.h: + * The solver is currently running presolve + * Note that this callback is fired only once at the end of presolve. + * Note that in case of restarts it might be fired once for each restart. + * @return non-zero to stop the solve. + */ +DEFINE_SCOPED_CB(Presolve, OrtoolsCallbackContext*, void, + (XPRSprob prob, void* cbdata)) { + PresolveScopedCb* cb = reinterpret_cast(cbdata); + OrtoolsCallbackContext* ctx = cb->or_tools_cb_; + CallbackDataProto cbargs; + cbargs.set_event(CALLBACK_EVENT_PRESOLVE); + bool hadLazy = false; + ABSL_RETURN_IF_ERROR(InitPresolveStats(prob, cbargs)); + ABSL_RETURN_IF_ERROR( + InvokeOrtoolsCallback(ctx, prob, cbargs, true, false, &hadLazy)); + return absl::OkStatus(); +} + +/** Preintsol callback. + * This is used to implement CALLBACK_EVENT_MIPSOLUTION. + * Specification of this event from math_opt/cpp/callback.h: + * Called every time a new MIP incumbent is found. + * Note that we can only add solutions, cuts, lazy constraints if soltype==0. + * In all other cases we capture what we should add and flush it only later + * from optnode/cutround. + */ +DEFINE_SCOPED_CB(PreIntSol, OrtoolsCallbackContext*, void, + (XPRSprob prob, void* cbdata, int soltype, int* p_reject, + double*)) { + // We could flush here, but that can have bad side effects. For example + // - if we inject lazy constraints and the solution is not violated by + // any of them, then this would still reject the solution. + // - if we flush a feasible solution but the current node is later + // rejected, then we lost this feasible solution. + // We therefore leave all the flushing to the optnode callback. + PreIntSolScopedCb* cb = reinterpret_cast(cbdata); + OrtoolsCallbackContext* ctx = cb->or_tools_cb_; + CallbackDataProto cbargs; + cbargs.set_event(CALLBACK_EVENT_MIP_SOLUTION); + /* The specification in callback.h says that primal_solution should hold + * the candidate relaxation. That is returned by XPRSgetcallbacksolution() in + * the preintsol callback context. + */ + int cols; + ABSL_RETURN_IF_ERROR( + Xpress::ToStatus(prob, XPRSgetintattrib(prob, XPRS_ORIGINALCOLS, &cols))); + std::vector x(cols); + ABSL_RETURN_IF_ERROR(Xpress::ToStatus( + prob, XPRSgetcallbacksolution(prob, nullptr, x.data(), 0, cols - 1))); + // Note that ortools has no way to communicate the objective value, + // users have to find that themselves. + *cbargs.mutable_primal_solution_vector() = + ctx->FilterSolution(absl::MakeSpan(x), ctx->mip_solution_filter_); + bool hadLazy = false; + ABSL_RETURN_IF_ERROR(InitMipStats(prob, cbargs)); + int solution_source = CALLBACK_SOLUTION_SOURCE_UNSPECIFIED; + switch (soltype) { + case 0: + solution_source = CALLBACK_SOLUTION_SOURCE_INTEGRAL; + break; + case 1: + solution_source = CALLBACK_SOLUTION_SOURCE_HEURISTIC; + break; + case 2: + solution_source = CALLBACK_SOLUTION_SOURCE_USER; + break; + } + cbargs.mutable_mip_stats()->set_solution_source(solution_source); + ABSL_RETURN_IF_ERROR( + InvokeOrtoolsCallback(ctx, prob, cbargs, true, soltype == 0, &hadLazy)); + if (hadLazy && soltype != 0) { + // The user provided lazy constraints but we could not add them. We have to + // flatly reject the solution. + // Note that we must NOT set *p_reject=1 if there were lazy constraints + // and we could add them, since that would drop the whole subtree. + *p_reject = 1; } - return 0; + return absl::OkStatus(); +} + +/** Cut round callback. + * While the optnode() callback is the obvious candidate for implementing + * CALLBACK_EVENT_MIP_NODE, we use the cutround callback because this allows + * us to immediately directly + * - add user cuts via XPRSaddmanagedcuts(), + * - add lazy constraints via XPRSaddcuts() (it is recommended to do this via + * optnode() but explicitly allowed for cutround()) + * - add new solutions via XPRSaddmipsol(). + * In the optnode() callback we cannot call XPRSaddmanagedcuts(). + */ +DEFINE_SCOPED_CB(CutRound, OrtoolsCallbackContext*, void, + (XPRSprob prob, void* cbdata, int ifxpresscuts, int*)) { + CutRoundScopedCb* cb = reinterpret_cast(cbdata); + OrtoolsCallbackContext* ctx = cb->or_tools_cb_; + + // First flush any pending information. + ABSL_RETURN_IF_ERROR(ctx->FlushDelayedInfo(prob, true)); + + // Only trigger the event if Xpress is done with its cutting. + if (ifxpresscuts) return absl::OkStatus(); + + CallbackDataProto cbargs; + cbargs.set_event(CALLBACK_EVENT_MIP_NODE); + // The specification in callback.h says that primal_solution should hold + // the LP relaxation. That is returned by XPRSgetcallbacksolution() in the + // optnode callback context. If the current node is infeasible then we shall + // not setup primal_solution. + int cols; + ABSL_RETURN_IF_ERROR( + Xpress::ToStatus(prob, XPRSgetintattrib(prob, XPRS_ORIGINALCOLS, &cols))); + std::vector x(cols); + int available = 0; + ABSL_RETURN_IF_ERROR(Xpress::ToStatus( + prob, XPRSgetcallbacksolution(prob, &available, x.data(), 0, cols - 1))); + if (available) { + *cbargs.mutable_primal_solution_vector() = + ctx->FilterSolution(absl::MakeSpan(x), ctx->mip_node_filter_); + } + bool hadLazy = false; + ABSL_RETURN_IF_ERROR(InitMipStats(prob, cbargs)); + ABSL_RETURN_IF_ERROR( + InvokeOrtoolsCallback(ctx, prob, cbargs, true, true, &hadLazy)); + return absl::OkStatus(); +} + +/** prenode callback. + * This is used to implement CALLBACK_EVENT_MIP. + * Specification of this event from math_opt/cpp/callback.h: + * The solver is in the MIP loop (called periodically before starting a + * new node). Useful for early termination. Note that this event does not + * provide information on LP relaxations nor about new incumbent solutions. + */ +DEFINE_SCOPED_CB(PreNode, OrtoolsCallbackContext*, void, + (XPRSprob prob, void* cbdata, int* p_infeasible)) { + PreNodeScopedCb* cb = reinterpret_cast(cbdata); + OrtoolsCallbackContext* ctx = cb->or_tools_cb_; + CallbackDataProto cbargs; + cbargs.set_event(CALLBACK_EVENT_MIP); + bool hadLazy = false; + /** TODO: Is it not clear from the specification whether injecting cuts, + * lazy constraints, or feasible solutions is allowed from this + * event. We currently allow that but defer the actual injection + * until the next optnode() event. + */ + ABSL_RETURN_IF_ERROR(InitMipStats(prob, cbargs)); + ABSL_RETURN_IF_ERROR( + InvokeOrtoolsCallback(ctx, prob, cbargs, true, false, &hadLazy)); + return absl::OkStatus(); +} + +/** optnode callback. + * This is only used for flushing information that could not be processed in + * preintsol() or prenode(). + */ +DEFINE_SCOPED_CB(OptNode, OrtoolsCallbackContext*, void, + (XPRSprob prob, void* cbdata, int* p_infeasible)) { + OptNodeScopedCb* cb = reinterpret_cast(cbdata); + OrtoolsCallbackContext* ctx = cb->or_tools_cb_; + + ABSL_RETURN_IF_ERROR(ctx->FlushDelayedInfo(prob, false)); + return absl::OkStatus(); } /** An ortools message callback that prints everything to stdout. */ @@ -309,9 +1134,27 @@ class ScopedSolverContext { ChecktimeScopedCb checktime_callback_; /** If we installed an interrupter callback then this removes it. */ std::unique_ptr interrupter_callback_; + /** Installed barlog callback (if any). */ + BarlogScopedCb barlog_callback_; + /** Installed lplog callback (if any). */ + LplogScopedCb lplog_callback_; + /** Installed presolve callback (if any). */ + PresolveScopedCb presolve_callback_; + /** Installed preintsol callback (if any). */ + PreIntSolScopedCb preintsol_callback_; + /** Installed optnode callback (if any). */ + OptNodeScopedCb optnode_callback_; + /** Installed prenode callback (if any). */ + PreNodeScopedCb prenode_callback_; + /** Installed cutround callback (if any). */ + CutRoundScopedCb cutround_callback_; + /** Context to invoke the ortools callback (if any). */ + std::unique_ptr ctx; /** A single control that must be reset in the destructor. */ struct OneControl { int id; + // std::variant (not a raw union) so the std::string alternative is + // destroyed automatically; index() drives the manual restore in the dtor. std::variant value; enum { INT_CONTROL, @@ -323,10 +1166,8 @@ class ScopedSolverContext { std::vector modified_controls_; public: - explicit ScopedSolverContext(Xpress* xpress) { shared_ctx_.xpress = xpress; } - absl::Status Set(int id, int32_t value) { - return Set(id, static_cast(value)); - } + ScopedSolverContext(Xpress* xpress) : shared_ctx_(xpress), ctx(nullptr) {} + absl::Status Set(int id, int32_t value) { return Set(id, int64_t(value)); } absl::Status Set(int id, int64_t value) { ABSL_ASSIGN_OR_RETURN(int64_t old, shared_ctx_.xpress->GetIntControl64(id)); modified_controls_.push_back({id, old}); @@ -347,8 +1188,12 @@ class ScopedSolverContext { return absl::OkStatus(); } - absl::Status AddCallbacks(SolverInterface::MessageCallback message_callback, - const SolveInterrupter* interrupter) { + absl::Status AddCallbacks( + SolverInterface::MessageCallback message_callback, + const CallbackRegistrationProto& callback_registration, + SolverInterface::Callback callback, const SolveInterrupter* interrupter, + absl::linked_hash_map const& varMap) { if (message_callback) ABSL_RETURN_IF_ERROR( message_callback_.Add(&shared_ctx_, message_callback)); @@ -363,9 +1208,81 @@ class ScopedSolverContext { interrupter_callback_ = std::make_unique( interrupter, [this] { CHECK_OK(shared_ctx_.xpress->Interrupt(XPRS_STOP_USER)); }); - /** @TODO Support CallbackRegistrationProto and Callback and install the - * ortools callback as required. Note that this is only for Solve(), not - * for ComputeInfeasibleSubsystem() */ + } + if (callback) { + absl::flat_hash_set const events = + EventSet(callback_registration); + + // If the user wants to add cuts or listen to CALLBACK_EVENT_MIP_NODE + // then we need XPRSaddcbcutround(). + if ((events.contains(CALLBACK_EVENT_MIP_NODE) || + callback_registration.add_cuts()) && + !XPRSaddcbcutround) { + return absl::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "Callback will add cuts or listen for " + "CALLBACK_EVENT_MIP_NODE but XPRSaddcbcutround() is not " + "available. Need at least Xpress optimizer version 45 " + "(Xpress 9.6.0)"; + } + + // If the user wants to add cuts but XPRSaddmanagedcuts() is not available + // then we error out. + if (callback_registration.add_cuts() && !XPRSaddmanagedcuts64) { + return absl::StatusBuilder(absl::StatusCode::kInvalidArgument) + << "Callback will add cuts but XPRSaddmanagedcuts64() is not " + "available. Need at least Xpress optimizer version 45 " + "(Xpress 9.6.0)"; + } + + ctx = std::make_unique( + callback, callback_registration, varMap); + + // Register the callbacks that we need to handle the required events. + // If we listen to any MIP event but CALLBACK_EVENT_MIP_NODE then we + // may have to flush info at the end of a node. + bool needFlush = false; + for (auto const& event : events) { + switch (event) { + case CALLBACK_EVENT_PRESOLVE: + ABSL_RETURN_IF_ERROR( + presolve_callback_.Add(&shared_ctx_, ctx.get())); + break; + case CALLBACK_EVENT_SIMPLEX: + ABSL_RETURN_IF_ERROR(lplog_callback_.Add(&shared_ctx_, ctx.get())); + break; + case CALLBACK_EVENT_MIP: + ABSL_RETURN_IF_ERROR( + prenode_callback_.Add(&shared_ctx_, ctx.get())); + needFlush = true; + break; + case CALLBACK_EVENT_MIP_SOLUTION: + ABSL_RETURN_IF_ERROR( + preintsol_callback_.Add(&shared_ctx_, ctx.get())); + needFlush = true; + break; + case CALLBACK_EVENT_MIP_NODE: + ABSL_RETURN_IF_ERROR( + cutround_callback_.Add(&shared_ctx_, ctx.get())); + break; + case CALLBACK_EVENT_BARRIER: + ABSL_RETURN_IF_ERROR(barlog_callback_.Add(&shared_ctx_, ctx.get())); + break; + case CALLBACK_EVENT_UNSPECIFIED: // fallthrough + default: + LOG(FATAL) << "Unsupported callback event: " << event; + } + } + + if (needFlush) { + ABSL_RETURN_IF_ERROR(optnode_callback_.Add(&shared_ctx_, ctx.get())); + } + + // If the user plans to ever add lazy constraints from callbacks then we + // must disable dual reductions. + if (callback_registration.add_lazy_constraints()) { + ABSL_RETURN_IF_ERROR( + shared_ctx_.xpress->SetIntControl(XPRS_MIPDUALREDUCTIONS, 0)); + } } return absl::OkStatus(); } @@ -615,6 +1532,10 @@ class ScopedSolverContext { shared_ctx_.xpress->GetIntAttr(XPRS_ORIGINALCOLS)); ABSL_ASSIGN_OR_RETURN(int const rows, shared_ctx_.xpress->GetIntAttr(XPRS_ORIGINALROWS)); + int majorVersion = -1; + ABSL_RETURN_IF_ERROR( + Xpress::GetVersionNumbers(&majorVersion, nullptr, nullptr)); + // Set initial basis if (model_parameters.has_initial_basis()) { // XPRSloadbasis() will raise an error if called on a model in presolved @@ -630,14 +1551,13 @@ class ScopedSolverContext { 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)] = + xpress_var_basis_status[or2xprs(variables_map, 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] = + xpress_constr_basis_status[or2xprs(linear_constraints_map, id)] = MathOptToXpressBasisStatus(static_cast(value), true); } @@ -658,7 +1578,7 @@ class ScopedSolverContext { colind.clear(); mipStart.clear(); for (const auto [id, value] : MakeView(hint.variable_values())) { - colind.push_back(variables_map.at(id)); + colind.push_back(or2xprs(variables_map, id)); mipStart.push_back(value); } if (mipStart.size() > cols) @@ -673,21 +1593,40 @@ class ScopedSolverContext { // Install branching priorities. if (model_parameters.has_branching_priorities()) { + // XPRSloaddirs() will raise an error if called on a model in presolved + // state. We still trap this already here because otherwise dimensions + // do not match and we may produce an out-of-bounds write while setting + // up the function arguments. + ABSL_ASSIGN_OR_RETURN(int const state, + shared_ctx_.xpress->GetIntAttr(XPRS_PRESOLVESTATE)); + if (state & ((1 << 1) | (1 << 2))) { + return ortools::InvalidArgumentErrorBuilder() + << "cannot set branching priorities for model in presolved " + "space (consider " + "FORCE_POSTSOLVE?)"; + } + // Things to observe here: + // - Xpress only allows priorities in [0,1000]. + // - In ortools higher priority takes precedence while in Xpress + // lower priority takes precedence. + // - The specification of `branching_priorities` explicitly requires + // that variables not in `branching_priorities` are set to default + // priority. So we must set priorities for _all_ columns to satisfy + // this, especially for incremental/repeated solves. auto const& prios = model_parameters.branching_priorities(); colind.clear(); - colind.reserve(prios.ids_size()); - std::vector priority; + if (colind.capacity() < cols) colind.reserve(cols); + for (int j = 0; j < cols; ++j) colind.push_back(j); + std::vector priority(cols, 1000); priority.reserve(prios.ids_size()); for (const auto [id, prio] : MakeView(prios)) { - colind.push_back(variables_map.at(id)); - // 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 ortools::InvalidArgumentErrorBuilder() << "Xpress only allows branching priorities in [0,1000]"; - priority.push_back( - 1000 - prio); // Smaller prios have higher precedence in Xpress! + int j = or2xprs(variables_map, id); + CHECK_LT(j, cols); + priority[j] = + 1000 - prio; // Smaller prios have higher precedence in Xpress! } ABSL_RETURN_IF_ERROR(shared_ctx_.xpress->LoadDirs( @@ -712,16 +1651,32 @@ class ScopedSolverContext { 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.empty()) { - return ortools::InvalidArgumentErrorBuilder() - << "Xpress does not support per-objective time limits"; - } ABSL_ASSIGN_OR_RETURN(auto l, util_time::DecodeGoogleApiProto(p.time_limit())); - - ABSL_RETURN_IF_ERROR(shared_ctx_.xpress->SetDblControl( - XPRS_TIMELIMIT, absl::ToDoubleSeconds(l))); + double const newTimeLimit = absl::ToDoubleSeconds(l); + if (objectives_map.empty()) { + // Single objective. Add this as global time limit, but only if it is + // tighter than any time limit that we might have already installed + // (via global time limit parameter). + ABSL_ASSIGN_OR_RETURN( + auto globalTimeLimit, + shared_ctx_.xpress->GetDblControl(XPRS_TIMELIMIT)); + double newTimeLimit = absl::ToDoubleSeconds(l); + if (newTimeLimit < globalTimeLimit) { + ABSL_RETURN_IF_ERROR(shared_ctx_.xpress->SetDblControl( + XPRS_TIMELIMIT, newTimeLimit)); + } + } else { + // Before Xpress 9.9 (optimizer version 47) we support a per objective + // time limit only if there is one single objective. + if (majorVersion < 47) { + return ortools::InvalidArgumentErrorBuilder() + << "Xpress does not support per-objective time limits " + "before version 9.9 (optimizer version 47)"; + } + ABSL_RETURN_IF_ERROR(shared_ctx_.xpress->SetObjectiveDoubleControl( + 0, XPRS_TIMELIMIT, newTimeLimit)); + } } } // Objective parameters: auxiliary objectives @@ -729,17 +1684,28 @@ class ScopedSolverContext { model_parameters.auxiliary_objective_parameters()) { if (p.has_objective_degradation_absolute_tolerance()) { ABSL_RETURN_IF_ERROR(shared_ctx_.xpress->SetObjectiveDoubleControl( - objectives_map.at(id), XPRS_OBJECTIVE_ABSTOL, + or2xprs(objectives_map, id), XPRS_OBJECTIVE_ABSTOL, p.objective_degradation_absolute_tolerance())); } if (p.has_objective_degradation_relative_tolerance()) { ABSL_RETURN_IF_ERROR(shared_ctx_.xpress->SetObjectiveDoubleControl( - objectives_map.at(id), XPRS_OBJECTIVE_RELTOL, + or2xprs(objectives_map, id), XPRS_OBJECTIVE_RELTOL, p.objective_degradation_relative_tolerance())); } if (p.has_time_limit()) { - return ortools::InvalidArgumentErrorBuilder() - << "Xpress does not support per-objective time limits"; + // Per-objective time limits are supported since Xpress 9.9 + // (optimizer version 47) + if (majorVersion < 47) { + return ortools::InvalidArgumentErrorBuilder() + << "Xpress does not support per-objective time limits before " + "version 9.9 (optimizer version 47)"; + } else { + ABSL_ASSIGN_OR_RETURN( + auto l, util_time::DecodeGoogleApiProto(p.time_limit())); + ABSL_RETURN_IF_ERROR(shared_ctx_.xpress->SetObjectiveDoubleControl( + or2xprs(objectives_map, id), XPRS_TIMELIMIT, + absl::ToDoubleSeconds(l))); + } } } @@ -747,7 +1713,7 @@ class ScopedSolverContext { 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); + delayedRows.push_back(or2xprs(linear_constraints_map, idx)); } if (delayedRows.size() > rows) return ortools::InvalidArgumentErrorBuilder() @@ -763,9 +1729,8 @@ class ScopedSolverContext { CHECK_OK(shared_ctx_.xpress->Interrupt(reason)); } - absl::Status GetCallbackStatus() { - const absl::MutexLock lock(shared_ctx_.mutex); - return shared_ctx_.callback_status; + absl::Status HandleCallbackProblems() { + return shared_ctx_.HandleCallbackProblems(); } ~ScopedSolverContext() { @@ -786,8 +1751,8 @@ class ScopedSolverContext { break; } } - // If pending callback exception was not reraised yet then do it now - CHECK_OK(shared_ctx_.callback_status); + // If pending callback exception was not reraised yet then do it now. + CHECK_OK(shared_ctx_.HandleCallbackProblems()); } }; @@ -905,21 +1870,33 @@ absl::Status AddNames(Xpress* xpress, int type, int offset, I begin, I end, T const& container) { std::vector buffer; int i = 0, start = 0; + bool allempty = true; while (begin != end) { + // Elements may have no names but there is no way for us to detect + // this. If no name is specified for an element then its name is set + // to "". This cannot be distinguished from the case in which one + // element explicitly has name "" and other elements have other names. + // Xpress will raise an error if two elements of the same type have the + // same name, in particular if all have an empty name. So we add some + // heuristic via allempty to detect the case in which none of them have + // names. This occurs for example in tests. std::string const& name = NameResolver::GetName(container, begin); char const* c_name = name.c_str(); + if (*c_name) allempty = false; buffer.insert(buffer.end(), c_name, c_name + name.size() + 1); // Add names in chunks of 1MB. if (buffer.size() > 1024 * 1024) { - ABSL_RETURN_IF_ERROR( - xpress->AddNames(type, buffer, offset + start, offset + i)); + if (!allempty) { + ABSL_RETURN_IF_ERROR( + xpress->AddNames(type, buffer, offset + start, offset + i)); + } start = i + 1; buffer.clear(); } ++i; ++begin; } - if (!buffer.empty()) { + if (!allempty && !buffer.empty()) { ABSL_RETURN_IF_ERROR( xpress->AddNames(type, buffer, offset + start, offset + i - 1)); } @@ -1027,9 +2004,11 @@ absl::Status XpressSolver::AddNewVariables( xpress_->AddVars(num_new_variables, {}, new_variables.lower_bounds(), new_variables.upper_bounds(), variable_type)); - ABSL_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_COLUMN, - num_old_variables, 0, num_new_variables, - new_variables)); + if (new_variables.names_size() > 0) { + ABSL_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_COLUMN, + num_old_variables, 0, num_new_variables, + new_variables)); + } return absl::OkStatus(); } @@ -1104,9 +2083,11 @@ absl::Status XpressSolver::AddNewLinearConstraints( // Add all constraints in one call. ABSL_RETURN_IF_ERROR( xpress_->AddConstrs(constraint_sense, constraint_rhs, constraint_rng)); - ABSL_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_ROW, - num_old_constraints, 0, num_new_constraints, - constraints)); + if (constraints.names_size() > 0) { + ABSL_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_ROW, + num_old_constraints, 0, num_new_constraints, + constraints)); + } return absl::OkStatus(); } @@ -1167,8 +2148,8 @@ absl::Status XpressSolver::AddObjective( const int64_t row_id = objective.quadratic_coefficients().row_ids(k); const int64_t column_id = objective.quadratic_coefficients().column_ids(k); - first_var_index[k] = variables_map_.at(row_id); - second_var_index[k] = variables_map_.at(column_id); + first_var_index[k] = or2xprs(variables_map_, row_id); + second_var_index[k] = or2xprs(variables_map_, column_id); // XPRESS supposes a 1/2 implicit multiplier to quadratic terms (see doc) // We have to multiply it by 2 for diagonal terms double m = first_var_index[k] == second_var_index[k] ? 2 : 1; @@ -1182,7 +2163,7 @@ absl::Status XpressSolver::AddObjective( 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)); + index.push_back(or2xprs(variables_map_, id)); } if (multiobj) { @@ -1205,7 +2186,7 @@ absl::Status XpressSolver::AddObjective( absl::MakeSpan(index), objective.linear_coefficients().values(), // checked above static_cast(-objective.priority()), weight)); - gtl::InsertOrDie(&objectives_map_, haveId, newid); + gtl::InsertOrDie(&objectives_map_, objective_id.value(), newid); } } else { ABSL_RETURN_IF_ERROR(xpress_->SetLinearObjective( @@ -1258,7 +2239,7 @@ absl::Status XpressSolver::AddSOS( ABSL_ASSIGN_OR_RETURN( std::optional x, ExtractSingleton(expr, SingletonType::SOS, nullptr)); - colind.push_back(variables_map_.at(x.value())); + colind.push_back(or2xprs(variables_map_, x.value())); refval.push_back(weight); } gtl::InsertOrDie(sosmap, sosId, nextId); @@ -1266,6 +2247,7 @@ absl::Status XpressSolver::AddSOS( } std::vector settype(start.size(), sos1 ? '1' : '2'); ABSL_RETURN_IF_ERROR(xpress_->AddSets(settype, start, colind, refval)); + // Note: SOS constraints always have names. ABSL_RETURN_IF_ERROR(AddNames(xpress_.get(), XPRS_NAMES_SET, num_old_sets, sets.begin(), sets.end(), sets)); return absl::OkStatus(); @@ -1281,7 +2263,7 @@ void XpressSolver::ExtractLinear(SparseDoubleVectorProto const& expr, 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))); + colind.push_back(or2xprs(variables_map_, expr.ids(i))); coef.push_back(expr.values(i)); } } @@ -1300,7 +2282,7 @@ void XpressSolver::ExtractQuadratic(QuadraticConstraintProto const& expr, 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_colind.push_back(or2xprs(variables_map_, lin.ids(i))); lin_coef.push_back(lin.values(i)); } auto const& quad = expr.quadratic_terms(); @@ -1309,8 +2291,8 @@ void XpressSolver::ExtractQuadratic(QuadraticConstraintProto const& expr, 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)); + int const col1 = or2xprs(variables_map_, quad.row_ids(i)); + int const col2 = or2xprs(variables_map_, quad.column_ids(i)); double coef = quad.coefficients(i); if (col1 != col2) coef *= 0.5; quad_col1.push_back(col1); @@ -1357,7 +2339,7 @@ absl::Status XpressSolver::AddIndicators( i_rowind[next] = oldRows + next; if (indicator.has_indicator_id()) { - i_colind[next] = variables_map_.at(indicator.indicator_id()); + i_colind[next] = or2xprs(variables_map_, indicator.indicator_id()); if (i_colind[next] < min_icol) min_icol = i_colind[next]; if (i_colind[next] > max_icol) max_icol = i_colind[next]; i_complement[next] = indicator.activate_on_zero() ? -1 : 1; @@ -1504,7 +2486,7 @@ absl::Status XpressSolver::AddSecondOrderConeConstraints( ABSL_ASSIGN_OR_RETURN(std::optional const x0, ExtractSingleton(ub, SingletonType::SOCBound, &coef)); if (x0.has_value()) { - cols.push_back(variables_map_.at(x0.value())); + cols.push_back(or2xprs(variables_map_, x0.value())); coefs.push_back(-coef * coef); } else { rhs = coef * coef; @@ -1514,7 +2496,7 @@ absl::Status XpressSolver::AddSecondOrderConeConstraints( ABSL_ASSIGN_OR_RETURN( std::optional const x, ExtractSingleton(arg, SingletonType::SOCNorm, &coef)); - cols.push_back(variables_map_.at(x.value())); + cols.push_back(or2xprs(variables_map_, x.value())); coefs.push_back(coef * coef); } ABSL_RETURN_IF_ERROR( @@ -1536,9 +2518,8 @@ absl::Status XpressSolver::ChangeCoefficients( std::vector col_index; col_index.reserve(num_coefficients); for (int k = 0; k < num_coefficients; ++k) { - row_index.push_back( - linear_constraints_map_.at(matrix.row_ids(k)).constraint_index); - col_index.push_back(variables_map_.at(matrix.column_ids(k))); + row_index.push_back(or2xprs(linear_constraints_map_, matrix.row_ids(k))); + col_index.push_back(or2xprs(variables_map_, matrix.column_ids(k))); } return xpress_->ChgCoeffs(row_index, col_index, matrix.coefficients()); } @@ -1547,8 +2528,8 @@ absl::StatusOr XpressSolver::Solve( const SolveParametersProto& parameters, const ModelSolveParametersProto& model_parameters, SolverInterface::MessageCallback message_callback, - const CallbackRegistrationProto& callback_registration, - Callback /*callback*/, const SolveInterrupter* absl_nullable interrupter) { + const CallbackRegistrationProto& callback_registration, Callback callback, + const SolveInterrupter* absl_nullable interrupter) { force_postsolve_ = false; primal_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND; @@ -1560,8 +2541,9 @@ absl::StatusOr XpressSolver::Solve( ABSL_ASSIGN_OR_RETURN(is_mip_, xpress_->IsMIP()); const absl::Time start = absl::Now(); - ABSL_RETURN_IF_ERROR(CheckRegisteredCallbackEvents(callback_registration, - /*supported_events=*/{})); + ABSL_RETURN_IF_ERROR(CheckRegisteredCallbackEvents( + callback_registration, + is_mip_ ? SupportedMIPEvents_ : SupportedLPEvents_)); // Check that bounds are not inverted just before solve // XPRESS returns "infeasible" when bounds are inverted @@ -1580,7 +2562,8 @@ absl::StatusOr XpressSolver::Solve( // exception has been thrown during optimization. ScopedSolverContext solveContext(xpress_.get()); ABSL_RETURN_IF_ERROR( - solveContext.AddCallbacks(message_callback, interrupter)); + solveContext.AddCallbacks(message_callback, callback_registration, + callback, interrupter, variables_map_)); std::string export_model = ""; ABSL_RETURN_IF_ERROR( solveContext.ApplyParameters(parameters, message_callback, &export_model, @@ -1612,7 +2595,7 @@ absl::StatusOr XpressSolver::Solve( // 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). - ABSL_RETURN_IF_ERROR(solveContext.GetCallbackStatus()); + ABSL_RETURN_IF_ERROR(solveContext.HandleCallbackProblems()); ABSL_RETURN_IF_ERROR( xpress_->GetSolution(&primal_sol_avail_, std::nullopt, 0, -1)); ABSL_RETURN_IF_ERROR( @@ -2180,7 +3163,8 @@ XpressSolver::ComputeInfeasibleSubsystem( ABSL_RETURN_IF_ERROR(xpress_->PostSolve()) << "XPRSpostsolve() failed"; ScopedSolverContext solveContext(xpress_.get()); ABSL_RETURN_IF_ERROR( - solveContext.AddCallbacks(message_callback, interrupter)); + solveContext.AddCallbacks(message_callback, CallbackRegistrationProto(), + nullptr, interrupter, variables_map_)); ABSL_RETURN_IF_ERROR(solveContext.ApplyParameters( parameters, message_callback, nullptr, nullptr, nullptr)); diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 1069e888980..16043bb3f94 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -105,6 +105,10 @@ class XpressSolver : public SolverInterface { return value < kPlusInf && value > kMinusInf; } + private: + static absl::flat_hash_set const SupportedMIPEvents_; + static absl::flat_hash_set const SupportedLPEvents_; + absl::StatusOr ExtractSolveResultProto( absl::Time start, const ModelSolveParametersProto& model_parameters, const SolveParametersProto& solve_parameters); diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 67517f34769..d72ab0ff8aa 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -54,6 +54,53 @@ namespace math_opt { namespace { using testing::ValuesIn; +/** Supported callback events. + * Xpress supports all callback events. + */ +absl::flat_hash_set SupportedEvents() { + return {CallbackEvent::kPresolve, CallbackEvent::kSimplex, + CallbackEvent::kMip, CallbackEvent::kMipSolution, + CallbackEvent::kMipNode, CallbackEvent::kBarrier}; +} + +/** Supported callback events for tests with integer_variables=false. + * Even though Xpress supports setting the callbacks for non-MIP (they + * will just not be invoked), the tests are not prepared for this and will + * fail if the callback is supported and integer_variables=false. + */ +absl::flat_hash_set SupportedEventsNoMip() { + return {CallbackEvent::kPresolve, CallbackEvent::kSimplex, + CallbackEvent::kBarrier}; +} + +/** Parameter settings to make sure we reach a callback that allows injection + * of cuts. + */ +SolveParameters ReachesCutCallback() { + SolveParameters params; + params.xpress.param_values["PRESOLVE"] = "0"; + params.xpress.param_values["COVERCUTS"] = "0"; + params.xpress.param_values["GOMCUTS"] = "0"; + params.xpress.param_values["TREECOVERCUTS"] = "0"; + params.xpress.param_values["TREEGOMCUTS"] = "0"; + params.xpress.param_values["HEUREMPHASIS"] = "0"; + return params; +} + +/** Parameters that we must set for Xpress for every callback test. + */ +SolveParameters CallbackTestXpressParams() { + SolveParameters params; + // By default, Xpress does not trigger the lplog callback for every simplex + // iteration since that results in quite some overhead. We have to force + // invocation after each iteration to pass the tests. + params.xpress.param_values["LPLOGSTYLE"] = "0"; + params.xpress.param_values["LPLOG"] = "1"; + // Never run concurrent + params.xpress.param_values["CONCURRENTTHREADS"] = "0"; + return params; +} + INSTANTIATE_TEST_SUITE_P( XpressSolverLpTest, SimpleLpTest, testing::Values(SimpleLpTestParameters( @@ -128,15 +175,17 @@ INSTANTIATE_TEST_SUITE_P( {CallbackTestParams(SolverType::kXpress, TestModelClass::kLp, /*add_lazy_constraints=*/false, /*add_cuts=*/false, - /*supported_events=*/{}, + /*supported_events=*/SupportedEventsNoMip(), /*all_solutions=*/std::nullopt, - /*reaches_cut_callback*/ std::nullopt), + /*reaches_cut_callback*/ std::nullopt, + /*solve_parameters*/ CallbackTestXpressParams()), CallbackTestParams(SolverType::kXpress, TestModelClass::kIp, - /*add_lazy_constraints=*/false, - /*add_cuts=*/false, - /*supported_events=*/{}, + /*add_lazy_constraints=*/true, + /*add_cuts=*/true, + /*supported_events=*/SupportedEvents(), /*all_solutions=*/std::nullopt, - /*reaches_cut_callback*/ std::nullopt)})); + /*reaches_cut_callback*/ ReachesCutCallback(), + /*solve_parameters*/ CallbackTestXpressParams())})); INSTANTIATE_TEST_SUITE_P( XpressInvalidInputTest, InvalidInputTest, diff --git a/ortools/third_party_solvers/xpress_environment.cc b/ortools/third_party_solvers/xpress_environment.cc index 27268767f55..39678e9675e 100644 --- a/ortools/third_party_solvers/xpress_environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -67,6 +67,7 @@ std::function XPRSgetintcon std::function XPRSgetdblcontrol = nullptr; std::function XPRSgetstringcontrol = nullptr; std::function XPRSgetintattrib = nullptr; +std::function XPRSgetintattrib64 = nullptr; std::function XPRSgetstringattrib = nullptr; std::function XPRSgetdblattrib = nullptr; std::function XPRSgetobjdblattrib = nullptr; @@ -81,6 +82,7 @@ std::function XPRSgetcoef std::function XPRSgetsolution = nullptr; std::function XPRSgetduals = nullptr; std::function XPRSgetredcosts = nullptr; +std::function XPRSgetcallbacksolution = nullptr; std::function XPRSaddrows = nullptr; std::function XPRSaddrows64 = nullptr; std::function XPRSdelrows = nullptr; @@ -90,6 +92,8 @@ std::function XPRSaddnames = nullptr; std::function XPRSgetnames = nullptr; std::function XPRSaddsets64 = nullptr; +std::function XPRSaddmanagedcuts64 = nullptr; +std::function XPRSaddcuts64 = nullptr; std::function XPRSdelcols = nullptr; std::function XPRSchgcoltype = nullptr; std::function XPRSloadbasis = nullptr; @@ -123,9 +127,24 @@ std::function XPRSremovecbmessage = nullptr; std::function XPRSaddcbchecktime = nullptr; std::function XPRSremovecbchecktime = nullptr; +std::function XPRSaddcbbarlog = nullptr; +std::function XPRSremovecbbarlog = nullptr; +std::function XPRSaddcblplog = nullptr; +std::function XPRSremovecblplog = nullptr; +std::function XPRSaddcbpresolve = nullptr; +std::function XPRSremovecbpresolve = nullptr; +std::functionXPRSaddcbprenode = nullptr; +std::function XPRSremovecbprenode = nullptr; +std::function XPRSaddcbpreintsol = nullptr; +std::function XPRSremovecbpreintsol = nullptr; +std::function XPRSaddcboptnode = nullptr; +std::function XPRSremovecboptnode = nullptr; +std::function XPRSaddcbcutround = nullptr; +std::function XPRSremovecbcutround = nullptr; std::function XPRSlpoptimize = nullptr; std::function XPRSmipoptimize = nullptr; std::function XPRSoptimize = nullptr; +std::function XPRSpresolverow = nullptr; // clang-format on // NOLINTEND(google3-runtime-global-variables) // NOLINTEND(whitespace/line_length) @@ -160,6 +179,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSgetdblcontrol, "XPRSgetdblcontrol"); xpress_dynamic_library->GetFunction(&XPRSgetstringcontrol, "XPRSgetstringcontrol"); xpress_dynamic_library->GetFunction(&XPRSgetintattrib, "XPRSgetintattrib"); + xpress_dynamic_library->GetFunction(&XPRSgetintattrib64, "XPRSgetintattrib64"); xpress_dynamic_library->GetFunction(&XPRSgetstringattrib, "XPRSgetstringattrib"); xpress_dynamic_library->GetFunction(&XPRSgetdblattrib, "XPRSgetdblattrib"); xpress_dynamic_library->GetFunction(&XPRSgetobjdblattrib, "XPRSgetobjdblattrib"); @@ -174,6 +194,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSgetsolution, "XPRSgetsolution"); xpress_dynamic_library->GetFunction(&XPRSgetduals, "XPRSgetduals"); xpress_dynamic_library->GetFunction(&XPRSgetredcosts, "XPRSgetredcosts"); + xpress_dynamic_library->GetFunction(&XPRSgetcallbacksolution, "XPRSgetcallbacksolution"); xpress_dynamic_library->GetFunction(&XPRSaddrows, "XPRSaddrows"); xpress_dynamic_library->GetFunction(&XPRSaddrows64, "XPRSaddrows64"); xpress_dynamic_library->GetFunction(&XPRSdelrows, "XPRSdelrows"); @@ -184,6 +205,7 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSaddnames, "XPRSaddnames"); xpress_dynamic_library->GetFunction(&XPRSgetnames, "XPRSgetnames"); xpress_dynamic_library->GetFunction(&XPRSaddsets64, "XPRSaddsets64"); + xpress_dynamic_library->GetFunction(&XPRSaddcuts64, "XPRSaddcuts64"); xpress_dynamic_library->GetFunction(&XPRSdelcols, "XPRSdelcols"); xpress_dynamic_library->GetFunction(&XPRSchgcoltype, "XPRSchgcoltype"); xpress_dynamic_library->GetFunction(&XPRSloadbasis, "XPRSloadbasis"); @@ -217,9 +239,35 @@ void LoadXpressFunctions(DynamicLibrary* xpress_dynamic_library) { xpress_dynamic_library->GetFunction(&XPRSremovecbmessage, "XPRSremovecbmessage"); xpress_dynamic_library->GetFunction(&XPRSaddcbchecktime, "XPRSaddcbchecktime"); xpress_dynamic_library->GetFunction(&XPRSremovecbchecktime, "XPRSremovecbchecktime"); + + xpress_dynamic_library->GetFunction(&XPRSaddcbbarlog, "XPRSaddcbbarlog"); + xpress_dynamic_library->GetFunction(&XPRSremovecbbarlog, "XPRSremovecbbarlog"); + xpress_dynamic_library->GetFunction(&XPRSaddcblplog, "XPRSaddcblplog"); + xpress_dynamic_library->GetFunction(&XPRSremovecblplog, "XPRSremovecblplog"); + xpress_dynamic_library->GetFunction(&XPRSaddcbpresolve, "XPRSaddcbpresolve"); + xpress_dynamic_library->GetFunction(&XPRSremovecbpresolve, "XPRSremovecbpresolve"); + xpress_dynamic_library->GetFunction(&XPRSaddcbprenode, "XPRSaddcbprenode"); + xpress_dynamic_library->GetFunction(&XPRSremovecbprenode,"XPRSremovecbprenode"); + xpress_dynamic_library->GetFunction(&XPRSaddcbpreintsol, "XPRSaddcbpreintsol"); + xpress_dynamic_library->GetFunction(&XPRSremovecbpreintsol, "XPRSremovecbpreintsol"); + xpress_dynamic_library->GetFunction(&XPRSaddcboptnode, "XPRSaddcboptnode"); + xpress_dynamic_library->GetFunction(&XPRSremovecboptnode, "XPRSremovecboptnode"); + xpress_dynamic_library->GetFunction(&XPRSlpoptimize, "XPRSlpoptimize"); xpress_dynamic_library->GetFunction(&XPRSmipoptimize, "XPRSmipoptimize"); xpress_dynamic_library->GetFunction(&XPRSoptimize, "XPRSoptimize"); + xpress_dynamic_library->GetFunction(&XPRSpresolverow, "XPRSpresolverow"); + // Get version numbers to load things that are not available for all versions + // we support here. Ignore any error since the function should not error + // anyway. + int major = -1, minor = -1, build = -1; + XPRSgetversionnumbers(&major, &minor, &build); + if (major >= 45) { + // Xpress 9.6.0 and higher + xpress_dynamic_library->GetFunction(&XPRSaddmanagedcuts64, "XPRSaddmanagedcuts64"); + xpress_dynamic_library->GetFunction(&XPRSaddcbcutround, "XPRSaddcbcutround"); + xpress_dynamic_library->GetFunction(&XPRSremovecbcutround, "XPRSremovecbcutround"); + } // clang-format on // NOLINTEND(whitespace/line_length) } diff --git a/ortools/third_party_solvers/xpress_environment.h b/ortools/third_party_solvers/xpress_environment.h index cdd060ea838..e059b4b06ce 100644 --- a/ortools/third_party_solvers/xpress_environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -118,8 +118,12 @@ 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_CUTS 1012 +#define XPRS_ACTIVENODES 1015 +#define XPRS_MIPSOLS 1017 #define XPRS_PRESOLVESTATE 1026 #define XPRS_MIPENTS 1032 +#define XPRS_MIPTHREADID 1037 #define XPRS_ALGORITHM 1049 #define XPRS_STOPSTATUS 1179 #define XPRS_SOLVESTATUS 1394 @@ -131,8 +135,13 @@ absl::Status LoadXpressDynamicLibrary(std::string& xpresspath); #define XPRS_ORIGINALSETS 1194 #define XPRS_ORIGINALINDICATORS 1255 #define XPRS_OPTIMIZETYPEUSED 1268 +#define XPRS_SUMPRIMALINF 2002 +#define XPRS_MIPBESTOBJVAL 2006 #define XPRS_OBJVAL 2118 #define XPRS_BARPRIMALOBJ 4001 +#define XPRS_BARPRIMALINF 4003 +#define XPRS_BARDUALINF 4004 +#define XPRS_BARCGAP 4005 #define XPRS_BARDUALOBJ 4002 #define XPRS_MPSRHSNAME 6001 #define XPRS_MPSOBJNAME 6002 @@ -551,6 +560,7 @@ OR_DLL extern std::function OR_DLL extern std::function XPRSgetdblcontrol; OR_DLL extern std::function XPRSgetstringcontrol; OR_DLL extern std::function XPRSgetintattrib; +OR_DLL extern std::function XPRSgetintattrib64; OR_DLL extern std::function XPRSgetstringattrib; OR_DLL extern std::function XPRSgetdblattrib; extern std::function XPRSgetobjdblattrib; @@ -565,6 +575,7 @@ OR_DLL extern std::function XPRSgetsolution; extern std::function XPRSgetduals; extern std::function XPRSgetredcosts; +extern std::function XPRSgetcallbacksolution; extern std::function XPRSaddrows; extern std::function XPRSaddrows64; extern std::function XPRSdelrows; @@ -575,6 +586,8 @@ extern std::function XPRSgetnames; extern std::function XPRSdelcols; extern std::function XPRSaddsets64; +extern std::function XPRSaddmanagedcuts64; +extern std::function XPRSaddcuts64; extern std::function XPRSchgcoltype; extern std::function XPRSloadbasis; extern std::function XPRSpostsolve; @@ -607,9 +620,24 @@ extern std::function XPRSremovecbmessage; extern std::function XPRSaddcbchecktime; extern std::function XPRSremovecbchecktime; +extern std::function XPRSaddcbbarlog; +extern std::function XPRSremovecbbarlog; +extern std::function XPRSaddcblplog; +extern std::function XPRSremovecblplog; +extern std::function XPRSaddcbpresolve; +extern std::function XPRSremovecbpresolve; +extern std::functionXPRSaddcbprenode; +extern std::function XPRSremovecbprenode; +extern std::function XPRSaddcbpreintsol; +extern std::function XPRSremovecbpreintsol; +extern std::function XPRSaddcboptnode; +extern std::function XPRSremovecboptnode; +extern std::function XPRSaddcbcutround; +extern std::function XPRSremovecbcutround; extern std::function XPRSlpoptimize; extern std::function XPRSmipoptimize; extern std::function XPRSoptimize; +extern std::function XPRSpresolverow; // clang-format on // NOLINTEND(whitespace/line_length)