diff --git a/ortools/math_opt/callback.proto b/ortools/math_opt/callback.proto index bdbbeab455..76b5ce3d85 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 c01e349fe9..4059a77e53 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 1671b32afd..f2c79035d8 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 cdb5eba3e8..c22c3ae1d6 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 70b7506e29..740f65a808 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 886558e584..453ea2fdec 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 df205f36d6..d8661e508b 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 68fd24a034..5faecafaeb 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 c403a2c788..c013231d3e 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 6422b190af..c99cbb398d 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 c6cc30501b..05358b77da 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 f10b3d8df4..54b33902eb 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 1069e88898..16043bb3f9 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 67517f3476..d72ab0ff8a 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 27268767f5..39678e9675 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 cdd060ea83..e059b4b06c 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)