math_opt: backport from main

This commit is contained in:
Mizux Seiha
2025-12-09 22:49:43 +01:00
parent 1666cf41ab
commit 26b01437f4
22 changed files with 3024 additions and 678 deletions

View File

@@ -111,6 +111,7 @@ proto_library(
"//ortools/math_opt/solvers:highs_proto",
"//ortools/math_opt/solvers:osqp_proto",
"//ortools/math_opt/solvers/gscip:gscip_proto",
"//ortools/math_opt/solvers:xpress_proto",
"//ortools/pdlp:solvers_proto",
"//ortools/sat:sat_parameters_proto",
"@protobuf//:duration_proto",

View File

@@ -481,6 +481,8 @@ class Model {
// The `weights` are an implementation detail in the solver used to order the
// `expressions`; see the Gurobi documentation for more detail:
// https://www.gurobi.com/documentation/9.5/refman/constraints.html#subsubsection:SOSConstraints
// For Xpress see
// https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/XPRSaddsets.html
//
// These `weights` must either be empty or the same length as `expressions`.
// If it is empty, default weights of 1, 2, ... will be used.
@@ -540,6 +542,8 @@ class Model {
// The `weights` are an implementation detail in the solver used to order the
// `expressions`; see the Gurobi documentation for more detail:
// https://www.gurobi.com/documentation/9.5/refman/constraints.html#subsubsection:SOSConstraints
// For Xpress see
// https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/XPRSaddsets.html
//
// These `weights` must either be empty or the same length as `expressions`.
// If it is empty, default weights of 1, 2, ... will be used.

View File

@@ -32,6 +32,7 @@
#include "ortools/math_opt/solvers/glpk.pb.h"
#include "ortools/math_opt/solvers/gurobi.pb.h"
#include "ortools/math_opt/solvers/highs.pb.h"
#include "ortools/math_opt/solvers/xpress.pb.h"
#include "ortools/port/proto_utils.h"
#include "ortools/util/status_macros.h"
@@ -213,6 +214,25 @@ GlpkParameters GlpkParameters::FromProto(const GlpkParametersProto& proto) {
return result;
}
XpressParametersProto XpressParameters::Proto() const {
XpressParametersProto result;
for (const auto& [key, val] : param_values) {
XpressParametersProto::Parameter& p = *result.add_parameters();
p.set_name(key);
p.set_value(val);
}
return result;
}
XpressParameters XpressParameters::FromProto(
const XpressParametersProto& proto) {
XpressParameters result;
for (const XpressParametersProto::Parameter& p : proto.parameters()) {
result.param_values[p.name()] = p.value();
}
return result;
}
SolveParametersProto SolveParameters::Proto() const {
SolveParametersProto result;
result.set_enable_output(enable_output);
@@ -265,6 +285,7 @@ SolveParametersProto SolveParameters::Proto() const {
*result.mutable_pdlp() = pdlp;
*result.mutable_glpk() = glpk.Proto();
*result.mutable_highs() = highs;
*result.mutable_xpress() = xpress.Proto();
return result;
}
@@ -324,6 +345,7 @@ absl::StatusOr<SolveParameters> SolveParameters::FromProto(
result.pdlp = proto.pdlp();
result.glpk = GlpkParameters::FromProto(proto.glpk());
result.highs = proto.highs();
result.xpress = XpressParameters::FromProto(proto.xpress());
return result;
}

View File

@@ -254,6 +254,44 @@ struct GlpkParameters {
static GlpkParameters FromProto(const GlpkParametersProto& proto);
};
// Xpress specific parameters for solving. See
// https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/chapter7.html
// for a list of possible parameters (called "controls" in Xpress).
// In addition to all Xpress controls, the following special parameters are
// also supported:
// "EXPORT_MODEL"(string) If present then the low level Xpress model
// (the XPRSprob instance) is written to that file
// right before XPRSoptimize() is called. This can
// be useful for debugging.
// "FORCE_POSTSOLVE"(int) If set to a non-zero value then the low-level code
// will call XPRSpostsolve() right after calling
// XPRSoptimize(). If not set or set to zero then
// calling XPRSpostsolve() is delayed to the latest
// possible point in time to enable incremental
// solves.
// "STOP_AFTER_LP"(int) If set to a non-zero value then the solve will be
// stopped right after solving the root relaxation.
// This is the same as passing the ' l' (ell) flag
// to XPRSoptimize() and stops the process earlier
// than a limit like MAXNODE=0.
//
// Example use:
// XpressParameters xpress;
// xpress.param_values["BarIterLimit"] = "10";
//
// Parameters are applied in the following order:
// * Any parameters derived from ortools parameters (like LP algorithm).
// * param_values in iteration order (insertion order).
struct XpressParameters {
// Parameter name-value pairs to set in insertion order.
gtl::linked_hash_map<std::string, std::string> param_values;
XpressParametersProto Proto() const;
static XpressParameters FromProto(const XpressParametersProto& proto);
bool empty() const { return param_values.empty(); }
};
// Parameters to control a single solve.
//
// Contains both parameters common to all solvers, e.g. time_limit, and
@@ -330,7 +368,8 @@ struct SolveParameters {
// Solvers will typically not return more solutions than the solution limit,
// but this is not enforced by MathOpt, see also b/214041169.
//
// Currently supported for Gurobi and SCIP, and for CP-SAT only with value 1.
// Currently supported for Gurobi, Xpress and SCIP, and for CP-SAT only with
// value 1.
std::optional<int32_t> solution_limit;
// If unset, use the solver default. If set, it must be >= 1.
@@ -347,6 +386,7 @@ struct SolveParameters {
// - Gurobi: [0:GRB_MAXINT] (which as of Gurobi 9.0 is 2x10^9).
// - GSCIP: [0:2147483647] (which is MAX_INT or kint32max or 2^31-1).
// - GLOP: [0:2147483647] (same as above)
// - Xpress: Any 32bit signed integer is allowed
// In all cases, the solver will receive a value equal to:
// MAX(0, MIN(MAX_VALID_VALUE_FOR_SOLVER, random_seed)).
std::optional<int32_t> random_seed;
@@ -425,6 +465,7 @@ struct SolveParameters {
GlpkParameters glpk;
HighsOptionsProto highs;
XpressParameters xpress;
SolveParametersProto Proto() const;
static absl::StatusOr<SolveParameters> FromProto(

View File

@@ -18,6 +18,7 @@
#include "absl/status/statusor.h"
#include "ortools/math_opt/parameters.pb.h"
#include "ortools/math_opt/solvers/gurobi.pb.h"
#include "ortools/math_opt/solvers/xpress.pb.h"
namespace operations_research {
namespace math_opt {
@@ -60,12 +61,34 @@ StreamableGurobiInitArguments StreamableGurobiInitArguments::FromProto(
return args;
}
XpressInitializerProto StreamableXpressInitArguments::Proto() const {
XpressInitializerProto params_proto;
if (extract_names.has_value()) {
params_proto.set_extract_names(extract_names.value());
}
return params_proto;
}
StreamableXpressInitArguments StreamableXpressInitArguments::FromProto(
const XpressInitializerProto& args_proto) {
StreamableXpressInitArguments args;
if (args_proto.has_extract_names()) {
args.extract_names = args_proto.extract_names();
}
return args;
}
SolverInitializerProto StreamableSolverInitArguments::Proto() const {
SolverInitializerProto params_proto;
if (gurobi) {
*params_proto.mutable_gurobi() = gurobi->Proto();
}
if (xpress) {
*params_proto.mutable_xpress() = xpress->Proto();
}
return params_proto;
}
@@ -77,6 +100,9 @@ StreamableSolverInitArguments::FromProto(
if (args_proto.has_gurobi()) {
args.gurobi = StreamableGurobiInitArguments::FromProto(args_proto.gurobi());
}
if (args_proto.has_xpress()) {
args.xpress = StreamableXpressInitArguments::FromProto(args_proto.xpress());
}
return args;
}

View File

@@ -77,6 +77,22 @@ struct StreamableGurobiInitArguments {
const GurobiInitializerProto& args_proto);
};
// Streamable Xpress specific parameters for solver instantiation.
struct StreamableXpressInitArguments {
// If present and set to true then variable and constraint names are
// extracted into the underlying Xpress instance. This can help debugging
// (especially if models are exported to disk) but also incurs runtime and
// memory overhead.
std::optional<bool> extract_names;
// Returns the proto corresponding to these parameters.
XpressInitializerProto Proto() const;
// Parses the proto corresponding to these parameters.
static StreamableXpressInitArguments FromProto(
const XpressInitializerProto& args_proto);
};
// Solver initialization parameters that can be streamed to be exchanged with
// another process.
//
@@ -89,6 +105,7 @@ struct StreamableSolverInitArguments {
std::optional<StreamableGlopInitArguments> glop;
std::optional<StreamableGlpkInitArguments> glpk;
std::optional<StreamableGurobiInitArguments> gurobi;
std::optional<StreamableXpressInitArguments> xpress;
// Returns the proto corresponding to these parameters.
SolverInitializerProto Proto() const;

View File

@@ -24,6 +24,7 @@ import "ortools/math_opt/solvers/gscip/gscip.proto";
import "ortools/math_opt/solvers/gurobi.proto";
import "ortools/math_opt/solvers/highs.proto";
import "ortools/math_opt/solvers/osqp.proto";
import "ortools/math_opt/solvers/xpress.proto";
import "ortools/sat/sat_parameters.proto";
option java_package = "com.google.ortools.mathopt";
@@ -176,6 +177,7 @@ message StrictnessProto {
message SolverInitializerProto {
GurobiInitializerProto gurobi = 1;
reserved 2;
XpressInitializerProto xpress = 3;
}
// Parameters to control a single solve.
@@ -376,5 +378,7 @@ message SolveParametersProto {
HighsOptionsProto highs = 27;
XpressParametersProto xpress = 28;
reserved 11; // Deleted
}

View File

@@ -151,6 +151,17 @@ TEST_P(MipSolutionHintTest, TwoHintTest) {
if (!TwoHintParams().has_value()) {
GTEST_SKIP() << "Multiple hints not supported. Ignoring this test.";
}
if (GetParam().solver_type == SolverType::kXpress) {
// Xpress has no configuration options to "just complete" a partial
// solution hint. For an incomplete solution it will always run simple
// heuristics to find a solution. The effort of this heuristic can be
// controlled via the USERSOLHEURISTIC control, but both values
// 0 (off) and 1 (light) make the test fail: with off no heuristic
// is applied on the provided solution and hence the expected solutions
// are not found. With light the heuristic finds the optimal solution
// from the solution hint.
GTEST_SKIP() << "Xpress cannot be forced to only complete a solution.";
}
ModelSolveParameters model_parameters;
@@ -281,6 +292,12 @@ TEST_P(BranchPrioritiesTest, PrioritiesAreSetProperly) {
// See PrioritiesAreSetProperly for details on the model and solve parameters.
TEST_P(BranchPrioritiesTest, PrioritiesClearedAfterIncrementalSolve) {
if (GetParam().solver_type == SolverType::kXpress) {
// This test does not work with Xpress since Xpress does not clear/reset
// model parameters after a solve. See the comment in XpressSolver::Solve
// in xpress_solver.cc.
GTEST_SKIP() << "Xpress does not clear model parameters in Solve().";
}
Model model;
Variable x = model.AddContinuousVariable(-3.0, 1.0, "x");
Variable y = model.AddContinuousVariable(0.0, 3.0, "y");
@@ -400,6 +417,12 @@ TEST_P(LazyConstraintsTest, AnnotationsAreSetProperly) {
// without. If the annotations are cleared after the first, then we expect the
// second to solve the entire LP (including c and d), giving a dual bound of 0.
TEST_P(LazyConstraintsTest, AnnotationsAreClearedAfterSolve) {
if (GetParam().solver_type == SolverType::kXpress) {
// For the AnnotationsAreSetProperly we set STOP_AFTER_LP=1 which stops
// Xpress right after the relaxation. Since the same parameters are
// also used for the test here, this settings kills the test.
GTEST_SKIP() << "Xpress stops too early with shared parameter settings.";
}
Model model;
Variable x = model.AddIntegerVariable(-1, 1, "x");
Variable y = model.AddIntegerVariable(-1, 1, "y");

View File

@@ -36,7 +36,8 @@ LogicalConstraintTestParameters::LogicalConstraintTestParameters(
const bool supports_incremental_add_and_deletes,
const bool supports_incremental_variable_deletions,
const bool supports_deleting_indicator_variables,
const bool supports_updating_binary_variables)
const bool supports_updating_binary_variables,
const bool supports_sos_on_expressions)
: solver_type(solver_type),
parameters(std::move(parameters)),
supports_integer_variables(supports_integer_variables),
@@ -49,7 +50,8 @@ LogicalConstraintTestParameters::LogicalConstraintTestParameters(
supports_incremental_variable_deletions),
supports_deleting_indicator_variables(
supports_deleting_indicator_variables),
supports_updating_binary_variables(supports_updating_binary_variables) {}
supports_updating_binary_variables(supports_updating_binary_variables),
supports_sos_on_expressions(supports_sos_on_expressions) {}
std::ostream& operator<<(std::ostream& out,
const LogicalConstraintTestParameters& params) {
@@ -68,7 +70,9 @@ std::ostream& operator<<(std::ostream& out,
<< ", supports_deleting_indicator_variables: "
<< (params.supports_deleting_indicator_variables ? "true" : "false")
<< ", supports_updating_binary_variables: "
<< (params.supports_updating_binary_variables ? "true" : "false") << " }";
<< (params.supports_updating_binary_variables ? "true" : "false")
<< ", supports_sos_on_expressions: "
<< (params.supports_sos_on_expressions ? "true" : "false") << " }";
return out;
}
@@ -86,9 +90,21 @@ constexpr absl::string_view no_sos2_support_message =
constexpr absl::string_view no_indicator_support_message =
"This test is disabled as the solver does not support indicator "
"constraints";
constexpr absl::string_view no_updating_binary_variables_message =
"This test is disabled as the solver does not support updating "
"binary variables";
constexpr absl::string_view no_deleting_indicator_variables_message =
"This test is disabled as the solver does not support deleting "
"indicator variables";
constexpr absl::string_view no_incremental_add_and_deletes_message =
"This test is disabled as the solver does not support incremental "
"add/delete";
// We test SOS1 constraints with both explicit weights and default weights.
TEST_P(SimpleLogicalConstraintTest, CanBuildSos1Model) {
if (!GetParam().supports_sos_on_expressions) {
GTEST_SKIP() << "skipped since SOS on expressions are not supported";
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
model.AddSos1Constraint({3.0 * x + 2.0}, {3.0});
@@ -105,6 +121,9 @@ TEST_P(SimpleLogicalConstraintTest, CanBuildSos1Model) {
// We test SOS2 constraints with both explicit weights and default weights.
TEST_P(SimpleLogicalConstraintTest, CanBuildSos2Model) {
if (!GetParam().supports_sos_on_expressions) {
GTEST_SKIP() << "skipped since SOS on expressions are not supported";
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
model.AddSos2Constraint({3.0 * x + 2.0}, {3.0});
@@ -284,6 +303,9 @@ TEST_P(SimpleLogicalConstraintTest, Sos2VariableInMultipleTerms) {
// The optimal solution for the modified problem is (x*, y*) = (0, 1) with
// objective value 2.
TEST_P(IncrementalLogicalConstraintTest, LinearToSos1Update) {
if (!GetParam().supports_incremental_add_and_deletes) {
GTEST_SKIP() << no_incremental_add_and_deletes_message;
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -335,6 +357,9 @@ TEST_P(IncrementalLogicalConstraintTest, LinearToSos1Update) {
// The optimal solution for the modified problem is (x*, y*, z*) = (0, 1, 1)
// with objective value 4.
TEST_P(IncrementalLogicalConstraintTest, LinearToSos2Update) {
if (!GetParam().supports_incremental_add_and_deletes) {
GTEST_SKIP() << no_incremental_add_and_deletes_message;
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -760,6 +785,13 @@ TEST_P(SimpleLogicalConstraintTest, IndicatorsWithOddButValidBounds) {
if (!GetParam().supports_indicator_constraints) {
GTEST_SKIP() << no_indicator_support_message;
}
if (GetParam().solver_type == SolverType::kXpress) {
// This test does not work with Xpress because the Xpress library will
// refuse to create an indicator constraint if the indicator variable is
// not a binary variable (it will raise error 1029);
GTEST_SKIP() << "This test is disabled as Xpress does not support "
"indicator constraints on non-binary variables";
}
Model model;
const Variable x = model.AddIntegerVariable(0.0, 0.0, "x");
const Variable y = model.AddIntegerVariable(1.0, 1.0, "y");
@@ -855,6 +887,9 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesIndicatorConstraint) {
if (!GetParam().supports_indicator_constraints) {
GTEST_SKIP() << no_indicator_support_message;
}
if (!GetParam().supports_incremental_add_and_deletes) {
GTEST_SKIP() << no_incremental_add_and_deletes_message;
}
Model model;
const Variable x = model.AddBinaryVariable("x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -898,6 +933,9 @@ TEST_P(IncrementalLogicalConstraintTest,
if (!GetParam().supports_indicator_constraints) {
GTEST_SKIP() << no_indicator_support_message;
}
if (!GetParam().supports_incremental_add_and_deletes) {
GTEST_SKIP() << no_incremental_add_and_deletes_message;
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
const Variable indicator = model.AddBinaryVariable("indicator");
@@ -942,6 +980,9 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateDeletesIndicatorVariable) {
if (!GetParam().supports_indicator_constraints) {
GTEST_SKIP() << no_indicator_support_message;
}
if (!GetParam().supports_deleting_indicator_variables) {
GTEST_SKIP() << no_deleting_indicator_variables_message;
}
Model model;
const Variable x = model.AddBinaryVariable("x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -1022,6 +1063,9 @@ TEST_P(IncrementalLogicalConstraintTest,
if (!GetParam().supports_indicator_constraints) {
GTEST_SKIP() << no_indicator_support_message;
}
if (!GetParam().supports_updating_binary_variables) {
GTEST_SKIP() << no_updating_binary_variables_message;
}
Model model;
const Variable x = model.AddBinaryVariable("x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -1069,6 +1113,9 @@ TEST_P(IncrementalLogicalConstraintTest, UpdateChangesIndicatorVariableBound) {
if (!GetParam().supports_indicator_constraints) {
GTEST_SKIP() << no_indicator_support_message;
}
if (!GetParam().supports_updating_binary_variables) {
GTEST_SKIP() << no_updating_binary_variables_message;
}
Model model;
const Variable x = model.AddIntegerVariable(0.0, 1.0, "x");
const Variable y = model.AddIntegerVariable(0.0, 1.0, "y");
@@ -1133,6 +1180,9 @@ TEST_P(IncrementalLogicalConstraintTest,
if (!GetParam().supports_indicator_constraints) {
GTEST_SKIP() << no_indicator_support_message;
}
if (!GetParam().supports_updating_binary_variables) {
GTEST_SKIP() << no_updating_binary_variables_message;
}
Model model;
const Variable x = model.AddIntegerVariable(0.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");

View File

@@ -30,7 +30,8 @@ struct LogicalConstraintTestParameters {
bool supports_incremental_add_and_deletes,
bool supports_incremental_variable_deletions,
bool supports_deleting_indicator_variables,
bool supports_updating_binary_variables);
bool supports_updating_binary_variables,
bool supports_sos_on_expressions = true);
// The tested solver.
SolverType solver_type;
@@ -62,6 +63,10 @@ struct LogicalConstraintTestParameters {
// True if the solver supports updates (changing bounds or vartype) to binary
// variables.
bool supports_updating_binary_variables;
// True if the solver supports SOS constraints on expressions. False if
// SOS constraints are only supported on singleton variables.
bool supports_sos_on_expressions;
};
std::ostream& operator<<(std::ostream& out,

View File

@@ -360,6 +360,11 @@ TEST_P(LpParameterTest, IterationLimitFirstOrder) {
if (!SupportsFirstOrder()) {
GTEST_SKIP() << "First order methods not supported. Ignoring this test.";
}
if (GetParam().solver_type == SolverType::kXpress) {
// Xpress is too smart for the model just here. EVen with n=300 it solves
// the problem to optimality (within tolerances) in the first iteration.
GTEST_SKIP() << "Test skipped for Xpress since model solves too easily.";
}
ASSERT_OK_AND_ASSIGN(
const SolveResult result,
LPForIterationLimit(TestedSolver(), LPAlgorithm::kFirstOrder, 3,

View File

@@ -143,6 +143,12 @@ TEST_P(SimpleMipTest, FractionalBoundsContainNoInteger) {
// TODO(b/272298816): Gurobi bindings are broken here.
GTEST_SKIP() << "TODO(b/272298816): Gurobi bindings are broken here.";
}
if (GetParam().solver_type == SolverType::kXpress) {
// Xpress rounds bounds of integer variables on input, so the bounds
// specified here result in [1,0]. Xpress also checks that bounds are
// not contradicting, so it rejects creation of such a variable.
GTEST_SKIP() << "Xpress does not support contradictory bounds.";
}
Model model;
const Variable x = model.AddIntegerVariable(0.5, 0.6, "x");
model.Maximize(x);

View File

@@ -371,6 +371,10 @@ TEST_P(SimpleMultiObjectiveTest,
if (!GetParam().supports_integer_variables) {
GTEST_SKIP() << kNoIntegerVariableSupportMessage;
}
if (GetParam().solver_type == SolverType::kXpress) {
GTEST_SKIP() << "Ignoring this test because Xpress does not support per "
"objective time limits at the moment";
}
ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
Load23588MiplibInstance());
const Objective aux_obj = model->AddMaximizationObjective(
@@ -402,6 +406,10 @@ TEST_P(SimpleMultiObjectiveTest,
if (!GetParam().supports_integer_variables) {
GTEST_SKIP() << kNoIntegerVariableSupportMessage;
}
if (GetParam().solver_type == SolverType::kXpress) {
GTEST_SKIP() << "Ignoring this test because Xpress does not support per "
"objective time limits at the moment";
}
ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
Load23588MiplibInstance());
model->AddMaximizationObjective(0, /*priority=*/1);
@@ -463,6 +471,10 @@ TEST_P(SimpleMultiObjectiveTest,
if (!GetParam().supports_integer_variables) {
GTEST_SKIP() << kNoIntegerVariableSupportMessage;
}
if (GetParam().solver_type == SolverType::kXpress) {
GTEST_SKIP() << "Ignoring this test because Xpress does not support per "
"objective time limits at the moment";
}
ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
Load23588MiplibInstance());
SolveArguments args = {
@@ -583,6 +595,10 @@ TEST_P(IncrementalMultiObjectiveTest, AddObjectiveToMultiObjectiveModel) {
if (!GetParam().supports_auxiliary_objectives) {
GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
}
if (!GetParam().supports_incremental_objective_add_and_delete) {
GTEST_SKIP()
<< "Ignoring this test as it requires support for incremental solve";
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -636,6 +652,10 @@ TEST_P(IncrementalMultiObjectiveTest, DeleteObjectiveFromMultiObjectiveModel) {
if (!GetParam().supports_auxiliary_objectives) {
GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
}
if (!GetParam().supports_incremental_objective_add_and_delete) {
GTEST_SKIP()
<< "Ignoring this test as it requires support for incremental solve";
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");

View File

@@ -171,6 +171,13 @@ TEST_P(SimpleQcTest, SolveHalfEllipseQc) {
// Additionally, if we impose integrality on x, then the optimal solution is
// x = 0 with objective value 1.
TEST_P(IncrementalQcTest, LinearToQuadraticUpdate) {
if (GetParam().solver_type == SolverType::kXpress) {
// This test suffers from a bug in Xpress 9.6.0 (bug introduced in 9.6.0,
// fixed in 9.6.1).
// We have no easy way to check the Xpress version here, so we just skip
// the test.
GTEST_SKIP() << "Skip test due to bug in Xpress 9.6.0.";
}
Model model;
const Variable x =
model.AddVariable(0.0, 1.0, GetParam().use_integer_variables, "x");

View File

@@ -62,6 +62,9 @@ constexpr double kTolerance = 1.0e-3;
constexpr absl::string_view kNoSocSupportMessage =
"This test is disabled as the solver does not support second-order cone "
"constraints";
constexpr absl::string_view kNoIncrementalAddAndDeletes =
"This test is disabled as the solver does not support incremental add and "
"deletes";
// Builds the simple (and uninteresting) SOC model:
//
@@ -69,6 +72,14 @@ constexpr absl::string_view kNoSocSupportMessage =
// s.t. ||x||_2 <= 2x
// 0 <= x <= 1.
TEST_P(SimpleSecondOrderConeTest, CanBuildSecondOrderConeModel) {
if (GetParam().solver_type == SolverType::kXpress) {
// For Xpress the second order cone constraint results in
// x^2 - 4x^2 <= 0
// This has two entries for x and will thus be rejected by the library.
// Hence we have to skip the test.
GTEST_SKIP()
<< "This test is disabled as Xpress rejects duplicate Q entries";
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
model.AddSecondOrderConeConstraint({x}, 2 * x);
@@ -94,6 +105,10 @@ TEST_P(SimpleSecondOrderConeTest, SolveSimpleSocModel) {
if (!GetParam().supports_soc_constraints) {
GTEST_SKIP() << kNoSocSupportMessage;
}
if (GetParam().solver_type == SolverType::kXpress) {
GTEST_SKIP() << "This test is disabled as Xpress only supports second "
"order cone constraints on singletons";
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -117,6 +132,10 @@ TEST_P(SimpleSecondOrderConeTest, SolveMultipleSocConstraintModel) {
if (!GetParam().supports_soc_constraints) {
GTEST_SKIP() << kNoSocSupportMessage;
}
if (GetParam().solver_type == SolverType::kXpress) {
GTEST_SKIP() << "This test is disabled as Xpress only supports second "
"order cone constraints on singletons";
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -169,6 +188,9 @@ TEST_P(SimpleSecondOrderConeTest, SolveModelWithSocAndLinearConstraints) {
// The unique optimal solution is then (x*, y*) = (0.5, 0.5) with objective
// value 1.
TEST_P(IncrementalSecondOrderConeTest, LinearToSecondOrderConeUpdate) {
if (!GetParam().supports_incremental_add_and_deletes) {
GTEST_SKIP() << kNoIncrementalAddAndDeletes;
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -232,6 +254,9 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesSecondOrderConeConstraint) {
if (!GetParam().supports_soc_constraints) {
GTEST_SKIP() << kNoSocSupportMessage;
}
if (!GetParam().supports_incremental_add_and_deletes) {
GTEST_SKIP() << kNoIncrementalAddAndDeletes;
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -319,6 +344,9 @@ TEST_P(IncrementalSecondOrderConeTest,
if (!GetParam().supports_soc_constraints) {
GTEST_SKIP() << kNoSocSupportMessage;
}
if (!GetParam().supports_incremental_add_and_deletes) {
GTEST_SKIP() << kNoIncrementalAddAndDeletes;
}
Model model;
const Variable x = model.AddContinuousVariable(0.0, 2.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -361,6 +389,9 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesVariableThatIsAnArgument) {
if (!GetParam().supports_soc_constraints) {
GTEST_SKIP() << kNoSocSupportMessage;
}
if (!GetParam().supports_incremental_add_and_deletes) {
GTEST_SKIP() << kNoIncrementalAddAndDeletes;
}
Model model;
const Variable x = model.AddContinuousVariable(1.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
@@ -403,6 +434,9 @@ TEST_P(IncrementalSecondOrderConeTest, UpdateDeletesVariableInAnArgument) {
if (!GetParam().supports_soc_constraints) {
GTEST_SKIP() << kNoSocSupportMessage;
}
if (!GetParam().supports_incremental_add_and_deletes) {
GTEST_SKIP() << kNoIncrementalAddAndDeletes;
}
Model model;
const Variable x = model.AddContinuousVariable(1.0, 1.0, "x");
const Variable y = model.AddContinuousVariable(0.0, 2.0, "y");

View File

@@ -658,6 +658,23 @@ py_proto_library(
deps = [":highs_proto"],
)
proto_library(
name = "xpress_proto",
srcs = ["xpress.proto"],
visibility = ["//visibility:public"],
)
cc_proto_library(
name = "xpress_cc_proto",
visibility = ["//visibility:public"],
deps = [":xpress_proto"],
)
py_proto_library(
name = "xpress_py_pb2",
deps = [":xpress_proto"],
)
cc_library(
name = "xpress_solver",
srcs = [
@@ -683,6 +700,7 @@ cc_library(
"//ortools/math_opt/core:math_opt_proto_utils",
"//ortools/math_opt/core:solver_interface",
"//ortools/math_opt/core:sparse_vector_view",
"//ortools/math_opt/cpp:math_opt",
"//ortools/math_opt/solvers/xpress:g_xpress",
"//ortools/math_opt/validators:callback_validator",
"//ortools/port:proto_utils",
@@ -710,11 +728,13 @@ cc_test(
"//ortools/math_opt/solver_tests:generic_tests",
"//ortools/math_opt/solver_tests:infeasible_subsystem_tests",
"//ortools/math_opt/solver_tests:invalid_input_tests",
"//ortools/math_opt/solver_tests:ip_model_solve_parameters_tests",
"//ortools/math_opt/solver_tests:logical_constraint_tests",
"//ortools/math_opt/solver_tests:lp_incomplete_solve_tests",
"//ortools/math_opt/solver_tests:lp_model_solve_parameters_tests",
"//ortools/math_opt/solver_tests:lp_parameter_tests",
"//ortools/math_opt/solver_tests:lp_tests",
"//ortools/math_opt/solver_tests:mip_tests",
"//ortools/math_opt/solver_tests:multi_objective_tests",
"//ortools/math_opt/solver_tests:qc_tests",
"//ortools/math_opt/solver_tests:qp_tests",

View File

@@ -0,0 +1,59 @@
// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Proto messages specific to Xpress.
syntax = "proto3";
package operations_research.math_opt;
// Parameters used to initialize the Xpress solver.
message XpressInitializerProto {
optional bool extract_names = 1;
}
// Xpress specific parameters for solving. See
// https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/chapter7.html
// for a list of possible parameters (called "controls" in Xpress).
// In addition to all Xpress controls, the following special parameters are
// also supported:
// "EXPORT_MODEL"(string) If present then the low level Xpress model
// (the XPRSprob instance) is written to that file
// right before XPRSoptimize() is called. This can
// be useful for debugging.
// "FORCE_POSTSOLVE"(int) If set to a non-zero value then the low-level code
// will call XPRSpostsolve() right after calling
// XPRSoptimize(). If not set or set to zero then
// calling XPRSpostsolve() is delayed to the latest
// possible point in time to enable incremental
// solves.
// "STOP_AFTER_LP"(int) If set to a non-zero value then the solve will be
// stopped right after solving the root relaxation.
// This is the same as passing the ' l' (ell) flag
// to XPRSoptimize() and stops the process earlier
// than a limit like MAXNODE=0.
//
// Example use:
// XpressParameters xpress;
// xpress.param_values["BarIterLimit"] = "10";
//
// Parameters are applied in the following order:
// * Any parameters derived from ortools parameters (like LP algorithm).
// * param_values in iteration order (insertion order).
message XpressParametersProto {
message Parameter {
string name = 1;
string value = 2;
}
repeated Parameter parameters = 1;
}

View File

@@ -36,6 +36,11 @@ namespace operations_research::math_opt {
namespace {
bool checkInt32Overflow(const std::size_t value) { return value > INT32_MAX; }
template <typename T>
/** Forward an optional span to the C API. */
T* forwardSpan(std::optional<absl::Span<T>> const& span) {
return span.has_value() ? span.value().data() : nullptr;
}
} // namespace
constexpr int kXpressOk = 0;
@@ -64,7 +69,6 @@ absl::StatusOr<std::unique_ptr<Xpress>> Xpress::New(absl::string_view) {
CHECK(correctlyLoaded);
XPRSprob model;
CHECK_EQ(kXpressOk, XPRScreateprob(&model));
CHECK_EQ(kXpressOk, XPRSaddcbmessage(model, printXpressMessage, nullptr, 0));
return absl::WrapUnique(new Xpress(model));
}
@@ -77,11 +81,26 @@ absl::Status Xpress::SetProbName(absl::string_view name) {
return ToStatus(XPRSsetprobname(xpress_model_, truncated.c_str()));
}
void XPRS_CC Xpress::printXpressMessage(XPRSprob, void*, const char* sMsg, int,
int) {
if (sMsg) {
LOG(INFO) << sMsg;
}
absl::Status Xpress::AddCbMessage(void(XPRS_CC* cb)(XPRSprob, void*,
char const*, int, int),
void* cbdata, int prio) {
return ToStatus(XPRSaddcbmessage(xpress_model_, cb, cbdata, prio));
}
absl::Status Xpress::RemoveCbMessage(void(XPRS_CC* cb)(XPRSprob, void*,
char const*, int, int),
void* cbdata) {
return ToStatus(XPRSremovecbmessage(xpress_model_, cb, cbdata));
}
absl::Status Xpress::AddCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*),
void* cbdata, int prio) {
return ToStatus(XPRSaddcbchecktime(xpress_model_, cb, cbdata, prio));
}
absl::Status Xpress::RemoveCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*),
void* cbdata) {
return ToStatus(XPRSremovecbchecktime(xpress_model_, cb, cbdata));
}
Xpress::~Xpress() {
@@ -96,38 +115,67 @@ void Xpress::initIntControlDefaults() {
}
}
absl::Status Xpress::AddVars(const absl::Span<const double> obj,
const absl::Span<const double> lb,
const absl::Span<const double> ub,
const absl::Span<const char> vtype) {
return AddVars({}, {}, {}, obj, lb, ub, vtype);
absl::Status Xpress::AddNames(int type, absl::Span<const char> names, int first,
int last) {
return ToStatus(XPRSaddnames(xpress_model_, type, names.data(), first, last));
}
absl::Status Xpress::AddVars(const absl::Span<const int> vbegin,
const absl::Span<const int> vind,
const absl::Span<const double> vval,
// All span arguments can be missing to indicate "use default values".
// Default objective value: 0
// Default lower bound: 0
// Default upper bound: infinity
// Default type: continuous
absl::Status Xpress::AddVars(std::size_t count,
const absl::Span<const double> obj,
const absl::Span<const double> lb,
const absl::Span<const double> ub,
const absl::Span<const char> vtype) {
if (checkInt32Overflow(lb.size())) {
ASSIGN_OR_RETURN(int const oldCols, GetIntAttr(XPRS_ORIGINALCOLS));
if (checkInt32Overflow(count) ||
checkInt32Overflow(std::size_t(oldCols) + std::size_t(count))) {
return absl::InvalidArgumentError(
"XPRESS cannot handle more than 2^31 variables");
}
const int num_vars = static_cast<int>(lb.size());
if (vind.size() != vval.size() || ub.size() != num_vars ||
vtype.size() != num_vars || (!obj.empty() && obj.size() != num_vars) ||
(!vbegin.empty() && vbegin.size() != num_vars)) {
return absl::InvalidArgumentError(
"Xpress::AddVars arguments are of inconsistent sizes");
}
double* c_obj = nullptr;
const int num_vars = static_cast<int>(count);
double const* c_obj = nullptr;
if (!obj.empty()) {
c_obj = const_cast<double*>(obj.data());
if (obj.size() != count)
return absl::InvalidArgumentError(
"Xpress::AddVars objective argument has bad size");
c_obj = obj.data();
}
// TODO: look into int64_t support for number of vars (use XPRSaddcols64)
return ToStatus(XPRSaddcols(xpress_model_, num_vars, 0, c_obj, nullptr,
nullptr, nullptr, lb.data(), ub.data()));
if (!lb.empty() && lb.size() != count)
return absl::InvalidArgumentError(
"Xpress::AddVars lower bound argument has bad size");
if (!ub.empty() && ub.size() != count)
return absl::InvalidArgumentError(
"Xpress::AddVars upper bound argument has bad size");
std::vector<int> colind;
if (!vtype.empty()) {
if (vtype.size() != count)
return absl::InvalidArgumentError(
"Xpress::AddVars type argument has bad size");
colind.reserve(count); // So that we don't OOM after adding
}
// XPRSaddcols64() allows to add variables with more than INT_MAX
// non-zero coefficients here. It does NOT allow adding more than INT_MAX
// variables.
// Since we don't add any non-zeros here, it is safe to use XPRSaddcols().
RETURN_IF_ERROR(ToStatus(XPRSaddcols(
xpress_model_, num_vars, 0, c_obj, nullptr, nullptr, nullptr,
lb.size() ? lb.data() : nullptr, ub.size() ? ub.data() : nullptr)));
if (!vtype.empty()) {
for (int i = 0; i < num_vars; ++i) colind.push_back(oldCols + i);
int const ret =
XPRSchgcoltype(xpress_model_, num_vars, colind.data(), vtype.data());
if (ret != 0) {
// Changing the column type failed. We must roll back XPRSaddcols() and
// then return an error.
XPRSdelcols(xpress_model_, num_vars, colind.data());
}
return ToStatus(ret);
}
return absl::OkStatus();
}
absl::Status Xpress::AddConstrs(const absl::Span<const char> sense,
@@ -216,12 +264,19 @@ absl::Status Xpress::PostSolve() {
return ToStatus(XPRSpostsolve(xpress_model_));
}
absl::Status Xpress::MipOptimize() {
return ToStatus(XPRSmipoptimize(xpress_model_, nullptr));
absl::Status Xpress::Optimize(std::string const& flags, int* p_solvestatus,
int* p_solstatus) {
return ToStatus(
XPRSoptimize(xpress_model_, flags.c_str(), p_solvestatus, p_solstatus));
}
void Xpress::Terminate() { XPRSinterrupt(xpress_model_, XPRS_STOP_USER); };
absl::Status Xpress::GetControlInfo(char const* name, int* p_id,
int* p_type) const {
return ToStatus(XPRSgetcontrolinfo(xpress_model_, name, p_id, p_type));
}
absl::StatusOr<int> Xpress::GetIntControl(int control) const {
int result;
RETURN_IF_ERROR(ToStatus(XPRSgetintcontrol(xpress_model_, control, &result)))
@@ -243,6 +298,45 @@ absl::Status Xpress::ResetIntControl(int control) {
", consider adding it to Xpress::initIntControlDefaults");
}
absl::StatusOr<int64_t> Xpress::GetIntControl64(int control) const {
XPRSint64 result;
RETURN_IF_ERROR(
ToStatus(XPRSgetintcontrol64(xpress_model_, control, &result)))
<< "Error getting Xpress int64 control: " << control;
return result;
}
absl::Status Xpress::SetIntControl64(int control, int64_t value) {
return ToStatus(XPRSsetintcontrol64(xpress_model_, control, value));
}
absl::StatusOr<double> Xpress::GetDblControl(int control) const {
double result;
RETURN_IF_ERROR(ToStatus(XPRSgetdblcontrol(xpress_model_, control, &result)))
<< "Error getting Xpress double control: " << control;
return result;
}
absl::Status Xpress::SetDblControl(int control, double value) {
return ToStatus(XPRSsetdblcontrol(xpress_model_, control, value));
}
absl::StatusOr<std::string> Xpress::GetStrControl(int control) const {
int nbytes = 0;
RETURN_IF_ERROR(
ToStatus(XPRSgetstringcontrol(xpress_model_, control, NULL, 0, &nbytes)));
std::vector<char> result(nbytes,
'\0'); // nbytes CONTAINS the terminating nul!
RETURN_IF_ERROR(ToStatus(XPRSgetstringcontrol(
xpress_model_, control, result.data(), nbytes, &nbytes)))
<< "Error getting Xpress string control: " << control;
return std::string(result.data(), nbytes);
}
absl::Status Xpress::SetStrControl(int control, std::string const& value) {
return ToStatus(XPRSsetstrcontrol(xpress_model_, control, value.c_str()));
}
absl::StatusOr<int> Xpress::GetIntAttr(int attribute) const {
int result;
RETURN_IF_ERROR(ToStatus(XPRSgetintattrib(xpress_model_, attribute, &result)))
@@ -257,16 +351,13 @@ absl::StatusOr<double> Xpress::GetDoubleAttr(int attribute) const {
return result;
}
int Xpress::GetNumberOfConstraints() const {
int n;
XPRSgetintattrib(xpress_model_, XPRS_ROWS, &n);
return n;
}
int Xpress::GetNumberOfVariables() const {
int n;
XPRSgetintattrib(xpress_model_, XPRS_COLS, &n);
return n;
absl::StatusOr<double> Xpress::GetObjectiveDoubleAttr(int objidx,
int attribute) const {
double result = 0.0;
RETURN_IF_ERROR(
ToStatus(XPRSgetobjdblattrib(xpress_model_, objidx, attribute, &result)))
<< "Error getting Xpress objective double attribute: " << attribute;
return result;
}
absl::StatusOr<int> Xpress::GetDualStatus() const {
@@ -281,8 +372,10 @@ absl::StatusOr<int> Xpress::GetDualStatus() const {
absl::Status Xpress::GetBasis(std::vector<int>& rowBasis,
std::vector<int>& colBasis) const {
rowBasis.resize(GetNumberOfConstraints());
colBasis.resize(GetNumberOfVariables());
ASSIGN_OR_RETURN(int const rows, GetIntAttr(XPRS_ORIGINALROWS));
ASSIGN_OR_RETURN(int const cols, GetIntAttr(XPRS_ORIGINALCOLS));
rowBasis.resize(rows);
colBasis.resize(cols);
return ToStatus(
XPRSgetbasis(xpress_model_, rowBasis.data(), colBasis.data()));
}
@@ -298,22 +391,251 @@ absl::Status Xpress::SetStartingBasis(std::vector<int>& rowBasis,
}
absl::StatusOr<std::vector<double>> Xpress::GetVarLb() const {
int nVars = GetNumberOfVariables();
ASSIGN_OR_RETURN(int const cols, GetIntAttr(XPRS_ORIGINALCOLS));
std::vector<double> bounds;
bounds.reserve(nVars);
bounds.reserve(cols);
RETURN_IF_ERROR(
ToStatus(XPRSgetlb(xpress_model_, bounds.data(), 0, nVars - 1)))
ToStatus(XPRSgetlb(xpress_model_, bounds.data(), 0, cols - 1)))
<< "Failed to retrieve variable LB from XPRESS";
return bounds;
}
absl::StatusOr<std::vector<double>> Xpress::GetVarUb() const {
int nVars = GetNumberOfVariables();
ASSIGN_OR_RETURN(int const cols, GetIntAttr(XPRS_ORIGINALCOLS));
std::vector<double> bounds;
bounds.reserve(nVars);
bounds.reserve(cols);
RETURN_IF_ERROR(
ToStatus(XPRSgetub(xpress_model_, bounds.data(), 0, nVars - 1)))
ToStatus(XPRSgetub(xpress_model_, bounds.data(), 0, cols - 1)))
<< "Failed to retrieve variable UB from XPRESS";
return bounds;
}
absl::Status Xpress::Interrupt(int reason) {
return ToStatus(XPRSinterrupt(xpress_model_, reason));
}
absl::StatusOr<bool> Xpress::IsMIP() const {
ASSIGN_OR_RETURN(int ents, GetIntAttr(XPRS_ORIGINALMIPENTS));
if (ents != 0) return true;
ASSIGN_OR_RETURN(int sets, GetIntAttr(XPRS_ORIGINALSETS));
if (sets != 0) return true;
return false;
}
absl::Status Xpress::GetDuals(int* p_status,
std::optional<absl::Span<double>> const& duals,
int first, int last) {
return ToStatus(
XPRSgetduals(xpress_model_, p_status, forwardSpan(duals), first, last));
}
absl::Status Xpress::GetSolution(int* p_status,
std::optional<absl::Span<double>> const& x,
int first, int last) {
return ToStatus(
XPRSgetsolution(xpress_model_, p_status, forwardSpan(x), first, last));
}
absl::Status Xpress::GetRedCosts(int* p_status,
std::optional<absl::Span<double>> const& dj,
int first, int last) {
return ToStatus(
XPRSgetredcosts(xpress_model_, p_status, forwardSpan(dj), first, last));
}
/** Add a mip start that is specified in the original space, i.e., in terms of
* ortools variables.
*/
absl::Status Xpress::AddMIPSol(absl::Span<double const> vals,
absl::Span<int const> colind, char const* name) {
if (checkInt32Overflow(colind.size()))
return absl::InvalidArgumentError("more start values than columns");
if (colind.size() != vals.size())
return absl::InvalidArgumentError("inconsitent data to AddMIPSol()");
// XPRSaddmipsol() supports colind=nullptr, but we do not support that here
// since we don't need it.
return ToStatus(XPRSaddmipsol(xpress_model_, static_cast<int>(colind.size()),
vals.data(), colind.data(), name));
}
absl::Status Xpress::LoadDelayedRows(absl::Span<int const> rows) {
if (checkInt32Overflow(rows.size()))
return absl::InvalidArgumentError("more delayed rows than rows");
return ToStatus(XPRSloaddelayedrows(
xpress_model_, static_cast<int>(rows.size()), rows.data()));
}
absl::Status Xpress::LoadDirs(
absl::Span<int const> cols,
std::optional<absl::Span<int const>> const& prio,
std::optional<absl::Span<char const>> const& dir,
std::optional<absl::Span<double const>> const& up,
std::optional<absl::Span<double const>> const& down) {
if (checkInt32Overflow(cols.size()))
return absl::InvalidArgumentError("more directions than columns");
return ToStatus(XPRSloaddirs(xpress_model_, static_cast<int>(cols.size()),
cols.data(), forwardSpan(prio), forwardSpan(dir),
forwardSpan(up), forwardSpan(down)));
}
absl::Status Xpress::SetObjectiveIntControl(int obj, int control, int value) {
return ToStatus(XPRSsetobjintcontrol(xpress_model_, obj, control, value));
}
absl::Status Xpress::SetObjectiveDoubleControl(int obj, int control,
double value) {
return ToStatus(XPRSsetobjdblcontrol(xpress_model_, obj, control, value));
}
absl::StatusOr<int> Xpress::AddObjective(double constant, int ncols,
absl::Span<int const> colind,
absl::Span<double const> objcoef,
int priority, double weight) {
ASSIGN_OR_RETURN(int const objs, GetIntAttr(XPRS_OBJECTIVES));
if (objs == INT_MAX) {
return util::StatusBuilder(absl::StatusCode::kInvalidArgument)
<< "too many objectives";
}
int ret = XPRSaddobj(xpress_model_, ncols, colind.data(), objcoef.data(),
priority, weight);
if (ret) {
return ToStatus(ret);
}
if (constant != 0.0) {
ret =
XPRSsetobjdblcontrol(xpress_model_, objs, XPRS_OBJECTIVE_RHS, constant);
if (ret) {
XPRSdelobj(xpress_model_, objs);
return ToStatus(ret);
}
}
return objs;
}
absl::StatusOr<double> Xpress::CalculateObjectiveN(int objidx,
double const* solution) {
double objval = 0.0;
int ret = XPRScalcobjn(xpress_model_, objidx, solution, &objval);
if (ret) {
return ToStatus(ret);
}
return objval;
}
absl::Status Xpress::AddSets(absl::Span<char const> settype,
absl::Span<XPRSint64 const> start,
absl::Span<int const> colind,
absl::Span<double const> refval) {
ASSIGN_OR_RETURN(int const oldSets, GetIntAttr(XPRS_ORIGINALSETS));
if (checkInt32Overflow(settype.size()) ||
checkInt32Overflow(std::size_t(oldSets) + settype.size())) {
return absl::InvalidArgumentError(
"XPRESS cannot handle more than 2^31 SOSs");
}
return ToStatus(XPRSaddsets64(xpress_model_, static_cast<int>(settype.size()),
colind.size(), settype.data(), start.data(),
colind.data(), refval.data()));
}
absl::Status Xpress::SetIndicators(absl::Span<int const> rowind,
absl::Span<int const> colind,
absl::Span<int const> complement) {
ASSIGN_OR_RETURN(int const oldInds, GetIntAttr(XPRS_ORIGINALINDICATORS));
if (checkInt32Overflow(rowind.size()) ||
checkInt32Overflow(std::size_t(oldInds) + rowind.size())) {
return absl::InvalidArgumentError(
"XPRESS cannot handle more than 2^31 indicators");
}
if (rowind.size() != colind.size() || rowind.size() != complement.size()) {
return absl::InvalidArgumentError(
"inconsistent arguments to SetInidicators");
}
return ToStatus(
XPRSsetindicators(xpress_model_, static_cast<int>(rowind.size()),
rowind.data(), colind.data(), complement.data()));
}
absl::Status Xpress::AddRows(absl::Span<char const> rowtype,
absl::Span<double const> rhs,
absl::Span<double const> rng,
absl::Span<XPRSint64 const> start,
absl::Span<int const> colind,
absl::Span<double const> rowcoef) {
ASSIGN_OR_RETURN(int const oldRows, GetIntAttr(XPRS_ORIGINALROWS));
if (checkInt32Overflow(rowtype.size()) ||
checkInt32Overflow(std::size_t(oldRows) + rowtype.size())) {
return absl::InvalidArgumentError(
"XPRESS cannot handle more than 2^31 rows");
}
if (rowtype.size() != rhs.size() || rowtype.size() != rng.size() ||
rowtype.size() != start.size() || colind.size() != rowcoef.size())
return absl::InvalidArgumentError("inconsistent arguments to AddRows");
return ToStatus(XPRSaddrows64(xpress_model_, static_cast<int>(rowtype.size()),
colind.size(), rowtype.data(), rhs.data(),
rng.data(), start.data(), colind.data(),
rowcoef.data()));
}
absl::Status Xpress::AddQRow(char sense, double rhs, double rng,
absl::Span<int const> colind,
absl::Span<double const> rowcoef,
absl::Span<int const> qcol1,
absl::Span<int const> qcol2,
absl::Span<double const> qcoef) {
ASSIGN_OR_RETURN(int const oldRows, GetIntAttr(XPRS_ORIGINALROWS));
if (checkInt32Overflow(std::size_t(oldRows) + 1))
return absl::InvalidArgumentError(
"XPRESS cannot handle more than 2^31 rows");
XPRSint64 const start = 0;
RETURN_IF_ERROR(
ToStatus(XPRSaddrows64(xpress_model_, 1, colind.size(), &sense, &rhs,
&rng, &start, colind.data(), rowcoef.data())));
if (qcol1.size() > 0) {
int const ret = XPRSaddqmatrix64(xpress_model_, oldRows, qcol1.size(),
qcol1.data(), qcol2.data(), qcoef.data());
if (ret != 0) {
XPRSdelrows(xpress_model_, 1, &oldRows);
return ToStatus(ret);
}
}
return absl::OkStatus();
}
absl::Status Xpress::WriteProb(std::string const& filename,
std::string const& flags) {
return ToStatus(
XPRSwriteprob(xpress_model_, filename.c_str(), flags.c_str()));
}
absl::Status Xpress::SaveAs(std::string const& filename) {
return ToStatus(XPRSsaveas(xpress_model_, filename.c_str()));
}
absl::Status Xpress::GetLB(absl::Span<double> lb, int first, int last) {
return ToStatus(XPRSgetlb(xpress_model_, lb.data(), first, last));
}
absl::Status Xpress::GetUB(absl::Span<double> ub, int first, int last) {
return ToStatus(XPRSgetub(xpress_model_, ub.data(), first, last));
}
absl::Status Xpress::GetColType(absl::Span<char> ctype, int first, int last) {
return ToStatus(XPRSgetcoltype(xpress_model_, ctype.data(), first, last));
}
absl::Status Xpress::ChgBounds(absl::Span<int const> colind,
absl::Span<char const> bndtype,
absl::Span<double const> bndval) {
if (colind.size() != bndtype.size() || colind.size() != bndval.size())
return absl::InvalidArgumentError("inconsitent data to ChgBounds()");
if (checkInt32Overflow(colind.size()))
return absl::InvalidArgumentError(
"XPRESS cannot handle more than 2^31 bound changes");
return ToStatus(XPRSchgbounds(xpress_model_, colind.size(), colind.data(),
bndtype.data(), bndval.data()));
}
absl::Status Xpress::ChgColType(absl::Span<int const> colind,
absl::Span<char const> coltype) {
if (colind.size() != coltype.size())
return absl::InvalidArgumentError("inconsitent data to ChgColType()");
if (checkInt32Overflow(colind.size()))
return absl::InvalidArgumentError(
"XPRESS cannot handle more than 2^31 type changes");
return ToStatus(XPRSchgcoltype(xpress_model_, colind.size(), colind.data(),
coltype.data()));
}
} // namespace operations_research::math_opt

View File

@@ -27,6 +27,7 @@
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
@@ -49,23 +50,32 @@ class Xpress {
~Xpress();
absl::Status GetControlInfo(char const* name, int* p_id, int* p_type) const;
absl::StatusOr<int> GetIntControl(int control) const;
absl::Status SetIntControl(int control, int value);
absl::Status ResetIntControl(int control); // reset to default value
absl::StatusOr<int64_t> GetIntControl64(int control) const;
absl::Status SetIntControl64(int control, int64_t value);
absl::StatusOr<double> GetDblControl(int control) const;
absl::Status SetDblControl(int control, double value);
absl::StatusOr<std::string> GetStrControl(int control) const;
absl::Status SetStrControl(int control, std::string const& value);
absl::StatusOr<int> GetIntAttr(int attribute) const;
absl::StatusOr<double> GetDoubleAttr(int attribute) const;
absl::StatusOr<double> GetObjectiveDoubleAttr(int objidx,
int attribute) const;
absl::Status AddVars(absl::Span<const double> obj,
absl::Span<const double> lb, absl::Span<const double> ub,
absl::Span<const char> vtype);
absl::Status AddVars(absl::Span<const int> vbegin, absl::Span<const int> vind,
absl::Span<const double> vval,
absl::Span<const double> obj,
absl::Status AddVars(std::size_t count, absl::Span<const double> obj,
absl::Span<const double> lb, absl::Span<const double> ub,
absl::Span<const char> vtype);
absl::Status AddNames(int type, absl::Span<const char> names, int first,
int last);
absl::Status AddConstrs(absl::Span<const char> sense,
absl::Span<const double> rhs,
@@ -95,9 +105,21 @@ class Xpress {
// size (nVars, nCons, and nVars respectively)
absl::Status GetLpSol(absl::Span<double> primals, absl::Span<double> duals,
absl::Span<double> reducedCosts);
absl::Status MipOptimize();
absl::Status Optimize(std::string const& flags = "",
int* p_solvestatus = nullptr,
int* p_solstatus = nullptr);
absl::Status PostSolve();
absl::Status GetLB(absl::Span<double> lb, int first, int last);
absl::Status GetUB(absl::Span<double> ub, int first, int last);
absl::Status GetColType(absl::Span<char> ctype, int first, int last);
absl::Status ChgBounds(absl::Span<int const> colind,
absl::Span<char const> bndtype,
absl::Span<double const> bndval);
absl::Status ChgColType(absl::Span<int const> colind,
absl::Span<char const> coltype);
void Terminate();
absl::StatusOr<int> GetDualStatus() const;
@@ -106,16 +128,73 @@ class Xpress {
absl::Status SetStartingBasis(std::vector<int>& rowBasis,
std::vector<int>& colBasis) const;
static void XPRS_CC printXpressMessage(XPRSprob prob, void* data,
const char* sMsg, int nLen,
int nMsgLvl);
int GetNumberOfConstraints() const;
int GetNumberOfVariables() const;
absl::Status AddCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*, int,
int),
void* cbdata, int prio = 0);
absl::Status RemoveCbMessage(void(XPRS_CC* cb)(XPRSprob, void*, char const*,
int, int),
void* cbdata = nullptr);
absl::Status AddCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*), void* cbdata,
int prio = 0);
absl::Status RemoveCbChecktime(int(XPRS_CC* cb)(XPRSprob, void*),
void* cbdata = nullptr);
absl::StatusOr<std::vector<double>> GetVarLb() const;
absl::StatusOr<std::vector<double>> GetVarUb() const;
absl::Status Interrupt(int reason);
absl::StatusOr<bool> IsMIP() const;
absl::Status GetDuals(int* p_status,
std::optional<absl::Span<double>> const& duals,
int first, int last);
absl::Status GetSolution(int* p_status,
std::optional<absl::Span<double>> const& x,
int first, int last);
absl::Status GetRedCosts(int* p_status,
std::optional<absl::Span<double>> const& dj,
int first, int last);
absl::Status AddMIPSol(absl::Span<double const> vals,
absl::Span<int const> colind,
char const* name = nullptr);
absl::Status LoadDelayedRows(absl::Span<int const> rows);
absl::Status LoadDirs(absl::Span<int const> cols,
std::optional<absl::Span<int const>> const& prio,
std::optional<absl::Span<char const>> const& dir,
std::optional<absl::Span<double const>> const& up,
std::optional<absl::Span<double const>> const& down);
absl::Status SetObjectiveIntControl(int obj, int control, int value);
absl::Status SetObjectiveDoubleControl(int obj, int control, double value);
absl::StatusOr<int> AddObjective(double constant, int ncols,
absl::Span<int const> colind,
absl::Span<double const> objcoef,
int priority, double weight);
absl::StatusOr<double> CalculateObjectiveN(int objidx,
double const* solution);
absl::Status AddSets(absl::Span<char const> settype,
absl::Span<XPRSint64 const> start,
absl::Span<int const> colind,
absl::Span<double const> refval);
absl::Status SetIndicators(absl::Span<int const> rowind,
absl::Span<int const> colind,
absl::Span<int const> complement);
absl::Status AddRows(absl::Span<char const> rowtype,
absl::Span<double const> rhs,
absl::Span<double const> rng,
absl::Span<XPRSint64 const> start,
absl::Span<int const> colind,
absl::Span<double const> rowcoef);
absl::Status AddQRow(char sense, double rhs, double rng,
absl::Span<int const> colind,
absl::Span<double const> rowcoef,
absl::Span<int const> qcol1, absl::Span<int const> qcol2,
absl::Span<double const> qcoef);
absl::Status WriteProb(std::string const& filename,
std::string const& flags = "");
absl::Status SaveAs(std::string const& filename);
private:
XPRSprob xpress_model_;

File diff suppressed because it is too large Load Diff

View File

@@ -42,7 +42,6 @@
namespace operations_research::math_opt {
// Interface to FICO XPRESS solver
// Largely inspired by the Gurobi interface
class XpressSolver : public SolverInterface {
public:
// Creates the XPRESS solver and loads the model into it
@@ -68,8 +67,9 @@ class XpressSolver : public SolverInterface {
const SolveInterrupter* interrupter) override;
private:
explicit XpressSolver(std::unique_ptr<Xpress> g_xpress);
explicit XpressSolver(std::unique_ptr<Xpress> g_xpress, bool extract_names);
public:
// For easing reading the code, we declare these types:
using VarId = int64_t;
using AuxiliaryObjectiveId = int64_t;
@@ -88,6 +88,7 @@ class XpressSolver : public SolverInterface {
using XpressGeneralConstraintIndex = int;
using XpressAnyConstraintIndex = int;
private:
static constexpr XpressVariableIndex kUnspecifiedIndex = -1;
static constexpr XpressAnyConstraintIndex kUnspecifiedConstraint = -2;
static constexpr double kPlusInf = XPRS_PLUSINFINITY;
@@ -98,18 +99,21 @@ class XpressSolver : public SolverInterface {
}
// Data associated with each linear constraint
public:
struct LinearConstraintData {
XpressLinearConstraintIndex constraint_index = kUnspecifiedConstraint;
double lower_bound = kMinusInf;
double upper_bound = kPlusInf;
};
private:
absl::StatusOr<SolveResultProto> ExtractSolveResultProto(
absl::Time start, const ModelSolveParametersProto& model_parameters,
const SolveParametersProto& solve_parameters);
absl::StatusOr<SolutionProto> GetSolution(
const ModelSolveParametersProto& model_parameters,
const SolveParametersProto& solve_parameters);
absl::Status ExtendWithMultiobj(SolutionProto& solution);
absl::Status AppendSolution(SolveResultProto& solve_result,
const ModelSolveParametersProto& model_parameters,
const SolveParametersProto& solve_parameters);
absl::StatusOr<SolveStatsProto> GetSolveStats(absl::Time start) const;
absl::StatusOr<double> GetBestPrimalBound() const;
@@ -118,26 +122,45 @@ class XpressSolver : public SolverInterface {
absl::StatusOr<TerminationProto> ConvertTerminationReason(
double best_primal_bound, double best_dual_bound) const;
absl::StatusOr<SolutionProto> GetLpSolution(
const ModelSolveParametersProto& model_parameters,
const SolveParametersProto& solve_parameters);
bool isPrimalFeasible() const;
bool isDualFeasible() const;
void ExtractBounds(double lb, double ub, char& sense, double& rhs,
double& rng);
void ExtractLinear(SparseDoubleVectorProto const& expr,
std::vector<int>& colind, std::vector<double>& coef);
void ExtractQuadratic(QuadraticConstraintProto const& expr,
std::vector<int>& lin_colind,
std::vector<double>& lin_coef,
std::vector<int>& quad_col1,
std::vector<int>& quad_col2,
std::vector<double>& quad_coef);
absl::StatusOr<std::optional<BasisProto>> GetBasisIfAvailable(
const SolveParametersProto& parameters);
absl::Status AddNewLinearConstraints(
const LinearConstraintsProto& constraints);
absl::Status AddNewVariables(const VariablesProto& new_variables);
absl::Status AddSingleObjective(const ObjectiveProto& objective);
absl::Status AddObjective(const ObjectiveProto& objective,
std::optional<AuxiliaryObjectiveId> objective_id,
bool multiobj);
absl::Status AddSOS(
const google::protobuf::Map<AnyConstraintId, SosConstraintProto>& sets,
bool sos1);
absl::Status AddIndicators(
const google::protobuf::Map<IndicatorConstraintId,
IndicatorConstraintProto>& indicators);
absl::Status AddQuadraticConstraints(
const google::protobuf::Map<QuadraticConstraintId,
QuadraticConstraintProto>& constraints);
absl::Status AddSecondOrderConeConstraints(
const google::protobuf::Map<SecondOrderConeConstraintId,
SecondOrderConeConstraintProto>& constraints);
absl::Status ChangeCoefficients(const SparseDoubleMatrixProto& matrix);
absl::Status LoadModel(const ModelProto& input_model);
std::string GetLpOptimizationFlags(const SolveParametersProto& parameters);
absl::Status CallXpressSolve(const SolveParametersProto& parameters);
// Fills in result with the values in xpress_values aided by the index
// conversion from map which should be either variables_map_ or
// linear_constraints_map_ as appropriate. Only key/value pairs that passes
@@ -149,6 +172,7 @@ class XpressSolver : public SolverInterface {
const SparseVectorFilterProto& filter) const;
const std::unique_ptr<Xpress> xpress_;
bool const extract_names_;
// Internal correspondence from variable proto IDs to Xpress-numbered
// variables.
@@ -157,28 +181,59 @@ class XpressSolver : public SolverInterface {
// Xpress-numbered linear constraint and extra information.
gtl::linked_hash_map<LinearConstraintId, LinearConstraintData>
linear_constraints_map_;
// Internal correspondence from objective proto IDs to Xpress-numbered
// objectives.
gtl::linked_hash_map<AuxiliaryObjectiveId, XpressMultiObjectiveIndex>
objectives_map_;
// Internal correspondence from SOS1 proto IDs to Xpress-numbered
// SOS1 constraints.
gtl::linked_hash_map<Sos1ConstraintId, XpressSosConstraintIndex> sos1_map_;
// Internal correspondence from SOS2 proto IDs to Xpress-numbered
// SOS2 constraints.
gtl::linked_hash_map<Sos2ConstraintId, XpressSosConstraintIndex> sos2_map_;
// Internal correspondence from indicator proto IDs to Xpress-numbered
// indicators.
gtl::linked_hash_map<IndicatorConstraintId, LinearConstraintData>
indicator_map_;
// Internal correspondence from quadratic proto IDs to Xpress-numbered
// rows.
gtl::linked_hash_map<QuadraticConstraintId, LinearConstraintData>
quad_constraints_map_;
// Internal correspondence from second order cone constraint proto IDs to
// Xpress-numbered rows.
gtl::linked_hash_map<QuadraticConstraintId, LinearConstraintData> soc_map_;
int get_model_index(XpressVariableIndex index) const { return index; }
int get_model_index(const LinearConstraintData& index) const {
return index.constraint_index;
}
SolutionStatusProto getLpSolutionStatus() const;
SolutionStatusProto getPrimalSolutionStatus() const;
SolutionStatusProto getDualSolutionStatus() const;
absl::StatusOr<InvertedBounds> ListInvertedBounds() const;
absl::Status SetXpressStartingBasis(const BasisProto& basis);
absl::Status SetLpIterLimits(const SolveParametersProto& parameters);
/** Whether to force an XPRSpostsolve() after solving. */
bool force_postsolve_ = false;
/** Stop immediately after the initial LP in MIPs. */
bool stop_after_lp_ = false;
/** Whether the model has a non-binary indicator variable.
* The behavior expected by ortools is that
* - we can happily create a model with non-binary indicators
* - this must fail at _solve_ time
* Xpress implicitly converts indicator variables to binaries, though,
* so we must keep track of this fact at build time and raise an error
* only at solve time.
*/
bool nonbinary_indicator_ = false;
bool is_multiobj_ = false;
bool is_mip_ = false;
bool is_maximize_ = false;
struct LpStatus {
int primal_status = 0;
int dual_status = 0;
};
LpStatus xpress_lp_status_;
LPAlgorithmProto lp_algorithm_ = LP_ALGORITHM_UNSPECIFIED;
int xpress_mip_status_ = 0;
// Results of the last solve
int primal_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND;
int dual_sol_avail_ = XPRS_SOLAVAILABLE_NOTFOUND;
// Information queried right after a solve and stored for solution reporting
int solvestatus_ = XPRS_SOLVESTATUS_UNSTARTED;
int solstatus_ = XPRS_SOLSTATUS_NOTFOUND;
int algorithm_ = XPRS_ALG_DEFAULT;
int optimizetypeused_ = -1;
};
} // namespace operations_research::math_opt

View File

@@ -25,11 +25,13 @@
#include "ortools/math_opt/solver_tests/generic_tests.h"
#include "ortools/math_opt/solver_tests/infeasible_subsystem_tests.h"
#include "ortools/math_opt/solver_tests/invalid_input_tests.h"
#include "ortools/math_opt/solver_tests/ip_model_solve_parameters_tests.h"
#include "ortools/math_opt/solver_tests/logical_constraint_tests.h"
#include "ortools/math_opt/solver_tests/lp_incomplete_solve_tests.h"
#include "ortools/math_opt/solver_tests/lp_model_solve_parameters_tests.h"
#include "ortools/math_opt/solver_tests/lp_parameter_tests.h"
#include "ortools/math_opt/solver_tests/lp_tests.h"
#include "ortools/math_opt/solver_tests/mip_tests.h"
#include "ortools/math_opt/solver_tests/multi_objective_tests.h"
#include "ortools/math_opt/solver_tests/qc_tests.h"
#include "ortools/math_opt/solver_tests/qp_tests.h"
@@ -37,6 +39,15 @@
#include "ortools/math_opt/solver_tests/status_tests.h"
#include "ortools/third_party_solvers/xpress_environment.h"
/** A string in the log file that indicates that the solution process
* finished successfully and found the optimal solution for LPs.
*/
#define OPTIMAL_SOLUTION_FOUND_LP "Optimal solution found"
/** A string in the log file that indicates that the solution process
* finished successfully and found the optimal solution for MIPs.
*/
#define OPTIMAL_SOLUTION_FOUND_MIP "*** Search completed ***"
namespace operations_research {
namespace math_opt {
namespace {
@@ -59,16 +70,18 @@ INSTANTIATE_TEST_SUITE_P(XpressLpModelSolveParametersTest,
INSTANTIATE_TEST_SUITE_P(
XpressLpParameterTest, LpParameterTest,
testing::Values(LpParameterTestParams(SolverType::kXpress,
/*supports_simplex=*/true,
/*supports_barrier=*/true,
/*supports_first_order=*/false,
/*supports_random_seed=*/false,
/*supports_presolve=*/false,
/*supports_cutoff=*/false,
/*supports_objective_limit=*/false,
/*supports_best_bound_limit=*/false,
/*reports_limits=*/false)));
testing::Values(LpParameterTestParams(
SolverType::kXpress,
/*supports_simplex=*/true,
/*supports_barrier=*/true,
/*supports_first_order=*/true,
/*supports_random_seed=*/false, // Xpress supports this but it does not
// generate enough variability for this
/*supports_presolve=*/true,
/*supports_cutoff=*/true,
/*supports_objective_limit=*/false, // See comments in xpress_solver.cc
/*supports_best_bound_limit=*/false,
/*reports_limits=*/false)));
INSTANTIATE_TEST_SUITE_P(
XpressPrimalSimplexLpIncompleteSolveTest, LpIncompleteSolveTest,
@@ -77,7 +90,7 @@ INSTANTIATE_TEST_SUITE_P(
/*lp_algorithm=*/LPAlgorithm::kPrimalSimplex,
/*supports_iteration_limit=*/true, /*supports_initial_basis=*/false,
/*supports_incremental_solve=*/false, /*supports_basis=*/true,
/*supports_presolve=*/false, /*check_primal_objective=*/true,
/*supports_presolve=*/true, /*check_primal_objective=*/true,
/*primal_solution_status_always_set=*/true,
/*dual_solution_status_always_set=*/true)));
INSTANTIATE_TEST_SUITE_P(
@@ -87,49 +100,93 @@ INSTANTIATE_TEST_SUITE_P(
/*lp_algorithm=*/LPAlgorithm::kDualSimplex,
/*supports_iteration_limit=*/true, /*supports_initial_basis=*/false,
/*supports_incremental_solve=*/false, /*supports_basis=*/true,
/*supports_presolve=*/false, /*check_primal_objective=*/true,
/*supports_presolve=*/true, /*check_primal_objective=*/true,
/*primal_solution_status_always_set=*/true,
/*dual_solution_status_always_set=*/true)));
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(IncrementalLpTest);
INSTANTIATE_TEST_SUITE_P(XpressMessageCallbackTest, MessageCallbackTest,
testing::Values(MessageCallbackTestParams(
SolverType::kXpress,
/*support_message_callback=*/false,
/*support_interrupter=*/false,
/*integer_variables=*/false, "")));
INSTANTIATE_TEST_SUITE_P(
XpressMessageCallbackTest, MessageCallbackTest,
testing::ValuesIn({MessageCallbackTestParams(
SolverType::kXpress,
/*support_message_callback=*/true,
/*support_interrupter=*/true,
/*integer_variables=*/false,
/*ending_substring*/ OPTIMAL_SOLUTION_FOUND_LP),
MessageCallbackTestParams(
SolverType::kXpress,
/*support_message_callback=*/true,
/*support_interrupter=*/true,
/*integer_variables=*/true,
/*ending_substring*/ OPTIMAL_SOLUTION_FOUND_MIP)}));
INSTANTIATE_TEST_SUITE_P(
XpressCallbackTest, CallbackTest,
testing::Values(CallbackTestParams(SolverType::kXpress,
/*integer_variables=*/false,
/*add_lazy_constraints=*/false,
/*add_cuts=*/false,
/*supported_events=*/{},
/*all_solutions=*/std::nullopt,
/*reaches_cut_callback*/ std::nullopt)));
testing::ValuesIn(
{CallbackTestParams(SolverType::kXpress,
/*integer_variables=*/false,
/*add_lazy_constraints=*/false,
/*add_cuts=*/false,
/*supported_events=*/{},
/*all_solutions=*/std::nullopt,
/*reaches_cut_callback*/ std::nullopt),
CallbackTestParams(SolverType::kXpress,
/*integer_variables=*/true,
/*add_lazy_constraints=*/false,
/*add_cuts=*/false,
/*supported_events=*/{},
/*all_solutions=*/std::nullopt,
/*reaches_cut_callback*/ std::nullopt)}));
INSTANTIATE_TEST_SUITE_P(XpressInvalidInputTest, InvalidInputTest,
testing::Values(InvalidInputTestParameters(
SolverType::kXpress,
/*use_integer_variables=*/false)));
INSTANTIATE_TEST_SUITE_P(
XpressInvalidInputTest, InvalidInputTest,
testing::ValuesIn(
{InvalidInputTestParameters(SolverType::kXpress,
/*use_integer_variables=*/true),
InvalidInputTestParameters(SolverType::kXpress,
/*use_integer_variables=*/false)}));
InvalidParameterTestParams InvalidThreadsParameters() {
InvalidParameterTestParams InvalidObjectiveLimitParameters() {
SolveParameters params;
params.threads = 2;
return InvalidParameterTestParams(SolverType::kXpress, std::move(params),
{"only supports parameters.threads = 1"});
params.objective_limit = 1.5;
return InvalidParameterTestParams(
SolverType::kXpress, std::move(params),
{"XpressSolver does not support objective_limit"});
}
InvalidParameterTestParams InvalidBestBoundLimitParameters() {
SolveParameters params;
params.best_bound_limit = 1.5;
return InvalidParameterTestParams(
SolverType::kXpress, std::move(params),
{"XpressSolver does not support best_bound_limit"});
}
InvalidParameterTestParams InvalidSolutionPoolSizeParameters() {
SolveParameters params;
params.solution_pool_size = 2;
return InvalidParameterTestParams(
SolverType::kXpress, std::move(params),
{"XpressSolver does not support solution_pool_size"});
}
INSTANTIATE_TEST_SUITE_P(XpressInvalidParameterTest, InvalidParameterTest,
ValuesIn({InvalidThreadsParameters()}));
ValuesIn({InvalidObjectiveLimitParameters(),
InvalidBestBoundLimitParameters(),
InvalidSolutionPoolSizeParameters()}));
INSTANTIATE_TEST_SUITE_P(XpressGenericTest, GenericTest,
testing::Values(GenericTestParameters(
SolverType::kXpress, /*support_interrupter=*/false,
/*integer_variables=*/false,
/*expected_log=*/"Optimal solution found")));
INSTANTIATE_TEST_SUITE_P(
XpressGenericTest, GenericTest,
testing::ValuesIn(
{GenericTestParameters(SolverType::kXpress,
/*support_interrupter=*/true,
/*integer_variables=*/false,
/*expected_log=*/OPTIMAL_SOLUTION_FOUND_LP),
GenericTestParameters(SolverType::kXpress,
/*support_interrupter=*/true,
/*integer_variables=*/true,
/*expected_log=*/OPTIMAL_SOLUTION_FOUND_MIP)}));
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TimeLimitTest);
@@ -142,23 +199,93 @@ INSTANTIATE_TEST_SUITE_P(XpressInfeasibleSubsystemTest, InfeasibleSubsystemTest,
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(IpModelSolveParametersTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(IpParameterTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(LargeInstanceIpParameterTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(SimpleMipTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(IncrementalMipTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipSolutionHintTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(BranchPrioritiesTest);
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(LazyConstraintsTest);
INSTANTIATE_TEST_SUITE_P(XpressSimpleMipTest, SimpleMipTest,
testing::Values(SolverType::kXpress));
SolveParameters GetXpressSingleHintParams() {
// Parameters to stop on the first solution that is created from extending
// the solution hints.
SolveParameters params;
params.solution_limit = 1;
params.xpress.param_values["PRESOLVE"] = "0";
params.xpress.param_values["HEUREMPHASIS"] = "0";
return params;
}
SolveParameters GetXpressTwoHintParams() {
// Parameters to stop on the second solution that is created from extending
// the solution hints.
SolveParameters params;
params.solution_limit = 2;
params.xpress.param_values["PRESOLVE"] = "0";
params.xpress.param_values["HEUREMPHASIS"] = "0";
return params;
}
INSTANTIATE_TEST_SUITE_P(XpressMipSolutionHintTest, MipSolutionHintTest,
testing::Values(SolutionHintTestParams(
SolverType::kXpress, GetXpressSingleHintParams(),
GetXpressTwoHintParams(),
"User solution (.*) stored")));
SolveParameters GetXpressLazyConstraintsParams() {
SolveParameters params;
// Disable heuristics since they may interfere with expected results.
params.xpress.param_values["HEUREMPHASIS"] = "0";
params.xpress.param_values["PRESOLVE"] = "0";
params.xpress.param_values["CUTSTRATEGY"] = "0";
// Without STOP_AFTER_LP Xpress will not stop right after the relaxation
// but will start the cut loop and inject the lazy constraints, which is
// unexpected in .
// On the other hand, these parameters are also used for test
// LazyConstraintsTest.AnnotationsAreClearedAfterSolve/0 which then fails
// because that test expects to finish of the root node. Therefore that
// test is disabled.
params.xpress.param_values["STOP_AFTER_LP"] = "1";
return params;
}
INSTANTIATE_TEST_SUITE_P(XpressLazyConstraintsTest, LazyConstraintsTest,
testing::Values(LazyConstraintsTestParams(
SolverType::kXpress,
GetXpressLazyConstraintsParams())));
SolveParameters GetXpressBranchPrioritiesParams() {
SolveParameters params;
// Disable anything that is different from plain branch & bound
params.xpress.param_values["HEUREMPHASIS"] = "0";
params.xpress.param_values["PRESOLVE"] = "0";
params.xpress.param_values["CUTSTRATEGY"] = "0";
params.xpress.param_values["NODEPROBINGEFFORT"] = "0.0";
// For BranchPrioritiesTest.PrioritiesClearedAfterIncrementalSolve,
// otherwise we attempt to set branching priorities on a problem in
// presolved state, which is not allowed.
params.xpress.param_values["FORCE_POSTSOLVE"] = "1";
return params;
}
INSTANTIATE_TEST_SUITE_P(XpressBranchPrioritiesTest, BranchPrioritiesTest,
testing::Values(BranchPrioritiesTestParams(
SolverType::kXpress,
GetXpressBranchPrioritiesParams())));
LogicalConstraintTestParameters GetXpressLogicalConstraintTestParameters() {
return LogicalConstraintTestParameters(
SolverType::kXpress, SolveParameters(),
/*supports_integer_variables=*/false,
/*supports_integer_variables=*/true,
// Note: Xpress supports SOS, but it only supports SOSs that comprise
// solely of variables (not expressions) and it does not support
// duplicate entries. Many of the SOS tests construct things
// like this, so we skip them.
/*supports_sos1=*/false,
/*supports_sos2=*/false,
/*supports_indicator_constraints=*/false,
/*supports_indicator_constraints=*/true,
/*supports_incremental_add_and_deletes=*/false,
/*supports_incremental_variable_deletions=*/false,
/*supports_deleting_indicator_variables=*/false,
/*supports_updating_binary_variables=*/false);
/*supports_updating_binary_variables=*/false,
/*supports_sos_on_expressions=*/false);
}
INSTANTIATE_TEST_SUITE_P(
@@ -171,10 +298,10 @@ INSTANTIATE_TEST_SUITE_P(
MultiObjectiveTestParameters GetXpressMultiObjectiveTestParameters() {
return MultiObjectiveTestParameters(
/*solver_type=*/SolverType::kXpress, /*parameters=*/SolveParameters(),
/*supports_auxiliary_objectives=*/false,
/*supports_auxiliary_objectives=*/true,
/*supports_incremental_objective_add_and_delete=*/false,
/*supports_incremental_objective_modification=*/false,
/*supports_integer_variables=*/false);
/*supports_integer_variables=*/true);
}
INSTANTIATE_TEST_SUITE_P(
@@ -185,37 +312,48 @@ INSTANTIATE_TEST_SUITE_P(
XpressIncrementalMultiObjectiveTest, IncrementalMultiObjectiveTest,
testing::Values(GetXpressMultiObjectiveTestParameters()));
QpTestParameters GetXpressQpTestParameters() {
return QpTestParameters(SolverType::kXpress, SolveParameters(),
/*qp_support=*/QpSupportType::kConvexQp,
/*supports_incrementalism_not_modifying_qp=*/false,
/*supports_qp_incrementalism=*/false,
/*use_integer_variables=*/false);
std::vector<QpTestParameters> GetXpressQpTestParameters() {
// TODO: Xpress also supports non-convex QP.
return {QpTestParameters(SolverType::kXpress, SolveParameters(),
/*qp_support=*/QpSupportType::kConvexQp,
/*supports_incrementalism_not_modifying_qp=*/false,
/*supports_qp_incrementalism=*/false,
/*use_integer_variables=*/true),
QpTestParameters(SolverType::kXpress, SolveParameters(),
/*qp_support=*/QpSupportType::kConvexQp,
/*supports_incrementalism_not_modifying_qp=*/false,
/*supports_qp_incrementalism=*/false,
/*use_integer_variables=*/false)};
}
INSTANTIATE_TEST_SUITE_P(XpressSimpleQpTest, SimpleQpTest,
testing::Values(GetXpressQpTestParameters()));
testing::ValuesIn(GetXpressQpTestParameters()));
INSTANTIATE_TEST_SUITE_P(XpressIncrementalQpTest, IncrementalQpTest,
testing::Values(GetXpressQpTestParameters()));
testing::ValuesIn(GetXpressQpTestParameters()));
INSTANTIATE_TEST_SUITE_P(XpressQpDualsTest, QpDualsTest,
testing::Values(GetXpressQpTestParameters()));
testing::ValuesIn(GetXpressQpTestParameters()));
QcTestParameters GetXpressQcTestParameters() {
return QcTestParameters(SolverType::kXpress, SolveParameters(),
/*supports_qc=*/false,
/*supports_incremental_add_and_deletes=*/false,
/*supports_incremental_variable_deletions=*/false,
/*use_integer_variables=*/false);
std::vector<QcTestParameters> GetXpressQcTestParameters() {
return {QcTestParameters(SolverType::kXpress, SolveParameters(),
/*supports_qc=*/true,
/*supports_incremental_add_and_deletes=*/false,
/*supports_incremental_variable_deletions=*/false,
/*use_integer_variables=*/true),
QcTestParameters(SolverType::kXpress, SolveParameters(),
/*supports_qc=*/true,
/*supports_incremental_add_and_deletes=*/false,
/*supports_incremental_variable_deletions=*/false,
/*use_integer_variables=*/false)};
}
INSTANTIATE_TEST_SUITE_P(XpressSimpleQcTest, SimpleQcTest,
testing::Values(GetXpressQcTestParameters()));
testing::ValuesIn(GetXpressQcTestParameters()));
INSTANTIATE_TEST_SUITE_P(XpressIncrementalQcTest, IncrementalQcTest,
testing::Values(GetXpressQcTestParameters()));
testing::ValuesIn(GetXpressQcTestParameters()));
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(QcDualsTest);
SecondOrderConeTestParameters GetXpressSecondOrderConeTestParameters() {
return SecondOrderConeTestParameters(
SolverType::kXpress, SolveParameters(),
/*supports_soc_constraints=*/false,
/*supports_soc_constraints=*/true,
/*supports_incremental_add_and_deletes=*/false);
}
INSTANTIATE_TEST_SUITE_P(
@@ -234,11 +372,20 @@ std::vector<StatusTestParameters> MakeStatusTestConfigs() {
test_parameters.push_back(StatusTestParameters(
SolverType::kXpress, solve_parameters,
/*disallow_primal_or_dual_infeasible=*/false,
/*supports_iteration_limit=*/false,
/*supports_iteration_limit=*/true,
/*use_integer_variables=*/false,
/*supports_node_limit=*/false,
/*support_interrupter=*/false, /*supports_one_thread=*/true));
/*supports_node_limit=*/true,
/*support_interrupter=*/true, /*supports_one_thread=*/true));
}
// Add a test with default LP algorithm and integer variables
SolveParameters solve_parameters = {.lp_algorithm = std::nullopt};
test_parameters.push_back(StatusTestParameters(
SolverType::kXpress, solve_parameters,
/*disallow_primal_or_dual_infeasible=*/false,
/*supports_iteration_limit=*/true,
/*use_integer_variables=*/true,
/*supports_node_limit=*/true,
/*support_interrupter=*/true, /*supports_one_thread=*/true));
return test_parameters;
}