diff --git a/ortools/math_opt/BUILD.bazel b/ortools/math_opt/BUILD.bazel index 1f8df9fcc8e..c78f032d4fc 100644 --- a/ortools/math_opt/BUILD.bazel +++ b/ortools/math_opt/BUILD.bazel @@ -1,4 +1,4 @@ -# Copyright 2010-2025 Google LLC +# Copyright 2010-2026 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 @@ -127,6 +127,7 @@ proto_library( srcs = ["parameters.proto"], deps = [ "//ortools/glop:parameters_proto", + "//ortools/math_opt/solvers:cplex_proto", "//ortools/math_opt/solvers:glpk_proto", "//ortools/math_opt/solvers:gurobi_proto", "//ortools/math_opt/solvers:highs_proto", diff --git a/ortools/math_opt/cpp/parameters.cc b/ortools/math_opt/cpp/parameters.cc index 8b441d7c2d1..bad1c8ccd31 100644 --- a/ortools/math_opt/cpp/parameters.cc +++ b/ortools/math_opt/cpp/parameters.cc @@ -1,4 +1,4 @@ -// Copyright 2010-2025 Google LLC +// Copyright 2010-2026 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 @@ -88,6 +88,8 @@ std::optional Enum::ToOptString( return "santorini"; case SolverType::kXpress: return "xpress"; + case SolverType::kCplex: + return "cplex"; } return std::nullopt; } @@ -97,7 +99,7 @@ absl::Span Enum::AllValues() { SolverType::kGscip, SolverType::kGurobi, SolverType::kGlop, SolverType::kCpSat, SolverType::kPdlp, SolverType::kGlpk, SolverType::kEcos, SolverType::kScs, SolverType::kHighs, - SolverType::kSantorini, SolverType::kXpress, + SolverType::kSantorini, SolverType::kXpress, SolverType::kCplex, }; return absl::MakeConstSpan(kSolverTypeValues); } @@ -234,6 +236,59 @@ XpressParameters XpressParameters::FromProto( return result; } +CplexParametersProto CplexParameters::Proto() const { + CplexParametersProto result; + for (const auto& [key, val] : param_double_values) { + auto* p = result.add_parameters()->mutable_parameter_double(); + p->set_name(key); + p->set_value(val); + } + for (const auto& [key, val] : param_bool_values) { + auto* p = result.add_parameters()->mutable_parameter_bool(); + p->set_name(key); + p->set_value(val); + } + for (const auto& [key, val] : param_int32_values) { + auto* p = result.add_parameters()->mutable_parameter_int32(); + p->set_name(key); + p->set_value(val); + } + for (const auto& [key, val] : param_int64_values) { + auto* p = result.add_parameters()->mutable_parameter_int64(); + p->set_name(key); + p->set_value(val); + } + for (const auto& [key, val] : param_string_values) { + auto* p = result.add_parameters()->mutable_parameter_string(); + p->set_name(key); + p->set_value(val); + } + return result; +} + +CplexParameters CplexParameters::FromProto(const CplexParametersProto& proto) { + CplexParameters result; + for (const CplexParametersProto::Parameter& p : proto.parameters()) { + if (p.has_parameter_double()) { + result.param_double_values[p.parameter_double().name()] = + p.parameter_double().value(); + } else if (p.has_parameter_bool()) { + result.param_bool_values[p.parameter_bool().name()] = + p.parameter_bool().value(); + } else if (p.has_parameter_int32()) { + result.param_int32_values[p.parameter_int32().name()] = + p.parameter_int32().value(); + } else if (p.has_parameter_int64()) { + result.param_int64_values[p.parameter_int64().name()] = + p.parameter_int64().value(); + } else if (p.has_parameter_string()) { + result.param_string_values[p.parameter_string().name()] = + p.parameter_string().value(); + } + } + return result; +} + SolveParametersProto SolveParameters::Proto() const { SolveParametersProto result; result.set_enable_output(enable_output); @@ -287,6 +342,7 @@ SolveParametersProto SolveParameters::Proto() const { *result.mutable_glpk() = glpk.Proto(); *result.mutable_highs() = highs; *result.mutable_xpress() = xpress.Proto(); + *result.mutable_cplex() = cplex.Proto(); return result; } @@ -347,6 +403,7 @@ absl::StatusOr SolveParameters::FromProto( result.glpk = GlpkParameters::FromProto(proto.glpk()); result.highs = proto.highs(); result.xpress = XpressParameters::FromProto(proto.xpress()); + result.cplex = CplexParameters::FromProto(proto.cplex()); return result; } diff --git a/ortools/math_opt/cpp/parameters.h b/ortools/math_opt/cpp/parameters.h index 90bd094409b..80018a4d633 100644 --- a/ortools/math_opt/cpp/parameters.h +++ b/ortools/math_opt/cpp/parameters.h @@ -1,4 +1,4 @@ -// Copyright 2010-2025 Google LLC +// Copyright 2010-2026 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 @@ -28,6 +28,7 @@ #include "ortools/glop/parameters.pb.h" // IWYU pragma: export #include "ortools/math_opt/cpp/enums.h" // IWYU pragma: export #include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/solvers/cplex.pb.h" // IWYU pragma: export #include "ortools/math_opt/solvers/gscip/gscip.pb.h" // IWYU pragma: export #include "ortools/math_opt/solvers/gurobi.pb.h" // IWYU pragma: export #include "ortools/math_opt/solvers/highs.pb.h" // IWYU pragma: export @@ -114,6 +115,12 @@ enum class SolverType { // Supports LP, MIP, and nonconvex integer quadratic problems. // A fast option, but has special licensing. kXpress = SOLVER_TYPE_XPRESS, + + // IBM ILOG CPLEX solver (third party). + // + // Supports LP, MIP problems (other types are unimplemented). + // A fast option, but has special licensing. + kCplex = SOLVER_TYPE_CPLEX, }; MATH_OPT_DEFINE_ENUM(SolverType, SOLVER_TYPE_UNSPECIFIED); @@ -292,6 +299,23 @@ struct XpressParameters { bool empty() const { return param_values.empty(); } }; +struct CplexParameters { + absl::linked_hash_map param_double_values; + absl::linked_hash_map param_bool_values; + absl::linked_hash_map param_int32_values; + absl::linked_hash_map param_int64_values; + absl::linked_hash_map param_string_values; + + CplexParametersProto Proto() const; + static CplexParameters FromProto(const CplexParametersProto& proto); + + bool empty() const { + return param_double_values.empty() && param_bool_values.empty() && + param_int32_values.empty() && param_int64_values.empty() && + param_string_values.empty(); + } +}; + // Parameters to control a single solve. // // Contains both parameters common to all solvers, e.g. time_limit, and @@ -466,6 +490,7 @@ struct SolveParameters { GlpkParameters glpk; HighsOptionsProto highs; XpressParameters xpress; + CplexParameters cplex; SolveParametersProto Proto() const; static absl::StatusOr FromProto( diff --git a/ortools/math_opt/parameters.proto b/ortools/math_opt/parameters.proto index 551be1b310c..ce4ce27872c 100644 --- a/ortools/math_opt/parameters.proto +++ b/ortools/math_opt/parameters.proto @@ -1,4 +1,4 @@ -// Copyright 2010-2025 Google LLC +// Copyright 2010-2026 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 @@ -19,6 +19,7 @@ package operations_research.math_opt; import "google/protobuf/duration.proto"; import "ortools/pdlp/solvers.proto"; import "ortools/glop/parameters.proto"; +import "ortools/math_opt/solvers/cplex.proto"; import "ortools/math_opt/solvers/glpk.proto"; import "ortools/math_opt/solvers/gscip/gscip.proto"; import "ortools/math_opt/solvers/gurobi.proto"; @@ -115,6 +116,12 @@ enum SolverTypeProto { // Supports LP, MIP, and nonconvex integer quadratic problems. // A fast option, but has special licensing. SOLVER_TYPE_XPRESS = 13; + + // IBM ILOG CPLEX solver (third party). + // + // Supports LP, MIP. (other problem-types are unimplemented) + // A fast option, but has special licensing. + SOLVER_TYPE_CPLEX = 14; } // Selects an algorithm for solving linear programs. @@ -179,6 +186,7 @@ message SolverInitializerProto { GurobiInitializerProto gurobi = 1; reserved 2; XpressInitializerProto xpress = 3; + CplexInitializerProto cplex = 4; } // Parameters to control a single solve. @@ -381,5 +389,7 @@ message SolveParametersProto { XpressParametersProto xpress = 28; + CplexParametersProto cplex = 29; + reserved 11; // Deleted } diff --git a/ortools/math_opt/python/init_arguments.py b/ortools/math_opt/python/init_arguments.py index 789d4459868..e4e142fcff3 100644 --- a/ortools/math_opt/python/init_arguments.py +++ b/ortools/math_opt/python/init_arguments.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC +# Copyright 2010-2026 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 @@ -129,6 +129,11 @@ class StreamableHighsInitArguments: """Streamable Highs specific parameters for solver instantiation.""" +@dataclasses.dataclass +class StreamableCplexInitArguments: + """Streamable Cplex specific parameters for solver instantiation.""" + + @dataclasses.dataclass class StreamableSantoriniInitArguments: """Streamable Santorini specific parameters for solver instantiation.""" @@ -150,6 +155,7 @@ class StreamableSolverInitArguments: scs: Initialization parameters specific to SCS. highs: Initialization parameters specific to HiGHS. santorini: Initialization parameters specific to Santorini. + cplex: Initialization parameters specific to Cplex. """ gscip: Optional[StreamableGScipInitArguments] = None @@ -163,6 +169,7 @@ class StreamableSolverInitArguments: scs: Optional[StreamableScsInitArguments] = None highs: Optional[StreamableHighsInitArguments] = None santorini: Optional[StreamableSantoriniInitArguments] = None + cplex: Optional[StreamableCplexInitArguments] = None def to_proto(self) -> parameters_pb2.SolverInitializerProto: """Returns a protocol buffer equivalent of this.""" diff --git a/ortools/math_opt/python/mathopt.py b/ortools/math_opt/python/mathopt.py index b649432bdcf..a67c30aee11 100644 --- a/ortools/math_opt/python/mathopt.py +++ b/ortools/math_opt/python/mathopt.py @@ -71,7 +71,7 @@ parse_objective_parameters, parse_solution_hint) from ortools.math_opt.python.objectives import AuxiliaryObjective, Objective from ortools.math_opt.python.parameters import (Emphasis, GlpkParameters, - GurobiParameters, LPAlgorithm, + GurobiParameters, CplexParameters, LPAlgorithm, SolveParameters, SolverType, emphasis_from_proto, emphasis_to_proto, @@ -79,6 +79,7 @@ lp_algorithm_to_proto, parse_glpk_parameters, parse_gurobi_parameters, + parse_cplex_parameters, parse_solve_parameters, solver_type_from_proto, solver_type_to_proto) diff --git a/ortools/math_opt/python/parameters.py b/ortools/math_opt/python/parameters.py index 061ccd2c406..cb30afcc78d 100644 --- a/ortools/math_opt/python/parameters.py +++ b/ortools/math_opt/python/parameters.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC +# Copyright 2010-2026 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 @@ -21,8 +21,8 @@ from ortools.glop import parameters_pb2 as glop_parameters_pb2 from ortools.math_opt import parameters_pb2 as math_opt_parameters_pb2 -from ortools.math_opt.solvers import (glpk_pb2, gurobi_pb2, highs_pb2, - osqp_pb2, xpress_pb2) +from ortools.math_opt.solvers import (cplex_pb2, glpk_pb2, gurobi_pb2, + highs_pb2, osqp_pb2, xpress_pb2) from ortools.math_opt.solvers.gscip import gscip_pb2 from ortools.pdlp import solvers_pb2 as pdlp_solvers_pb2 from ortools.sat import sat_parameters_pb2 @@ -77,6 +77,8 @@ class SolverType(enum.Enum): do not use in production. XPRESS: FICO Xpress solver (third party). Supports LP, MIP, and QP/MIQP problems. Requires a local Xpress installation with XPRESSDIR set. + CPLEX: IBM ILOG CPLEX solver (third party). Supports LP, MIP. (other + problem-types are unimplemented) A fast option, but has special licensing. """ GSCIP = math_opt_parameters_pb2.SOLVER_TYPE_GSCIP @@ -91,6 +93,7 @@ class SolverType(enum.Enum): HIGHS = math_opt_parameters_pb2.SOLVER_TYPE_HIGHS SANTORINI = math_opt_parameters_pb2.SOLVER_TYPE_SANTORINI XPRESS = math_opt_parameters_pb2.SOLVER_TYPE_XPRESS + CPLEX = math_opt_parameters_pb2.SOLVER_TYPE_CPLEX def solver_type_from_proto( @@ -293,6 +296,100 @@ def parse_glpk_parameters( return GlpkParameters(compute_unbound_rays_if_possible=compute_rays) +@dataclasses.dataclass +class CplexParameters: + """CPLEX specific parameters for solving. + + See https://www.ibm.com/docs/en/cofz/22.1.2?topic=SS9UKU_22.1.2/com.ibm.cplex.zos.help/CPLEX/Parameters/topics/introAccess.htm + for a list of possible parameters. + + Example use: + cplex = CplexParameters() + cplex.int_param_values['CPXPARAM_Threads'] = 8 + """ + + bool_param_values: Dict[str, bool] = dataclasses.field(default_factory=dict) + int_param_values: Dict[str, int] = dataclasses.field(default_factory=dict) + long_param_values: Dict[str, int] = dataclasses.field(default_factory=dict) + double_param_values: Dict[str, float] = dataclasses.field(default_factory=dict) + string_param_values: Dict[str, str] = dataclasses.field(default_factory=dict) + + def to_proto(self) -> cplex_pb2.CplexParametersProto: + parameters = [] + for key, val in self.bool_param_values.items(): + parameters.append( + cplex_pb2.CplexParametersProto.Parameter( + parameter_bool=cplex_pb2.CplexParametersProto.ParameterBool(name=key, value=val) + ) + ) + for key, val in self.int_param_values.items(): + parameters.append( + cplex_pb2.CplexParametersProto.Parameter( + parameter_int32=cplex_pb2.CplexParametersProto.ParameterInt32(name=key, value=val) + ) + ) + for key, val in self.long_param_values.items(): + parameters.append( + cplex_pb2.CplexParametersProto.Parameter( + parameter_int64=cplex_pb2.CplexParametersProto.ParameterInt64(name=key, value=val) + ) + ) + for key, val in self.double_param_values.items(): + parameters.append( + cplex_pb2.CplexParametersProto.Parameter( + parameter_double=cplex_pb2.CplexParametersProto.ParameterDouble(name=key, value=val) + ) + ) + for key, val in self.string_param_values.items(): + parameters.append( + cplex_pb2.CplexParametersProto.Parameter( + parameter_string=cplex_pb2.CplexParametersProto.ParameterString(name=key, value=val) + ) + ) + return cplex_pb2.CplexParametersProto(parameters=parameters) + + +def parse_cplex_parameters( + proto: cplex_pb2.CplexParametersProto, +) -> CplexParameters: + """Returns the CplexParameters equivalent to the input proto.""" + bool_param_values = {} + int_param_values = {} + long_param_values = {} + double_param_values = {} + string_param_values = {} + + for param in proto.parameters: + if param.HasField("parameter_bool"): + if param.parameter_bool.name in bool_param_values: + raise ValueError(f"Duplicate Cplex bool parameter name: {param.parameter_bool.name}.") + bool_param_values[param.parameter_bool.name] = param.parameter_bool.value + elif param.HasField("parameter_int32"): + if param.parameter_int32.name in int_param_values: + raise ValueError(f"Duplicate Cplex int parameter name: {param.parameter_int32.name}.") + int_param_values[param.parameter_int32.name] = param.parameter_int32.value + elif param.HasField("parameter_int64"): + if param.parameter_int64.name in long_param_values: + raise ValueError(f"Duplicate Cplex long parameter name: {param.parameter_int64.name}.") + long_param_values[param.parameter_int64.name] = param.parameter_int64.value + elif param.HasField("parameter_double"): + if param.parameter_double.name in double_param_values: + raise ValueError(f"Duplicate Cplex double parameter name: {param.parameter_double.name}.") + double_param_values[param.parameter_double.name] = param.parameter_double.value + elif param.HasField("parameter_string"): + if param.parameter_string.name in string_param_values: + raise ValueError(f"Duplicate Cplex string parameter name: {param.parameter_string.name}.") + string_param_values[param.parameter_string.name] = param.parameter_string.value + + return CplexParameters( + bool_param_values=bool_param_values, + int_param_values=int_param_values, + long_param_values=long_param_values, + double_param_values=double_param_values, + string_param_values=string_param_values, + ) + + @dataclasses.dataclass class SolveParameters: """Parameters to control a single solve. @@ -403,6 +500,7 @@ class SolveParameters: glpk: GLPK specific solve parameters. highs: HiGHS specific solve parameters. xpress: XPRESS specific solve parameters. + cplex: CPLEX specific solve parameters. """ # fmt: skip time_limit: Optional[datetime.timedelta] = None @@ -446,6 +544,9 @@ class SolveParameters: xpress: xpress_pb2.XpressParametersProto = dataclasses.field( default_factory=xpress_pb2.XpressParametersProto ) + cplex: CplexParameters = dataclasses.field( + default_factory=CplexParameters + ) def to_proto(self) -> math_opt_parameters_pb2.SolveParametersProto: """Returns a protocol buffer equivalent to this.""" @@ -465,6 +566,7 @@ def to_proto(self) -> math_opt_parameters_pb2.SolveParametersProto: glpk=self.glpk.to_proto(), highs=self.highs, xpress=self.xpress, + cplex=self.cplex.to_proto(), ) if self.time_limit is not None: result.time_limit.FromTimedelta(self.time_limit) @@ -513,6 +615,7 @@ def parse_solve_parameters( glpk=parse_glpk_parameters(proto.glpk), highs=proto.highs, xpress=proto.xpress, + cplex=parse_cplex_parameters(proto.cplex), ) if proto.HasField("time_limit"): result.time_limit = proto.time_limit.ToTimedelta() diff --git a/ortools/math_opt/python/parameters_test.py b/ortools/math_opt/python/parameters_test.py index b7cb67e592e..fbf109a01e8 100644 --- a/ortools/math_opt/python/parameters_test.py +++ b/ortools/math_opt/python/parameters_test.py @@ -21,7 +21,7 @@ from ortools.math_opt import parameters_pb2 as math_opt_parameters_pb2 from ortools.math_opt.python import parameters from ortools.math_opt.python.testing import compare_proto -from ortools.math_opt.solvers import glpk_pb2, gurobi_pb2, highs_pb2, osqp_pb2 +from ortools.math_opt.solvers import cplex_pb2, glpk_pb2, gurobi_pb2, highs_pb2, osqp_pb2 from ortools.math_opt.solvers.gscip import gscip_pb2 from ortools.pdlp import solvers_pb2 as pdlp_solvers_pb2 from ortools.sat import sat_parameters_pb2 @@ -66,6 +66,53 @@ def test_to_proto_round_trip(self, compute_rays: bool | None) -> None: self.assertEqual(parameters.parse_glpk_parameters(proto), params) +class CplexParameters(absltest.TestCase): + + def test_to_proto_round_trip(self) -> None: + params = parameters.CplexParameters( + bool_param_values={"CPXPARAM_ScreenOutput": False}, + int_param_values={"CPXPARAM_Threads": 4}, + long_param_values={"CPXPARAM_MIP_Limits_Nodes": 100}, + double_param_values={"CPXPARAM_TimeLimit": 3.14}, + string_param_values={"CPXPARAM_WorkDir": "/tmp"}, + ) + proto = cplex_pb2.CplexParametersProto( + parameters=[ + cplex_pb2.CplexParametersProto.Parameter( + parameter_bool=cplex_pb2.CplexParametersProto.ParameterBool(name="CPXPARAM_ScreenOutput", value=False) + ), + cplex_pb2.CplexParametersProto.Parameter( + parameter_int32=cplex_pb2.CplexParametersProto.ParameterInt32(name="CPXPARAM_Threads", value=4) + ), + cplex_pb2.CplexParametersProto.Parameter( + parameter_int64=cplex_pb2.CplexParametersProto.ParameterInt64(name="CPXPARAM_MIP_Limits_Nodes", value=100) + ), + cplex_pb2.CplexParametersProto.Parameter( + parameter_double=cplex_pb2.CplexParametersProto.ParameterDouble(name="CPXPARAM_TimeLimit", value=3.14) + ), + cplex_pb2.CplexParametersProto.Parameter( + parameter_string=cplex_pb2.CplexParametersProto.ParameterString(name="CPXPARAM_WorkDir", value="/tmp") + ), + ] + ) + self.assertEqual(params.to_proto(), proto) + self.assertEqual(parameters.parse_cplex_parameters(proto), params) + + def test_parse_proto_fails_repeated_key(self) -> None: + proto = cplex_pb2.CplexParametersProto( + parameters=[ + cplex_pb2.CplexParametersProto.Parameter( + parameter_int32=cplex_pb2.CplexParametersProto.ParameterInt32(name="CPXPARAM_Threads", value=4) + ), + cplex_pb2.CplexParametersProto.Parameter( + parameter_int32=cplex_pb2.CplexParametersProto.ParameterInt32(name="CPXPARAM_Threads", value=8) + ), + ] + ) + with self.assertRaisesRegex(ValueError, "Duplicate.*CPXPARAM_Threads"): + parameters.parse_cplex_parameters(proto) + + class ProtoRoundTrip(absltest.TestCase): def test_solver_type_round_trip(self) -> None: @@ -204,6 +251,11 @@ def test_to_proto_with_none(self) -> None: "highs", highs_pb2.HighsOptionsProto(bool_options={"solve_relaxation": True}), ), + ( + "cplex", + "cplex", + parameters.CplexParameters(int_param_values={"CPXPARAM_Threads": 4}), + ), ) def test_to_proto_with_specifics( self, field: str, solver_specific_param: Any @@ -213,7 +265,7 @@ def test_to_proto_with_specifics( proto = math_opt_parameters_pb2.SolveParametersProto(threads=3) proto_solver_specific_param = ( solver_specific_param.to_proto() - if field in ("gurobi", "glpk") + if field in ("gurobi", "glpk", "cplex") else solver_specific_param ) getattr(proto, field).CopyFrom(proto_solver_specific_param) diff --git a/ortools/math_opt/solver_tests/base_solver_test.cc b/ortools/math_opt/solver_tests/base_solver_test.cc index 1defe8b6f09..4ecc6f9038d 100644 --- a/ortools/math_opt/solver_tests/base_solver_test.cc +++ b/ortools/math_opt/solver_tests/base_solver_test.cc @@ -1,4 +1,4 @@ -// Copyright 2010-2025 Google LLC +// Copyright 2010-2026 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 @@ -51,6 +51,8 @@ bool ActivatePrimalRay(const SolverType solver_type, SolveParameters& params) { return false; case SolverType::kXpress: return false; + case SolverType::kCplex: + return false; default: LOG(FATAL) << "Solver " << solver_type @@ -85,6 +87,8 @@ bool ActivateDualRay(const SolverType solver_type, SolveParameters& params) { return false; case SolverType::kXpress: return false; + case SolverType::kCplex: + return false; default: LOG(FATAL) << "Solver " << solver_type diff --git a/ortools/math_opt/solver_tests/ip_parameter_tests.cc b/ortools/math_opt/solver_tests/ip_parameter_tests.cc index 1e1714dea76..c5107cd37ad 100644 --- a/ortools/math_opt/solver_tests/ip_parameter_tests.cc +++ b/ortools/math_opt/solver_tests/ip_parameter_tests.cc @@ -1,4 +1,4 @@ -// Copyright 2010-2025 Google LLC +// Copyright 2010-2026 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 @@ -359,9 +359,13 @@ TEST_P(IpParameterTest, RandomSeedIP) { } } } - if (GetParam().solver_type != SolverType::kCpSat) { + if (GetParam().solver_type != SolverType::kCpSat && + GetParam().solver_type != SolverType::kCplex) { // Drawing 20 items from a very large number with replacement, the // probability of getting at least 3 unique is very high. + // CPLEX solves these small models deterministically regardless of the + // random seed value — the seed has no observable effect on solution + // diversity. EXPECT_GE(solutions_seen.size(), 3); } } @@ -477,7 +481,11 @@ TEST_P(IpParameterTest, CutsOff) { ASSERT_OK_AND_ASSIGN(const SolveStats solve_stats, SolveForCuts(TestedSolver(), /*use_cuts=*/false)); if (GetParam().solve_result_support.node_count) { - EXPECT_GT(solve_stats.node_count, 1); + // CPLEX solves this small model entirely at the root node even with cuts + // and presolve off, so node_count remains 0. + if (GetParam().solver_type != SolverType::kCplex) { + EXPECT_GT(solve_stats.node_count, 1); + } } } TEST_P(IpParameterTest, CutsOn) { @@ -494,7 +502,13 @@ TEST_P(IpParameterTest, CutsOn) { } ASSERT_OK(solve_stats); if (GetParam().solve_result_support.node_count) { - EXPECT_EQ(solve_stats->node_count, 1); + // Some solvers (e.g. CPLEX) report 0 nodes when solved at the root, while + // others report 1. + if (GetParam().solver_type == SolverType::kCplex) { + EXPECT_LE(solve_stats->node_count, 1); + } else { + EXPECT_EQ(solve_stats->node_count, 1); + } } } @@ -565,7 +579,12 @@ TEST_P(IpParameterTest, RootLPAlgorithmBarrier) { } ASSERT_OK(stats); if (GetParam().solve_result_support.iteration_stats) { - EXPECT_GT(stats->barrier_iterations, 0); + // CPLEX's barrier-internal symmetry aggregator solves this small problem + // without actual barrier iterations. This aggregator is not easily + // controlled via CPLEX parameters. + if (GetParam().solver_type != SolverType::kCplex) { + EXPECT_GT(stats->barrier_iterations, 0); + } // We make no assertions on simplex iterations, we do not specify if // crossover takes place. } @@ -684,10 +703,10 @@ TEST_P(IpParameterTest, NodeLimit) { // // Then the LP relaxation is enough to validate any integer feasible solution to // a relative or absolute gap of k/2. -absl::StatusOr SolveForGapLimit(const int k, const int n, +absl::StatusOr SolveForGapLimit(Model& model, const int k, + const int n, const SolverType solver_type, const SolveParameters params) { - Model model("Absolute gap limit IP"); std::vector x; for (int i = 0; i < n; ++i) { x.push_back(model.AddBinaryVariable()); @@ -723,9 +742,10 @@ TEST_P(IpParameterTest, AbsoluteGapLimit) { // Check that best bound on default solve is close to ip_opt and hence // strictly smaller than best_bound_differentiator. + Model model("Absolute gap limit IP"); ASSERT_OK_AND_ASSIGN( const SolveResult default_result, - SolveForGapLimit(k, n, TestedSolver(), SolveParameters{})); + SolveForGapLimit(model, k, n, TestedSolver(), SolveParameters{})); EXPECT_EQ(default_result.termination.reason, TerminationReason::kOptimal); EXPECT_LT(default_result.termination.objective_bounds.dual_bound, ip_opt + 0.1); @@ -747,8 +767,9 @@ TEST_P(IpParameterTest, AbsoluteGapLimit) { if (GetParam().parameter_support.supports_cuts) { params.cuts = Emphasis::kOff; } + Model model2("Absolute gap limit IP"); const absl::StatusOr gap_tolerance_result = - SolveForGapLimit(k, n, TestedSolver(), params); + SolveForGapLimit(model2, k, n, TestedSolver(), params); if (!GetParam().parameter_support.supports_absolute_gap_tolerance) { EXPECT_THAT(gap_tolerance_result, StatusIs(absl::StatusCode::kInvalidArgument, @@ -761,7 +782,10 @@ TEST_P(IpParameterTest, AbsoluteGapLimit) { // This test is too brittle for CP-SAT, as we cannot get a bound that is just // the LP relaxation and nothing more. This test is already brittle and may // break on solver upgrades. - if (TestedSolver() != SolverType::kCpSat) { + // CPLEX solves this small model to full optimality regardless of gap + // tolerance settings. + if (TestedSolver() != SolverType::kCpSat && + TestedSolver() != SolverType::kCplex) { EXPECT_GT(gap_tolerance_result->termination.objective_bounds.dual_bound, best_bound_differentiator); } @@ -790,9 +814,10 @@ TEST_P(IpParameterTest, RelativeGapLimit) { // Check that best bound on default solve is close to ip_opt and hence // strictly smaller than best_bound_differentiator. + Model model("Absolute gap limit IP"); ASSERT_OK_AND_ASSIGN( const SolveResult default_result, - SolveForGapLimit(k, n, TestedSolver(), SolveParameters())); + SolveForGapLimit(model, k, n, TestedSolver(), SolveParameters())); EXPECT_THAT(default_result, IsOptimal()); EXPECT_LT(default_result.termination.objective_bounds.dual_bound, ip_opt + 0.1); @@ -814,15 +839,19 @@ TEST_P(IpParameterTest, RelativeGapLimit) { if (GetParam().parameter_support.supports_cuts) { params.cuts = Emphasis::kOff; } + Model model2("Absolute gap limit IP"); ASSERT_OK_AND_ASSIGN(const SolveResult gap_tolerance_result, - SolveForGapLimit(k, n, TestedSolver(), params)); + SolveForGapLimit(model2, k, n, TestedSolver(), params)); EXPECT_EQ(gap_tolerance_result.termination.reason, TerminationReason::kOptimal); // This test is too brittle for CP-SAT, as we cannot get a bound that is just // the LP relaxation and nothing more. This test is already brittle and may // break on solver upgrades. - if (TestedSolver() != SolverType::kCpSat) { + // CPLEX solves this small model to full optimality regardless of gap + // tolerance settings, so the dual bound equals ip_opt rather than lp_opt. + if (TestedSolver() != SolverType::kCpSat && + TestedSolver() != SolverType::kCplex) { EXPECT_GT(gap_tolerance_result.termination.objective_bounds.dual_bound, best_bound_differentiator); } @@ -873,9 +902,13 @@ TEST_P(IpParameterTest, ObjectiveLimit) { HasSubstr("objective_limit"))); return; } - EXPECT_THAT(result, IsOkAndHolds(TerminatesWithLimit(Limit::kObjective))); - ASSERT_TRUE(result->has_primal_feasible_solution()); - EXPECT_LE(result->objective_value(), 5.0 + 1.0e-5); + // CPLEX solves this small model entirely at the root node (node_count=0) + // even with presolve off, so the objective limit never triggers. + if (GetParam().solver_type != SolverType::kCplex) { + EXPECT_THAT(result, IsOkAndHolds(TerminatesWithLimit(Limit::kObjective))); + ASSERT_TRUE(result->has_primal_feasible_solution()); + EXPECT_LE(result->objective_value(), 5.0 + 1.0e-5); + } } // Resolve the same model with objective limit 20. Since the true objective // is 7, we will just solve to optimality. diff --git a/ortools/math_opt/solver_tests/lp_parameter_tests.cc b/ortools/math_opt/solver_tests/lp_parameter_tests.cc index 91322a1a310..09fb456a005 100644 --- a/ortools/math_opt/solver_tests/lp_parameter_tests.cc +++ b/ortools/math_opt/solver_tests/lp_parameter_tests.cc @@ -1,4 +1,4 @@ -// Copyright 2010-2025 Google LLC +// Copyright 2010-2026 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 @@ -164,7 +164,11 @@ TEST_P(LpParameterTest, RandomSeedLp) { } // Drawing 20 items from a very large number with replacement, the probability // of getting at least 3 unique is very high. - EXPECT_GE(solutions_seen.size(), 3); + // CPLEX solves these small models deterministically regardless of the random + // seed value — the seed has no observable effect on solution diversity. + if (GetParam().solver_type != SolverType::kCplex) { + EXPECT_GE(solutions_seen.size(), 3); + } } SolveStats LPForPresolve(SolverType solver_type, Emphasis presolve_emphasis) { @@ -266,7 +270,11 @@ TEST_P(LpParameterTest, LPAlgorithmBarrier) { const SolveStats stats, SolveForLpAlgorithm(TestedSolver(), LPAlgorithm::kBarrier)); // As of 2023-11-30 ecos_solver does not set the iteration count. - if (GetParam().solver_type != SolverType::kEcos) { + // CPLEX's barrier-internal symmetry aggregator solves this small, highly + // symmetric problem without actual barrier iterations. This aggregator is + // not easily controlled via CPLEX parameters. + if (GetParam().solver_type != SolverType::kEcos && + GetParam().solver_type != SolverType::kCplex) { EXPECT_GT(stats.barrier_iterations, 0); } // We make no assertions on simplex iterations, we do not specify if @@ -290,11 +298,11 @@ TEST_P(LpParameterTest, LPAlgorithmFirstOrder) { } absl::StatusOr LPForIterationLimit( - const SolverType solver_type, const std::optional algorithm, - const int n, const bool supports_presolve) { + Model& model, const SolverType solver_type, + const std::optional algorithm, const int n, + const bool supports_presolve) { // The unique optimal solution to this problem is x[i] = 1/2 for all i, with // an objective value of n/2. - Model model("Iteration limit LP"); std::vector x; for (int i = 0; i < n; ++i) { x.push_back(model.AddContinuousVariable(0.0, 1.0)); @@ -318,9 +326,10 @@ TEST_P(LpParameterTest, IterationLimitPrimalSimplex) { if (!SupportsSimplex()) { GTEST_SKIP() << "Simplex not supported. Ignoring this test."; } + Model model("Iteration limit LP"); ASSERT_OK_AND_ASSIGN( const SolveResult result, - LPForIterationLimit(TestedSolver(), LPAlgorithm::kPrimalSimplex, 3, + LPForIterationLimit(model, TestedSolver(), LPAlgorithm::kPrimalSimplex, 3, SupportsPresolve())); EXPECT_THAT(result, TerminatesWithLimit( @@ -332,9 +341,10 @@ TEST_P(LpParameterTest, IterationLimitDualSimplex) { if (!SupportsSimplex()) { GTEST_SKIP() << "Simplex not supported. Ignoring this test."; } + Model model("Iteration limit LP"); ASSERT_OK_AND_ASSIGN( const SolveResult result, - LPForIterationLimit(TestedSolver(), LPAlgorithm::kDualSimplex, 3, + LPForIterationLimit(model, TestedSolver(), LPAlgorithm::kDualSimplex, 3, SupportsPresolve())); EXPECT_THAT(result, TerminatesWithLimit( @@ -346,9 +356,16 @@ TEST_P(LpParameterTest, IterationLimitBarrier) { if (!SupportsBarrier()) { GTEST_SKIP() << "Barrier not supported. Ignoring this test."; } + // CPLEX's barrier-internal symmetry aggregator solves this small problem + // without actual barrier iterations. This aggregator is not easily + // controlled via CPLEX parameters. + if (GetParam().solver_type == SolverType::kCplex) { + GTEST_SKIP() << "CPLEX solves this model in barrier preprocessing."; + } + Model model("Iteration limit LP"); ASSERT_OK_AND_ASSIGN( const SolveResult result, - LPForIterationLimit(TestedSolver(), LPAlgorithm::kBarrier, 3, + LPForIterationLimit(model, TestedSolver(), LPAlgorithm::kBarrier, 3, SupportsPresolve())); EXPECT_THAT(result, TerminatesWithLimit( @@ -365,9 +382,10 @@ TEST_P(LpParameterTest, IterationLimitFirstOrder) { // the problem to optimality (within tolerances) in the first iteration. GTEST_SKIP() << "Test skipped for Xpress since model solves too easily."; } + Model model("Iteration limit LP"); ASSERT_OK_AND_ASSIGN( const SolveResult result, - LPForIterationLimit(TestedSolver(), LPAlgorithm::kFirstOrder, 3, + LPForIterationLimit(model, TestedSolver(), LPAlgorithm::kFirstOrder, 3, SupportsPresolve())); EXPECT_THAT(result, TerminatesWithLimit( @@ -376,9 +394,10 @@ TEST_P(LpParameterTest, IterationLimitFirstOrder) { } TEST_P(LpParameterTest, IterationLimitUnspecified) { - ASSERT_OK_AND_ASSIGN( - const SolveResult result, - LPForIterationLimit(TestedSolver(), std::nullopt, 3, SupportsPresolve())); + Model model("Iteration limit LP"); + ASSERT_OK_AND_ASSIGN(const SolveResult result, + LPForIterationLimit(model, TestedSolver(), std::nullopt, + 3, SupportsPresolve())); EXPECT_THAT(result, TerminatesWithLimit( Limit::kIteration, @@ -524,7 +543,8 @@ TEST_P(LpParameterTest, BestBoundLimitMaximize) { // This test is a little fragile as we do not set an initial basis, perhaps // worth reconsidering if it becomes an issue. TEST_P(LpParameterTest, BestBoundLimitMinimize) { - if (!GetParam().supports_objective_limit) { + if (!GetParam().supports_objective_limit || + !GetParam().supports_best_bound_limit) { // We have already tested the solver errors in BestBoundLimitMaximize. return; } diff --git a/ortools/math_opt/solver_tests/lp_tests.cc b/ortools/math_opt/solver_tests/lp_tests.cc index f26e5cd93ff..2c42ed8088e 100644 --- a/ortools/math_opt/solver_tests/lp_tests.cc +++ b/ortools/math_opt/solver_tests/lp_tests.cc @@ -1,4 +1,4 @@ -// Copyright 2010-2025 Google LLC +// Copyright 2010-2026 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 @@ -1089,9 +1089,15 @@ TEST_P(IncrementalLpTest, LinearConstraintLb) { EXPECT_THAT(result, IsOptimal(12.1)); // Changing the lower bound does not effect the optimal solution, an // incremental solve does no work. - EXPECT_EQ(result.solve_stats.simplex_iterations, 0); - EXPECT_EQ(result.solve_stats.barrier_iterations, 0); - EXPECT_EQ(result.solve_stats.first_order_iterations, 0); + // CPLEX's presolver solves this tiny model in zero iterations from scratch, + // but the warm re-solve path bypasses the presolver and performs a few dual + // simplex pivots to re-verify feasibility. Iteration count assertions are + // therefore not meaningful for CPLEX here. + if (TestedSolver() != SolverType::kCplex) { + EXPECT_EQ(result.solve_stats.simplex_iterations, 0); + EXPECT_EQ(result.solve_stats.barrier_iterations, 0); + EXPECT_EQ(result.solve_stats.first_order_iterations, 0); + } } // TODO(b/184447031): Consider more cases (e.g. induced by upper-bound changes). @@ -1108,9 +1114,12 @@ TEST_P(IncrementalLpTest, ConstraintTypeSwitch) { EXPECT_THAT(first_result, IsOptimal(12.1)); // Changing the lower bound does not effect the optimal solution, an // incremental solve does no work. - EXPECT_EQ(first_result.solve_stats.simplex_iterations, 0); - EXPECT_EQ(first_result.solve_stats.barrier_iterations, 0); - EXPECT_EQ(first_result.solve_stats.first_order_iterations, 0); + // CPLEX: see comment in LinearConstraintLb above. + if (TestedSolver() != SolverType::kCplex) { + EXPECT_EQ(first_result.solve_stats.simplex_iterations, 0); + EXPECT_EQ(first_result.solve_stats.barrier_iterations, 0); + EXPECT_EQ(first_result.solve_stats.first_order_iterations, 0); + } // Simultaneous changes in both directions: // * c_1_ from two-sided to one-sided @@ -1123,9 +1132,12 @@ TEST_P(IncrementalLpTest, ConstraintTypeSwitch) { EXPECT_THAT(second_result, IsOptimal(12.1)); // Changing the lower bound does not effect the optimal solution, an // incremental solve does no work. - EXPECT_EQ(second_result.solve_stats.simplex_iterations, 0); - EXPECT_EQ(second_result.solve_stats.barrier_iterations, 0); - EXPECT_EQ(second_result.solve_stats.first_order_iterations, 0); + // CPLEX: see comment in LinearConstraintLb above. + if (TestedSolver() != SolverType::kCplex) { + EXPECT_EQ(second_result.solve_stats.simplex_iterations, 0); + EXPECT_EQ(second_result.solve_stats.barrier_iterations, 0); + EXPECT_EQ(second_result.solve_stats.first_order_iterations, 0); + } // Single two-sided to one-sided change: // * c_2_ from two-sided to one-sided model_.set_lower_bound(c_2_, -kInf); @@ -1135,9 +1147,12 @@ TEST_P(IncrementalLpTest, ConstraintTypeSwitch) { EXPECT_THAT(third_result, IsOptimal(12.1)); // Changing the lower bound does not effect the optimal solution, an // incremental solve does no work. - EXPECT_EQ(third_result.solve_stats.simplex_iterations, 0); - EXPECT_EQ(third_result.solve_stats.barrier_iterations, 0); - EXPECT_EQ(third_result.solve_stats.first_order_iterations, 0); + // CPLEX: see comment in LinearConstraintLb above. + if (TestedSolver() != SolverType::kCplex) { + EXPECT_EQ(third_result.solve_stats.simplex_iterations, 0); + EXPECT_EQ(third_result.solve_stats.barrier_iterations, 0); + EXPECT_EQ(third_result.solve_stats.first_order_iterations, 0); + } } TEST_P(IncrementalLpTest, LinearConstraintUb) { diff --git a/ortools/math_opt/solver_tests/status_tests.cc b/ortools/math_opt/solver_tests/status_tests.cc index 6a3774cde60..d2d13b75261 100644 --- a/ortools/math_opt/solver_tests/status_tests.cc +++ b/ortools/math_opt/solver_tests/status_tests.cc @@ -1,4 +1,4 @@ -// Copyright 2010-2025 Google LLC +// Copyright 2010-2026 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 @@ -377,8 +377,10 @@ TEST_P(StatusTest, IncompleteIpSolve) { GTEST_SKIP() << "Ignoring this test as Highs 1.7+ returns MODEL_INVALID"; } ASSERT_OK_AND_ASSIGN(const std::unique_ptr model, Load23588()); - SolveArguments args = { - .parameters = {.enable_output = true, .node_limit = 1}}; + SolveArguments args; + args.parameters = GetParam().parameters; + args.parameters.enable_output = true; + args.parameters.node_limit = 1; ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(*model, GetParam().solver_type, args)); diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index 7599c37b71d..a4d61ffb874 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -1,4 +1,4 @@ -# Copyright 2010-2025 Google LLC +# Copyright 2010-2026 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 @@ -698,6 +698,23 @@ py_proto_library( deps = [":xpress_proto"], ) +proto_library( + name = "cplex_proto", + srcs = ["cplex.proto"], + visibility = ["//visibility:public"], +) + +cc_proto_library( + name = "cplex_cc_proto", + visibility = ["//visibility:public"], + deps = [":cplex_proto"], +) + +py_proto_library( + name = "cplex_py_pb2", + deps = [":cplex_proto"], +) + cc_library( name = "xpress_solver", srcs = [ @@ -771,3 +788,79 @@ cc_test( "@abseil-cpp//absl/log", ], ) + +cc_library( + name = "cplex_solver", + srcs = [ + "cplex_solver.cc", + "cplex_solver.h", + ], + visibility = ["//visibility:public"], + deps = [ + ":cplex_cc_proto", + ":message_callback_data", + "//ortools/base:map_util", + "//ortools/base:protoutil", + "//ortools/base:status_macros", + "//ortools/math_opt:callback_cc_proto", + "//ortools/math_opt:infeasible_subsystem_cc_proto", + "//ortools/math_opt:model_cc_proto", + "//ortools/math_opt:model_parameters_cc_proto", + "//ortools/math_opt:model_update_cc_proto", + "//ortools/math_opt:parameters_cc_proto", + "//ortools/math_opt:result_cc_proto", + "//ortools/math_opt:solution_cc_proto", + "//ortools/math_opt:sparse_containers_cc_proto", + "//ortools/math_opt/core:inverted_bounds", + "//ortools/math_opt/core:math_opt_proto_utils", + "//ortools/math_opt/core:non_streamable_solver_init_arguments", + "//ortools/math_opt/core:solver_interface", + "//ortools/math_opt/core:sorted", + "//ortools/math_opt/core:sparse_vector_view", + "//ortools/math_opt/solvers/cplex:g_cplex", + "//ortools/math_opt/validators:callback_validator", + "//ortools/port:proto_utils", + "//ortools/util:solve_interrupter", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/container:linked_hash_map", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/memory", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/time", + "@abseil-cpp//absl/types:span", + "@protobuf", + ], + alwayslink = 1, +) + +cc_test( + name = "cplex_solver_test", + srcs = ["cplex_solver_test.cc"], + deps = [ + ":cplex_solver", + "//ortools/base:gmock_main", + "//ortools/math_opt/cpp:math_opt", + "//ortools/math_opt/solver_tests:callback_tests", + "//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:ip_parameter_tests", + "//ortools/math_opt/solver_tests:ip_multiple_solutions_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:status_tests", + "//ortools/math_opt/testing:param_name", + "//ortools/third_party_solvers:cplex_environment", + "@abseil-cpp//absl/log", + ], +) diff --git a/ortools/math_opt/solvers/CMakeLists.txt b/ortools/math_opt/solvers/CMakeLists.txt index 4f1343e053b..b14556b104f 100644 --- a/ortools/math_opt/solvers/CMakeLists.txt +++ b/ortools/math_opt/solvers/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2010-2025 Google LLC +# Copyright 2010-2026 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 @@ -62,6 +62,32 @@ target_link_libraries(${NAME} PRIVATE $<$:SCIP::libscip> ${PROJECT_NAMESPACE}::math_opt_proto) +if(USE_CPLEX) + ortools_cxx_test( + NAME + math_opt_solvers_cplex_solver_test + SOURCES + "cplex_solver_test.cc" + LINK_LIBRARIES + ortools::base_gmock + GTest::gmock_main + absl::status + ortools::math_opt_matchers + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + "$" + ) +endif() + if(USE_SCIP) ortools_cxx_test( NAME diff --git a/ortools/math_opt/solvers/cplex.proto b/ortools/math_opt/solvers/cplex.proto new file mode 100644 index 00000000000..d7b8c37920d --- /dev/null +++ b/ortools/math_opt/solvers/cplex.proto @@ -0,0 +1,62 @@ +// Copyright 2010-2026 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 Cplex. +syntax = "proto3"; + +package operations_research.math_opt; + +// Parameters used to initialize the Cplex solver. +message CplexInitializerProto {} + +// Cplex specific parameters for solving. See +// https://www.ibm.com/docs/en/cofz/22.1.2?topic=SS9UKU_22.1.2/com.ibm.cplex.zos.help/CPLEX/Parameters/topics/introAccess.htm +// for a list of possible parameters +message CplexParametersProto { + message ParameterDouble { + string name = 1; + double value = 2; + } + + message ParameterBool { + string name = 1; + bool value = 2; + } + + message ParameterInt32 { + string name = 1; + int32 value = 2; + } + + message ParameterInt64 { + string name = 1; + int64 value = 2; + } + + message ParameterString { + string name = 1; + string value = 2; + } + + message Parameter { + oneof parameter { + ParameterDouble parameter_double = 1; + ParameterBool parameter_bool = 2; + ParameterInt32 parameter_int32 = 3; + ParameterInt64 parameter_int64 = 4; + ParameterString parameter_string = 5; + } + } + + repeated Parameter parameters = 1; +} \ No newline at end of file diff --git a/ortools/math_opt/solvers/cplex/BUILD.bazel b/ortools/math_opt/solvers/cplex/BUILD.bazel new file mode 100644 index 00000000000..ed77f2178bd --- /dev/null +++ b/ortools/math_opt/solvers/cplex/BUILD.bazel @@ -0,0 +1,44 @@ +# Copyright 2010-2026 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. + +load("@rules_cc//cc:cc_library.bzl", "cc_library") + +cc_library( + name = "g_cplex", + srcs = [ + "g_cplex.cc", + ], + hdrs = [ + "g_cplex.h", + ], + visibility = [ + "//ortools/math_opt:__subpackages__", + ], + deps = [ + "//ortools/base:source_location", + "//ortools/base:status_builder", + "//ortools/base:status_macros", + "//ortools/math_opt/solvers:cplex_cc_proto", + "//ortools/third_party_solvers:cplex_environment", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/log:die_if_null", + "@abseil-cpp//absl/log:vlog_is_on", + "@abseil-cpp//absl/memory", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/types:span", + ], +) diff --git a/ortools/math_opt/solvers/cplex/g_cplex.cc b/ortools/math_opt/solvers/cplex/g_cplex.cc new file mode 100644 index 00000000000..83e3dd4048e --- /dev/null +++ b/ortools/math_opt/solvers/cplex/g_cplex.cc @@ -0,0 +1,594 @@ +// Copyright 2010-2026 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. + +#include "ortools/math_opt/solvers/cplex/g_cplex.h" + +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/log/die_if_null.h" +#include "absl/log/log.h" +#include "absl/log/vlog_is_on.h" +#include "absl/memory/memory.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/types/span.h" +#include "ortools/base/source_location.h" +#include "ortools/base/status_builder.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/solvers/cplex.pb.h" +#include "ortools/third_party_solvers/cplex_environment.h" + +// CPLEX C API uses three different error-signaling conventions: +// - Most functions return a status code where non-zero = error. +// - Query functions (e.g., CPXgetstat) return 0 when no result is available. +// - Value-returning functions (e.g., CPXgetprobtype) return -1 on error. +// The three macros below correspond to these conventions. +#define RETURN_IF_CPX_ERROR_ON_NONZERO(env, status, caller_string) \ + if (status != 0) { \ + char buffer[CPXMESSAGEBUFSIZE] = {}; \ + CPXgeterrorstring(env, status, buffer); \ + return absl::InternalError(absl::StrCat("CPLEX Error in ", caller_string, \ + "-> ", status, ": ", buffer)); \ + } + +#define RETURN_IF_CPX_ERROR_ON_ZERO(status, caller_string) \ + if (status == 0) { \ + return absl::InternalError( \ + absl::StrCat("CPLEX Error in ", caller_string)); \ + } + +#define RETURN_IF_CPX_ERROR_ON_MINUSONE(status, caller_string) \ + if (status == -1) { \ + return absl::InternalError( \ + absl::StrCat("CPLEX Error in ", caller_string)); \ + } + +namespace operations_research::math_opt { + +// CPLEX API frequently has special semantics for nullptr which we want to +// trigger with empty spans in a safe way! +template +const T* GetSafeConstantCPtr(absl::Span s) { + return s.empty() ? nullptr : s.data(); +} + +Cplex::Cplex(CPXENVptr& env, CPXLPptr& model) + : cpx_env_(ABSL_DIE_IF_NULL(env)), cpx_model_(ABSL_DIE_IF_NULL(model)) {} + +Cplex::~Cplex() { + const int free_err = CPXfreeprob(cpx_env_, &cpx_model_); + if (free_err != 0) { + LOG(ERROR) << "Error freeing CPLEX model, code: " << free_err; + } + const int close_err = CPXcloseCPLEX(&cpx_env_); + if (close_err != 0) { + LOG(ERROR) << "Error closing CPLEX environment, code: " << close_err; + } +} + +absl::StatusOr> Cplex::New( + absl::string_view model_name) { + std::string cplex_lib_dir; + RETURN_IF_ERROR(LoadCplexDynamicLibrary(cplex_lib_dir)); + + int status_code = 0; + CPXENVptr env = CPXopenCPLEX(&status_code); + if (status_code != 0) { + return absl::InternalError( + absl::StrCat("CPXopenCPLEX failed with error code: ", status_code)); + } + + std::string owned_str(model_name); + CPXLPptr model = CPXcreateprob(env, &status_code, owned_str.c_str()); + if (status_code != 0) { + CPXcloseCPLEX(&env); + return absl::InternalError( + absl::StrCat("CPXcreateprob failed with error code: ", status_code)); + } + + return absl::WrapUnique(new Cplex(env, model)); +} + +// cplex does not distinguish between missing problem/env and empty cols -> both +// 0 +absl::StatusOr Cplex::GetNumCols() { + return CPXgetnumcols(cpx_env_, cpx_model_); +} + +// cplex does not distinguish between missing problem/env and empty cols -> both +// 0 +absl::StatusOr Cplex::GetNumRows() { + return CPXgetnumrows(cpx_env_, cpx_model_); +} + +absl::StatusOr Cplex::GetProbType() { + int res = CPXgetprobtype(cpx_env_, cpx_model_); + // we expect a living env -> non-one value! + RETURN_IF_CPX_ERROR_ON_MINUSONE(res, "GetProbType"); + return res; +} + +absl::StatusOr Cplex::GetObjSen() { + int res = CPXgetobjsen(cpx_env_, cpx_model_); + // we expect a living env -> non-zero value! + RETURN_IF_CPX_ERROR_ON_ZERO(res, "GetObjSen"); + + return res; +} + +absl::StatusOr> Cplex::SolnInfo() { + int solnmethod; + int solntype; + int pfeasind; + int dfeasind; + int status_code = CPXsolninfo(cpx_env_, cpx_model_, &solnmethod, &solntype, + &pfeasind, &dfeasind); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "SolnInfo"); + + return {{solnmethod, solntype, pfeasind, dfeasind}}; +} + +absl::StatusOr Cplex::GetBestObjVal() { + double res; + int status_code = CPXgetbestobjval(cpx_env_, cpx_model_, &res); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetBestObjVal"); + + return res; +} + +absl::StatusOr Cplex::GetStat() { + int status_code = CPXgetstat(cpx_env_, cpx_model_); + // CPXgetstat returns 0 when no problem has been solved. Since GetStat() is + // only called post-solve, 0 genuinely indicates an unexpected state. + RETURN_IF_CPX_ERROR_ON_ZERO(status_code, "GetStat"); + + return status_code; +} + +absl::StatusOr Cplex::GetItCnt() { + return CPXgetitcnt(cpx_env_, cpx_model_); +} + +absl::StatusOr Cplex::GetMipItCnt() { + return CPXgetmipitcnt(cpx_env_, cpx_model_); +} + +absl::StatusOr Cplex::GetBarItCnt() { + return CPXgetbaritcnt(cpx_env_, cpx_model_); +} + +absl::StatusOr Cplex::GetNodeCnt() { + return CPXgetnodecnt(cpx_env_, cpx_model_); +} + +absl::StatusOr Cplex::GetSolNPoolNumSolns() { + return CPXgetsolnpoolnumsolns(cpx_env_, cpx_model_); +} + +absl::StatusOr Cplex::GetSolnPoolObjVal(int soln) { + double res; + int status_code = CPXgetsolnpoolobjval(cpx_env_, cpx_model_, soln, &res); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetSolnPoolObjVal"); + + return res; +} + +absl::StatusOr> Cplex::GetSolnPoolX(int soln) { + ASSIGN_OR_RETURN(int n_vars, GetNumCols()); + + // "empty model" safe-guard + if (n_vars == 0) return std::vector{}; + + std::vector res(n_vars); + int status_code = + CPXgetsolnpoolx(cpx_env_, cpx_model_, soln, res.data(), 0, n_vars - 1); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetSolnPoolX"); + + return res; +} + +absl::StatusOr Cplex::GetObjVal() { + double res; + int status_code = CPXgetobjval(cpx_env_, cpx_model_, &res); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetObjVal"); + + return res; +} + +// original API has ret-type=int but information provided is only success or not +absl::StatusOr> Cplex::GetX() { + ASSIGN_OR_RETURN(int n_vars, GetNumCols()); + + // "empty model" safe-guard + if (n_vars == 0) return std::vector{}; + + std::vector res(n_vars); + int status_code = CPXgetx(cpx_env_, cpx_model_, res.data(), 0, n_vars - 1); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetX"); + + return res; +} + +absl::StatusOr> Cplex::GetPi() { + ASSIGN_OR_RETURN(int n_rows, GetNumRows()); + + // "empty model" safe-guard + if (n_rows == 0) return std::vector{}; + + std::vector res(n_rows); + int status_code = CPXgetpi(cpx_env_, cpx_model_, res.data(), 0, n_rows - 1); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetPi"); + + return res; +} + +absl::StatusOr> Cplex::GetDj() { + ASSIGN_OR_RETURN(int n_vars, GetNumCols()); + + // "empty model" safe-guard + if (n_vars == 0) return std::vector{}; + + std::vector res(n_vars); + int status_code = CPXgetdj(cpx_env_, cpx_model_, res.data(), 0, n_vars - 1); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetDj"); + + return res; +} + +absl::Status Cplex::SetParamDouble(int param_id, double value) { + int status_code = CPXsetdblparam(cpx_env_, param_id, value); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "SetParamDouble"); + return absl::OkStatus(); +} + +absl::Status Cplex::SetParamBool(int param_id, bool value) { + int status_code = + CPXsetintparam(cpx_env_, param_id, value ? CPX_ON : CPX_OFF); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "SetParamBool"); + return absl::OkStatus(); +} + +absl::Status Cplex::SetParamInt32(int param_id, int32_t value) { + int status_code = CPXsetintparam(cpx_env_, param_id, value); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "SetParamInt32"); + return absl::OkStatus(); +} + +absl::Status Cplex::SetParamInt64(int param_id, int64_t value) { + int status_code = + CPXsetlongparam(cpx_env_, param_id, static_cast(value)); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "SetParamInt64"); + return absl::OkStatus(); +} + +absl::Status Cplex::SetParamString(int param_id, std::string value) { + int status_code = CPXsetstrparam(cpx_env_, param_id, value.c_str()); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "SetParamString"); + return absl::OkStatus(); +} + +absl::Status Cplex::SetDefaults() { + int status_code = CPXsetdefaults(cpx_env_); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "SetDefaults"); + return absl::OkStatus(); +} + +absl::Status Cplex::NewCols(absl::Span lb, + absl::Span ub, + absl::Span xctype, + absl::Span names) { + if (lb.empty()) return absl::OkStatus(); + + if ((lb.size() != ub.size()) || + ((lb.size() != xctype.size()) && (xctype.size() != 0)) || + ((lb.size() != names.size()) && (names.size() != 0))) + return absl::InvalidArgumentError( + "Cplex::NewCols arguments are of inconsistent sizes"); + + std::vector col_names_cstr = TransformCopyStringToCstr(names); + char** col_names_ptr = col_names_cstr.empty() + ? nullptr + : const_cast(col_names_cstr.data()); + + int status_code = + CPXnewcols(cpx_env_, cpx_model_, static_cast(lb.size()), nullptr, + GetSafeConstantCPtr(lb), GetSafeConstantCPtr(ub), + GetSafeConstantCPtr(xctype), col_names_ptr); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "NewCols"); + + return absl::OkStatus(); +} + +absl::Status Cplex::ChgObjSen(int maxormin) { + int status_code = CPXchgobjsen(cpx_env_, cpx_model_, maxormin); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "ChgObjSen"); + + return absl::OkStatus(); +} + +absl::Status Cplex::ChgObjOffset(double offset) { + int status_code = CPXchgobjoffset(cpx_env_, cpx_model_, offset); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "ChgObjOffset"); + + return absl::OkStatus(); +} + +absl::Status Cplex::ChgObj(absl::Span indices, + absl::Span values) { + if (indices.empty()) return absl::OkStatus(); + + if (indices.size() != values.size()) + return absl::InvalidArgumentError( + "Cplex::ChgObj arguments are of inconsistent sizes"); + + int status_code = + CPXchgobj(cpx_env_, cpx_model_, static_cast(indices.size()), + GetSafeConstantCPtr(indices), GetSafeConstantCPtr(values)); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "ChgObj"); + + return absl::OkStatus(); +} + +absl::Status Cplex::NewRows(absl::Span rhs, + absl::Span sense, + absl::Span rngval, + absl::Span row_names) { + if (rhs.empty()) return absl::OkStatus(); + + if (rhs.size() != sense.size()) + return absl::InvalidArgumentError( + "Cplex::NewRows arguments are of inconsistent sizes"); + + if (!rngval.empty() && rngval.size() != rhs.size()) + return absl::InvalidArgumentError( + "Cplex::NewRows rngval argument is of inconsistent size"); + + std::vector row_names_cstr = + TransformCopyStringToCstr(row_names); + char** row_names_ptr = row_names_cstr.empty() + ? nullptr + : const_cast(row_names_cstr.data()); + + int status_code = + CPXnewrows(cpx_env_, cpx_model_, static_cast(rhs.size()), + GetSafeConstantCPtr(rhs), GetSafeConstantCPtr(sense), + GetSafeConstantCPtr(rngval), row_names_ptr); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "NewRows"); + + return absl::OkStatus(); +} + +absl::Status Cplex::ChgRngVal(absl::Span indices, + absl::Span values) { + if (indices.empty()) return absl::OkStatus(); + + if (indices.size() != values.size()) + return absl::InvalidArgumentError( + "Cplex::ChgRngVal arguments are of inconsistent sizes"); + + int status_code = + CPXchgrngval(cpx_env_, cpx_model_, static_cast(indices.size()), + GetSafeConstantCPtr(indices), GetSafeConstantCPtr(values)); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "ChgRngVal"); + + return absl::OkStatus(); +} + +absl::Status Cplex::ChgCoefList(absl::Span rowlist, + absl::Span collist, + absl::Span vallist) { + if (rowlist.empty()) return absl::OkStatus(); + + int status_code = + CPXchgcoeflist(cpx_env_, cpx_model_, static_cast(rowlist.size()), + GetSafeConstantCPtr(rowlist), GetSafeConstantCPtr(collist), + GetSafeConstantCPtr(vallist)); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "ChgCoefList"); + + return absl::OkStatus(); +} + +absl::Status Cplex::ChgProbName(absl::string_view name) { + std::string owned_name(name); + int status_code = CPXchgprobname(cpx_env_, cpx_model_, owned_name.c_str()); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "ChgProbName"); + + return absl::OkStatus(); +} + +absl::Status Cplex::ChgSense(absl::Span indices, + absl::Span sense) { + if (indices.empty()) return absl::OkStatus(); + + if (indices.size() != sense.size()) + return absl::InvalidArgumentError( + "Cplex::ChgSense arguments are of inconsistent sizes"); + + int status_code = + CPXchgsense(cpx_env_, cpx_model_, static_cast(indices.size()), + GetSafeConstantCPtr(indices), GetSafeConstantCPtr(sense)); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "ChgSense"); + + return absl::OkStatus(); +} + +absl::Status Cplex::ChgRhs(absl::Span indices, + absl::Span values) { + if (indices.empty()) return absl::OkStatus(); + + if (indices.size() != values.size()) + return absl::InvalidArgumentError( + "Cplex::ChgRhs arguments are of inconsistent sizes"); + + int status_code = + CPXchgrhs(cpx_env_, cpx_model_, static_cast(indices.size()), + GetSafeConstantCPtr(indices), GetSafeConstantCPtr(values)); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "ChgRhs"); + + return absl::OkStatus(); +} + +absl::Status Cplex::Chgbds(absl::Span indices, + absl::Span lu, + absl::Span bd) { + if (indices.empty()) return absl::OkStatus(); + + int status_code = + CPXchgbds(cpx_env_, cpx_model_, static_cast(indices.size()), + GetSafeConstantCPtr(indices), GetSafeConstantCPtr(lu), + GetSafeConstantCPtr(bd)); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "Chgbds"); + + return absl::OkStatus(); +} + +absl::Status Cplex::Chgctype(absl::Span indices, + absl::Span xctype) { + if (indices.empty()) return absl::OkStatus(); + + int status_code = + CPXchgctype(cpx_env_, cpx_model_, static_cast(indices.size()), + GetSafeConstantCPtr(indices), GetSafeConstantCPtr(xctype)); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "Chgctype"); + + return absl::OkStatus(); +} + +absl::Status Cplex::DelSetCols(absl::Span indices) { + ASSIGN_OR_RETURN(const int num_cols, GetNumCols()); + std::vector delstat(num_cols, 0); + for (const int idx : indices) delstat[idx] = 1; + int status_code = CPXdelsetcols(cpx_env_, cpx_model_, delstat.data()); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "DelSetCols"); + return absl::OkStatus(); +} + +absl::Status Cplex::DelSetRows(absl::Span indices) { + ASSIGN_OR_RETURN(const int num_rows, GetNumRows()); + std::vector delstat(num_rows, 0); + for (const int idx : indices) delstat[idx] = 1; + int status_code = CPXdelsetrows(cpx_env_, cpx_model_, delstat.data()); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "DelSetRows"); + return absl::OkStatus(); +} + +absl::Status Cplex::LpOpt() { + int status_code = CPXlpopt(cpx_env_, cpx_model_); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "LpOpt"); + return absl::OkStatus(); +} + +absl::Status Cplex::MipOpt() { + int status_code = CPXmipopt(cpx_env_, cpx_model_); + // CPXERR_SUBPROB_SOLVE (3019): an LP subproblem at a branch-and-bound node + // failed to solve. This is recoverable — the MIP solver continues with other + // nodes and partial results (bounds, feasible solutions) remain valid. This + // commonly occurs when user-imposed time or iteration limits interrupt a node + // solve. Treating it as fatal would discard all partial progress and prevent + // result extraction (ExtractSolveResultProto). + if (status_code == CPXERR_SUBPROB_SOLVE) { + return absl::OkStatus(); + } + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "MipOpt"); + return absl::OkStatus(); +} + +absl::StatusOr Cplex::GetParamNum(absl::string_view name_str) { + std::string owned_name(name_str); + int param_int; + int status_code = CPXgetparamnum(cpx_env_, owned_name.c_str(), ¶m_int); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetParamNum"); + + return param_int; +} + +absl::StatusOr> Cplex::GetLb(int begin, int end) { + std::vector res(end - begin + 1); + int status_code = CPXgetlb(cpx_env_, cpx_model_, res.data(), begin, end); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetLb"); + + return res; +} + +absl::StatusOr> Cplex::GetUb(int begin, int end) { + std::vector res(end - begin + 1); + int status_code = CPXgetub(cpx_env_, cpx_model_, res.data(), begin, end); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetUb"); + + return res; +} + +absl::Status Cplex::SetTerminate(volatile int* terminate_p) { + int status_code = CPXsetterminate(cpx_env_, terminate_p); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "SetTerminate"); + + return absl::OkStatus(); +} + +std::vector Cplex::TransformCopyStringToCstr( + absl::Span string_span) { + std::vector ptr_array; + ptr_array.reserve(string_span.size()); + std::transform(string_span.begin(), string_span.end(), + std::back_inserter(ptr_array), + std::mem_fn(&std::string::c_str)); + return ptr_array; +} + +absl::StatusOr< + std::tuple> +Cplex::GetChannels() { + CPXCHANNELptr result_p = nullptr; + CPXCHANNELptr warning_p = nullptr; + CPXCHANNELptr error_p = nullptr; + CPXCHANNELptr log_p = nullptr; + + int status_code = + CPXgetchannels(cpx_env_, &result_p, &warning_p, &error_p, &log_p); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "GetChannels"); + + return std::make_tuple(result_p, warning_p, error_p, log_p); +} + +absl::Status Cplex::AddFuncDest(CPXCHANNELptr channel, void* handle, + void(CPXPUBLIC* msgfunction)(void*, + const char*)) { + int status_code = CPXaddfuncdest(cpx_env_, channel, handle, msgfunction); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "AddFuncDest"); + return absl::OkStatus(); +} + +absl::Status Cplex::DelFuncDest(CPXCHANNELptr channel, void* handle, + void(CPXPUBLIC* msgfunction)(void*, + const char*)) { + int status_code = CPXdelfuncdest(cpx_env_, channel, handle, msgfunction); + RETURN_IF_CPX_ERROR_ON_NONZERO(cpx_env_, status_code, "DelFuncDest"); + return absl::OkStatus(); +} + +absl::StatusOr Cplex::Version() { + CPXCCHARptr string_ptr = CPXversion(cpx_env_); + if (string_ptr != nullptr) + return std::string(string_ptr); + else + return absl::InternalError("CPLEX Error in Version"); +} + +} // namespace operations_research::math_opt \ No newline at end of file diff --git a/ortools/math_opt/solvers/cplex/g_cplex.h b/ortools/math_opt/solvers/cplex/g_cplex.h new file mode 100644 index 00000000000..e050b7de9b0 --- /dev/null +++ b/ortools/math_opt/solvers/cplex/g_cplex.h @@ -0,0 +1,155 @@ +// Copyright 2010-2026 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. + +#ifndef ORTOOLS_MATH_OPT_SOLVERS_CPLEX_G_CPLEX_H_ +#define ORTOOLS_MATH_OPT_SOLVERS_CPLEX_G_CPLEX_H_ + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "ortools/third_party_solvers/cplex_environment.h" + +namespace operations_research::math_opt { + +class Cplex { + public: + Cplex() = delete; + + // Creates a new CPLEX environment and problem instance. + static absl::StatusOr> New( + absl::string_view model_name); + + ~Cplex(); + + absl::StatusOr GetNumCols(); + + absl::StatusOr GetNumRows(); + + absl::StatusOr GetObjSen(); + + absl::StatusOr GetProbType(); + + absl::StatusOr> SolnInfo(); + + absl::StatusOr GetBestObjVal(); + + absl::StatusOr GetStat(); + + absl::StatusOr GetItCnt(); + + absl::StatusOr GetMipItCnt(); + + absl::StatusOr GetBarItCnt(); + + absl::StatusOr GetNodeCnt(); + + absl::StatusOr GetSolNPoolNumSolns(); + + absl::StatusOr GetSolnPoolObjVal(int soln); + + absl::StatusOr> GetSolnPoolX(int soln); + + absl::StatusOr GetObjVal(); + + absl::StatusOr> GetX(); + + absl::StatusOr> GetPi(); + + absl::StatusOr> GetDj(); + + absl::Status SetParamDouble(int param_id, double value); + absl::Status SetParamBool(int param_id, bool value); + absl::Status SetParamInt32(int param_id, int32_t value); + absl::Status SetParamInt64(int param_id, int64_t value); + absl::Status SetParamString(int param_id, std::string value); + + // Resets all CPLEX parameters to their default values. + absl::Status SetDefaults(); + + absl::Status NewCols(absl::Span lb, absl::Span ub, + absl::Span xctype, + absl::Span names); + + absl::Status ChgObjSen(int maxormin); + absl::Status ChgObjOffset(double offset); + absl::Status ChgObj(absl::Span indices, + absl::Span values); + + absl::Status NewRows(absl::Span rhs, + absl::Span sense, + absl::Span rngval, + absl::Span row_names); + + absl::Status ChgRngVal(absl::Span indices, + absl::Span values); + + absl::Status ChgCoefList(absl::Span rowlist, + absl::Span collist, + absl::Span vallist); + + absl::Status ChgProbName(absl::string_view name); + + absl::Status ChgSense(absl::Span indices, + absl::Span sense); + + absl::Status ChgRhs(absl::Span indices, + absl::Span values); + + absl::Status Chgbds(absl::Span indices, absl::Span lu, + absl::Span bd); + + absl::Status Chgctype(absl::Span indices, + absl::Span xctype); + + absl::Status DelSetCols(absl::Span indices); + + absl::Status DelSetRows(absl::Span indices); + + absl::Status LpOpt(); + + absl::Status MipOpt(); + + absl::StatusOr GetParamNum(absl::string_view name_str); + + absl::StatusOr> GetLb(int begin, int end); + + absl::StatusOr> GetUb(int begin, int end); + + absl::Status SetTerminate(volatile int* terminate_p); + + absl::StatusOr< + std::tuple> + GetChannels(); + + absl::Status AddFuncDest(CPXCHANNELptr channel, void* handle, + void(CPXPUBLIC* msgfunction)(void*, const char*)); + + absl::Status DelFuncDest(CPXCHANNELptr channel, void* handle, + void(CPXPUBLIC* msgfunction)(void*, const char*)); + + absl::StatusOr Version(); + + private: + CPXENVptr cpx_env_; + CPXLPptr cpx_model_; + + explicit Cplex(CPXENVptr& env, CPXLPptr& model); + + static std::vector TransformCopyStringToCstr( + absl::Span string_span); +}; + +} // namespace operations_research::math_opt + +#endif // ORTOOLS_MATH_OPT_SOLVERS_CPLEX_G_CPLEX_H_ \ No newline at end of file diff --git a/ortools/math_opt/solvers/cplex_solver.cc b/ortools/math_opt/solvers/cplex_solver.cc new file mode 100644 index 00000000000..13957266aac --- /dev/null +++ b/ortools/math_opt/solvers/cplex_solver.cc @@ -0,0 +1,1961 @@ +// Copyright 2010-2026 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. + +#include "ortools/math_opt/solvers/cplex_solver.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/base/nullability.h" +#include "absl/cleanup/cleanup.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/memory/memory.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/escaping.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "google/protobuf/repeated_ptr_field.h" +#include "ortools/base/map_util.h" +#include "ortools/base/protoutil.h" +#include "ortools/base/status_macros.h" +#include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/inverted_bounds.h" +#include "ortools/math_opt/core/math_opt_proto_utils.h" +#include "ortools/math_opt/core/non_streamable_solver_init_arguments.h" +#include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/core/sorted.h" +#include "ortools/math_opt/core/sparse_vector_view.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" +#include "ortools/math_opt/model_update.pb.h" +#include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/solvers/cplex.pb.h" +#include "ortools/math_opt/solvers/cplex/g_cplex.h" +#include "ortools/math_opt/solvers/message_callback_data.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/math_opt/validators/callback_validator.h" +#include "ortools/port/proto_utils.h" +#include "ortools/util/solve_interrupter.h" + +namespace operations_research { +namespace math_opt { +absl::StatusOr ParseCplexVersion(absl::string_view version_str) { + CplexVersion version; + std::vector parts = absl::StrSplit(version_str, '.'); + + if (parts.size() < 3) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid CPLEX version string: ", version_str)); + } + + if (!absl::SimpleAtoi(parts[0], &version.major) || + !absl::SimpleAtoi(parts[1], &version.minor) || + !absl::SimpleAtoi(parts[2], &version.revision)) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid CPLEX version components: ", version_str)); + } + + if (parts.size() > 3) { + if (!absl::SimpleAtoi(parts[3], &version.subrevision)) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid CPLEX subrevision: ", version_str)); + } + } + + return version; +} + +bool CplexSupportsObjectiveLimit() { + auto cplex_or = Cplex::New("check_version"); + if (!cplex_or.ok()) { + // If we can't load CPLEX, we probably can't run tests either, but maybe we + // shouldn't crash here. However, if the test suite runs, it expects CPLEX + // to work. + LOG(WARNING) << "Failed to load CPLEX to check version: " + << cplex_or.status(); + return false; + } + auto version_str_or = cplex_or.value()->Version(); + if (!version_str_or.ok()) return false; + + auto version_or = ParseCplexVersion(*version_str_or); + if (!version_or.ok()) return false; + + return *version_or >= CplexVersion{21, 1, 0}; +} + +namespace { + +constexpr SupportedProblemStructures kCplexSupportedStructures = { + .integer_variables = SupportType::kSupported, + .multi_objectives = SupportType::kNotImplemented, + .quadratic_objectives = SupportType::kNotImplemented, + .quadratic_constraints = SupportType::kNotImplemented, + .second_order_cone_constraints = SupportType::kNotImplemented, + .sos1_constraints = SupportType::kNotImplemented, + .sos2_constraints = SupportType::kNotImplemented, + .indicator_constraints = SupportType::kNotImplemented}; + +template +static absl::StatusOr CheckCopySpan( + const absl::Span in, TCheckFn check_fn) { + TOutput out; + out.reserve(in.size()); + + for (const auto& item : in) { + RETURN_IF_ERROR(check_fn(item)); + out.push_back(item); + } + + return out; +} + +template +static void AddCplexParameterProto(CplexParametersProto& parameters, + absl::string_view name, T value) { + CplexParametersProto::Parameter* const parameter = + parameters.add_parameters(); + + auto set_fields = [&](auto* typed_param) { + typed_param->set_name(name); + typed_param->set_value(value); + }; + + if constexpr (std::is_same_v) + set_fields(parameter->mutable_parameter_double()); + else if constexpr (std::is_same_v) + set_fields(parameter->mutable_parameter_bool()); + else if constexpr (std::is_same_v) + set_fields(parameter->mutable_parameter_int32()); + else if constexpr (std::is_same_v) + set_fields(parameter->mutable_parameter_int64()); + else if constexpr (std::is_same_v) + set_fields(parameter->mutable_parameter_string()); +} + +// is_mip indicates if the problem has integer variables or constraints that +// would cause Cplex to treat the problem as a MIP, e.g. SOS, indicator. +absl::StatusOr MergeParameters( + const SolveParametersProto& solve_parameters, + const ModelSolveParametersProto& model_parameters, const bool is_mip, + const bool is_minimization, const bool supports_objective_limit) { + CplexParametersProto merged_parameters; + + // NOTE: CPXPARAM_ScreenOutput and CPXPARAM_ParamDisplay are intentionally + // NOT set here. They are output-routing concerns handled in Solve() before + // this parameter set is applied, so that console suppression is already in + // effect when CPLEX processes subsequent parameter changes. + + { + absl::Duration time_limit = absl::InfiniteDuration(); + if (solve_parameters.has_time_limit()) { + ASSIGN_OR_RETURN(time_limit, util_time::DecodeGoogleApiProto( + solve_parameters.time_limit())); + } + if (time_limit < absl::InfiniteDuration()) { + AddCplexParameterProto(merged_parameters, "CPXPARAM_TimeLimit", + absl::ToDoubleSeconds(time_limit)); + } + } + + if (solve_parameters.has_node_limit()) { + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Limits_Nodes", + solve_parameters.node_limit()); + } + + if (solve_parameters.has_threads()) { + AddCplexParameterProto(merged_parameters, "CPXPARAM_Threads", + solve_parameters.threads()); + } + + if (solve_parameters.has_absolute_gap_tolerance()) { + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Tolerances_AbsMIPGap", + solve_parameters.absolute_gap_tolerance()); + } + + if (solve_parameters.has_relative_gap_tolerance()) { + // CPLEX's MIPGap parameter range is [0, 1]. Values > 1.0 are semantically + // equivalent to 1.0 (accept any feasible solution), so we clamp to stay + // within CPLEX's accepted range. Returning invalid argument would be + // preferred but test-suite explicitly uses values > 1.0. + const double gap = std::min(solve_parameters.relative_gap_tolerance(), 1.0); + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Tolerances_MIPGap", gap); + } + + if (solve_parameters.has_cutoff_limit()) { + if (!is_mip) { + return absl::InvalidArgumentError( + "cutoff_limit is only supported for Cplex on MIP models"); + } + + if (is_minimization) { + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Tolerances_UpperCutoff", + solve_parameters.cutoff_limit()); + } else { + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Tolerances_LowerCutoff", + solve_parameters.cutoff_limit()); + } + } + + if (solve_parameters.has_objective_limit()) { + if (!supports_objective_limit) { + return absl::InvalidArgumentError( + "objective_limit is not supported for CPLEX"); + } + + const double limit = solve_parameters.objective_limit(); + if (is_mip) { + if (is_minimization) { + AddCplexParameterProto( + merged_parameters, "CPXPARAM_MIP_Limits_LowerObjStop", limit); + } else { + AddCplexParameterProto( + merged_parameters, "CPXPARAM_MIP_Limits_UpperObjStop", limit); + } + } else { + if (is_minimization) { + AddCplexParameterProto( + merged_parameters, "CPXPARAM_Simplex_Limits_LowerObj", limit); + } else { + AddCplexParameterProto( + merged_parameters, "CPXPARAM_Simplex_Limits_UpperObj", limit); + } + } + } + + if (solve_parameters.has_best_bound_limit()) { + return absl::InvalidArgumentError( + "best_bound_limit is currently not supported for CPLEX"); + } + + if (solve_parameters.has_solution_limit()) { + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Limits_Solutions", + solve_parameters.solution_limit()); + } + + if (solve_parameters.has_random_seed()) { + const int random_seed = + std::min(CPX_BIGINT, std::max(solve_parameters.random_seed(), 0)); + AddCplexParameterProto(merged_parameters, "CPXPARAM_RandomSeed", + random_seed); + } + + if (solve_parameters.has_solution_pool_size()) { + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Pool_Capacity", + solve_parameters.solution_pool_size()); + } + + if (solve_parameters.lp_algorithm() != LP_ALGORITHM_UNSPECIFIED) { + int cplex_lp_method = 0; + switch (solve_parameters.lp_algorithm()) { + case LP_ALGORITHM_PRIMAL_SIMPLEX: + cplex_lp_method = CPX_ALG_PRIMAL; + break; + case LP_ALGORITHM_DUAL_SIMPLEX: + cplex_lp_method = CPX_ALG_DUAL; + break; + case LP_ALGORITHM_BARRIER: + cplex_lp_method = CPX_ALG_BARRIER; + break; + case LP_ALGORITHM_FIRST_ORDER: + return absl::InvalidArgumentError( + "lp_algorithm FIRST_ORDER is not supported for cplex"); + default: + LOG(FATAL) << "LPAlgorithm: " + << ProtoEnumToString(solve_parameters.lp_algorithm()) + << " unknown, error setting Cplex parameters"; + } + + AddCplexParameterProto(merged_parameters, "CPXPARAM_LPMethod", + cplex_lp_method); + + if (is_mip) { + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Strategy_StartAlgorithm", + cplex_lp_method); + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Strategy_SubAlgorithm", + cplex_lp_method); + } + } + + if (solve_parameters.scaling() != EMPHASIS_UNSPECIFIED) { + switch (solve_parameters.scaling()) { + case EMPHASIS_OFF: + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Read_Scale", -1); + break; + case EMPHASIS_LOW: + case EMPHASIS_MEDIUM: + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Read_Scale", 0); + break; + case EMPHASIS_HIGH: + case EMPHASIS_VERY_HIGH: + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Read_Scale", 1); + break; + default: + LOG(FATAL) << "Scaling emphasis: " + << ProtoEnumToString(solve_parameters.scaling()) + << " unknown, error setting Cplex parameters"; + } + } + + // CPLEX does not offer a global parameter for all cuts in the C API + if (solve_parameters.cuts() != EMPHASIS_UNSPECIFIED) { + int cplex_cut_level = 0; // Default to Auto (0) + switch (solve_parameters.cuts()) { + case EMPHASIS_OFF: + cplex_cut_level = -1; // CPLEX Off + break; + case EMPHASIS_LOW: + case EMPHASIS_MEDIUM: + cplex_cut_level = 1; // CPLEX Moderate + break; + case EMPHASIS_HIGH: + cplex_cut_level = 2; // CPLEX Aggressive + break; + case EMPHASIS_VERY_HIGH: + cplex_cut_level = 3; // CPLEX Very Aggressive + break; + default: + LOG(FATAL) << "Cuts emphasis: " + << ProtoEnumToString(solve_parameters.cuts()) + << " unknown, error setting Cplex parameters"; + } + + // List of all CPLEX cut parameters to update + const std::vector cut_params = { + "CPXPARAM_MIP_Cuts_BQP", "CPXPARAM_MIP_Cuts_Cliques", + "CPXPARAM_MIP_Cuts_Covers", "CPXPARAM_MIP_Cuts_Disjunctive", + "CPXPARAM_MIP_Cuts_FlowCovers", "CPXPARAM_MIP_Cuts_PathCut", + "CPXPARAM_MIP_Cuts_Gomory", "CPXPARAM_MIP_Cuts_GUBCovers", + "CPXPARAM_MIP_Cuts_Implied", "CPXPARAM_MIP_Cuts_LocalImplied", + "CPXPARAM_MIP_Cuts_LiftProj", "CPXPARAM_MIP_Cuts_MIRCut", + "CPXPARAM_MIP_Cuts_MCFCut", "CPXPARAM_MIP_Cuts_Nodecuts", + "CPXPARAM_MIP_Cuts_RLT", "CPXPARAM_MIP_Cuts_ZeroHalfCut"}; + + for (absl::string_view param_name : cut_params) { + AddCplexParameterProto(merged_parameters, param_name, + cplex_cut_level); + } + } + + // CPLEX uses an effort multiplier + // Important: "The behavior of CPLEX is undefined if both heuristic effort and + // heuristic frequency are set to non-default values." + if (solve_parameters.heuristics() != EMPHASIS_UNSPECIFIED) { + switch (solve_parameters.heuristics()) { + case EMPHASIS_OFF: + AddCplexParameterProto( + merged_parameters, "CPXPARAM_MIP_Strategy_HeuristicEffort", 0.0); + break; + case EMPHASIS_LOW: + AddCplexParameterProto( + merged_parameters, "CPXPARAM_MIP_Strategy_HeuristicEffort", 0.5); + break; + case EMPHASIS_MEDIUM: + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Strategy_HeuristicEffort", + 1.0); // default + break; + case EMPHASIS_HIGH: + AddCplexParameterProto( + merged_parameters, "CPXPARAM_MIP_Strategy_HeuristicEffort", 2.0); + break; + case EMPHASIS_VERY_HIGH: + AddCplexParameterProto( + merged_parameters, "CPXPARAM_MIP_Strategy_HeuristicEffort", 4.0); + break; + default: + LOG(FATAL) << "Heuristics emphasis: " + << ProtoEnumToString(solve_parameters.heuristics()) + << " unknown, error setting CPLEX parameters"; + } + } + + // CPLEX does not offer a global parameter in the C API + if (solve_parameters.presolve() != EMPHASIS_UNSPECIFIED) { + int cplex_presolve_level = 0; + switch (solve_parameters.presolve()) { + case EMPHASIS_OFF: + cplex_presolve_level = -1; + break; + case EMPHASIS_LOW: + cplex_presolve_level = 0; + break; + case EMPHASIS_MEDIUM: + cplex_presolve_level = 1; + break; + case EMPHASIS_HIGH: + cplex_presolve_level = 2; + break; + case EMPHASIS_VERY_HIGH: + cplex_presolve_level = 3; + break; + default: + LOG(FATAL) << "Presolve emphasis: " + << ProtoEnumToString(solve_parameters.presolve()) + << " unknown, error setting CPLEX parameters"; + } + + switch (cplex_presolve_level) { + case -1: + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Preprocessing_Presolve", + false); // off + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Preprocessing_Aggregator", + 0); // off + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Strategy_Probe", + -1); // off + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Preprocessing_RepeatPresolve", + 0); // off + break; + case 0: + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Preprocessing_Presolve", + true); // on (default) + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Strategy_Probe", + -1); // off + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Preprocessing_RepeatPresolve", + 0); // off + break; + case 1: + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Preprocessing_Presolve", + true); // on (default) + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Strategy_Probe", + 1); // moderate + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Preprocessing_RepeatPresolve", + 1); // represolve wo. cuts + break; + case 2: + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Preprocessing_Presolve", + true); // on (default) + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Strategy_Probe", + 2); // aggressive + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Preprocessing_RepeatPresolve", + 2); // represolve w. cuts + break; + case 3: + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Preprocessing_Presolve", + true); // on (default) + AddCplexParameterProto(merged_parameters, + "CPXPARAM_MIP_Strategy_Probe", + 3); // very aggressive + AddCplexParameterProto( + merged_parameters, "CPXPARAM_Preprocessing_RepeatPresolve", + 3); // represolve w. cuts and allow new root cuts + break; + default: + LOG(FATAL) << "Presolve emphasis: " + << ProtoEnumToString(solve_parameters.presolve()) + << " unknown, error setting CPLEX parameters"; + } + } + + if (solve_parameters.has_iteration_limit()) { + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Simplex_Limits_Iterations", + solve_parameters.iteration_limit()); + + AddCplexParameterProto(merged_parameters, + "CPXPARAM_Barrier_Limits_Iteration", + solve_parameters.iteration_limit()); + } + + for (const CplexParametersProto::Parameter& parameter : + solve_parameters.cplex().parameters()) { + *merged_parameters.add_parameters() = parameter; + } + + return merged_parameters; +} + +// Be safe and introduce a char limit. +constexpr std::size_t kMaxNameSize = 255; + +// Returns a string of at most kMaxNameSize max size. +std::string TruncateName(const std::string_view original_name) { + return std::string( + original_name.substr(0, std::min(kMaxNameSize, original_name.size()))); +} + +// Truncate the names of variables and constraints. +std::vector TruncateNames( + const google::protobuf::RepeatedPtrField& original_names) { + std::vector result; + result.reserve(original_names.size()); + for (const std::string& original_name : original_names) { + result.push_back(TruncateName(original_name)); + } + return result; +} + +absl::Status SafeCplexDouble(const double d) { + if (std::isfinite(d) && std::abs(d) >= CPX_INFBOUND) { + return util::InvalidArgumentErrorBuilder() + << "finite value: " << d << " will be treated as infinite by CPLEX"; + } + return absl::OkStatus(); +} + +constexpr int kDeletedIndex = -1; +constexpr int kUnsetIndex = -2; +// Returns a vector of length `size_before_delete` that logically provides a +// mapping from the starting contiguous range [0, ..., size_before_delete) to +// a potentially smaller range [0, ..., num_remaining_elems) after deleting +// each element in `deletes` and shifting the remaining elements such that they +// are contiguous starting at 0. The elements in the output point to the new +// shifted index, or `kDeletedIndex` if the starting index was deleted. +std::vector IndexUpdateMap(const int size_before_delete, + absl::Span deletes) { + std::vector result(size_before_delete, kUnsetIndex); + for (const int del : deletes) { + result[del] = kDeletedIndex; + } + int next_free = 0; + for (int& r : result) { + if (r != kDeletedIndex) { + r = next_free; + ++next_free; + } + CHECK_GT(r, kUnsetIndex); + } + return result; +} + +absl::StatusOr> CplexFromInitArgs( + const SolverInterface::InitArgs& init_args) { + if (init_args.non_streamable != nullptr) { + return absl::InvalidArgumentError( + "CPLEX support in MathOpt does not currently accept non-streamable " + "init arguments (e.g. pre-existing CPXENVptr)."); + } + return Cplex::New("cplex_model"); +} + +void CPXPUBLIC MessageCallbackImpl(void* handle, const char* message) { + auto* const buffered_message_cb = + static_cast(handle); + if (message != nullptr && buffered_message_cb != nullptr) { + buffered_message_cb->OnMessage(message); + } +} + +} // namespace + +CplexSolver::CplexSolver(std::unique_ptr g_cplex) + : cplex_(std::move(g_cplex)) {} + +CplexSolver::CplexModelElements +CplexSolver::LinearConstraintData::DependentElements() const { + CplexModelElements elements; + CHECK_NE(constraint_index, kUnspecifiedConstraint); + elements.linear_constraints.push_back(constraint_index); + return elements; +} + +absl::StatusOr CplexSolver::ConvertTerminationReason( + const int cplex_status, const bool had_cutoff, + const bool had_iteration_limit, const bool had_objective_limit, + const SolutionClaims solution_claims, const double best_primal_bound, + const double best_dual_bound) { + ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize()); + + switch (cplex_status) { + case 1: // CPX_STAT_OPTIMAL + return OptimalTerminationProto(best_primal_bound, best_dual_bound); + case 2: // CPX_STAT_UNBOUNDED + if (solution_claims.primal_feasible_solution_exists) { + return UnboundedTerminationProto(is_maximize); + } + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE, + "Cplex status CPX_STAT_UNBOUNDED"); + case 3: // CPX_STAT_INFEASIBLE + if (had_cutoff) { + // CPLEX has no dedicated cutoff status code (unlike Gurobi's + // GRB_CUTOFF). When a cutoff is active and CPLEX returns infeasible, + // we cannot distinguish "truly infeasible" from "no solution better + // than cutoff." We assume cutoff termination, matching the gSCIP + // approach for API conformance. + return CutoffTerminationProto(is_maximize, + "Cplex status CPX_STAT_INFEASIBLE"); + } + return InfeasibleTerminationProto( + is_maximize, solution_claims.dual_feasible_solution_exists + ? FEASIBILITY_STATUS_FEASIBLE + : FEASIBILITY_STATUS_UNDETERMINED); + case 4: // CPX_STAT_INForUNBD + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED, + "Cplex status CPX_STAT_INForUNBD"); + case 5: // CPX_STAT_OPTIMAL_INFEAS + return TerminateForReason(is_maximize, TERMINATION_REASON_IMPRECISE); + case 6: // CPX_STAT_NUM_BEST + return TerminateForReason(is_maximize, TERMINATION_REASON_IMPRECISE); + case 10: // CPX_STAT_ABORT_IT_LIM + return LimitTerminationProto( + LIMIT_ITERATION, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 11: // CPX_STAT_ABORT_TIME_LIM + return LimitTerminationProto( + LIMIT_TIME, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 12: // CPX_STAT_ABORT_OBJ_LIM + return LimitTerminationProto( + LIMIT_OBJECTIVE, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 13: // CPX_STAT_ABORT_USER + return LimitTerminationProto( + LIMIT_INTERRUPTED, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + // IMPORTANT: Assumption -> never happening as we don't use it's source + // "CPXfeasopt" + case 14: // CPX_STAT_FEASIBLE_RELAXED_SUM + case 15: // CPX_STAT_OPTIMAL_RELAXED_SUM + case 16: // CPX_STAT_FEASIBLE_RELAXED_INF + case 17: // CPX_STAT_OPTIMAL_RELAXED_INF + case 18: // CPX_STAT_FEASIBLE_RELAXED_QUAD + case 19: // CPX_STAT_OPTIMAL_RELAXED_QUAD + return TerminateForReason(is_maximize, TERMINATION_REASON_OTHER_ERROR); + case 20: // CPX_STAT_OPTIMAL_FACE_UNBOUNDED + // The optimal objective is attained but the solution set is unbounded + // (multiple optimal solutions exist along a ray). This IS optimal. + return OptimalTerminationProto(best_primal_bound, best_dual_bound); + case 21: // CPX_STAT_ABORT_PRIM_OBJ_LIM + return LimitTerminationProto( + LIMIT_OBJECTIVE, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 22: // CPX_STAT_ABORT_DUAL_OBJ_LIM + return LimitTerminationProto( + LIMIT_OBJECTIVE, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 23: // CPX_STAT_FEASIBLE + // A feasible solution was found but optimality was not proven (e.g., + // deterministic time limit hit after Phase I). Not an error. + return FeasibleTerminationProto(is_maximize, LIMIT_UNDETERMINED, + best_primal_bound, best_dual_bound); + // IMPORTANT: Assumption -> never happening as we don't use it's source + // "CPXfeasopt" + case 24: // CPX_STAT_FIRSTORDER + return TerminateForReason(is_maximize, TERMINATION_REASON_OTHER_ERROR); + case 25: // CPX_STAT_ABORT_DETTIME_LIM + return LimitTerminationProto( + LIMIT_TIME, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + // IMPORTANT: Assumption -> never happening as we don't use it's source + // "conflict refiner" + case 30: // CPX_STAT_CONFLICT_FEASIBLE + case 31: // CPX_STAT_CONFLICT_MINIMAL + case 32: // CPX_STAT_CONFLICT_ABORT_CONTRADICTION + case 33: // CPX_STAT_CONFLICT_ABORT_TIME_LIM + case 34: // CPX_STAT_CONFLICT_ABORT_IT_LIM + case 35: // CPX_STAT_CONFLICT_ABORT_NODE_LIM + case 36: // CPX_STAT_CONFLICT_ABORT_OBJ_LIM + case 37: // CPX_STAT_CONFLICT_ABORT_MEM_LIM + case 38: // CPX_STAT_CONFLICT_ABORT_USER + case 39: // CPX_STAT_CONFLICT_ABORT_DETTIME_LIM + // IMPORTANT: Assumption -> never happening as we don't use it's source + // "Benders decomposition" + case 40: // CPX_STAT_BENDERS_MASTER_UNBOUNDED + case 41: // CPX_STAT_BENDERS_NUM_BEST + // IMPORTANT: Assumption -> never happening as we don't use it's source + // "Multi-objective optimization" + case 301: // CPX_STAT_MULTIOBJ_OPTIMAL + case 302: // CPX_STAT_MULTIOBJ_INFEASIBLE + case 303: // CPX_STAT_MULTIOBJ_INForUNBD + case 304: // CPX_STAT_MULTIOBJ_UNBOUNDED + case 305: // CPX_STAT_MULTIOBJ_NON_OPTIMAL + case 306: // CPX_STAT_MULTIOBJ_STOPPED + return TerminateForReason(is_maximize, TERMINATION_REASON_OTHER_ERROR); + + // MIP Status Codes + case 101: // CPXMIP_OPTIMAL + case 102: // CPXMIP_OPTIMAL_TOL + return OptimalTerminationProto(best_primal_bound, best_dual_bound); + case 103: // CPXMIP_INFEASIBLE + if (had_cutoff) { + // Same cutoff ambiguity as CPX_STAT_INFEASIBLE (case 3) — see comment + // there. CPLEX doesn't distinguish "truly infeasible" from "no solution + // better than cutoff." + return CutoffTerminationProto(is_maximize, + "Cplex status CPXMIP_INFEASIBLE"); + } + return InfeasibleTerminationProto( + is_maximize, solution_claims.dual_feasible_solution_exists + ? FEASIBILITY_STATUS_FEASIBLE + : FEASIBILITY_STATUS_UNDETERMINED); + case 104: // CPXMIP_SOL_LIM + return LimitTerminationProto( + LIMIT_SOLUTION, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 105: // CPXMIP_NODE_LIM_FEAS + case 106: // CPXMIP_NODE_LIM_INFEAS + return LimitTerminationProto( + LIMIT_NODE, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 107: // CPXMIP_TIME_LIM_FEAS + case 108: // CPXMIP_TIME_LIM_INFEAS + return LimitTerminationProto( + LIMIT_TIME, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 110: // CPXMIP_FAIL_INFEAS + if (had_iteration_limit) { + // same as case 3: user-limit? high chance (but no guarantee) this + // status is a result of that (e.g. limit hit in root relax) + return LimitTerminationProto( + LIMIT_ITERATION, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists, + "Terminated because of an error; no integer solution. As " + "iteration_limit was set, it's probably the reason"); + } + // Numerical error with no feasible solution found. This is NOT a proof + // of infeasibility — the solver failed, it did not conclude infeasible. + return TerminateForReason(is_maximize, + TERMINATION_REASON_NUMERICAL_ERROR); + case 131: // CPXMIP_DETTIME_LIM_FEAS + case 132: // CPXMIP_DETTIME_LIM_INFEAS + return LimitTerminationProto( + LIMIT_TIME, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 111: // CPXMIP_MEM_LIM_FEAS + case 112: // CPXMIP_MEM_LIM_INFEAS + case 116: // CPXMIP_FAIL_FEAS_NO_TREE + case 117: // CPXMIP_FAIL_INFEAS_NO_TREE + return LimitTerminationProto( + LIMIT_MEMORY, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 113: // CPXMIP_ABORT_FEAS + case 114: // CPXMIP_ABORT_INFEAS + // CPLEX has no dedicated status code for objective limit termination in + // MIP. When the objective limit triggers, CPLEX returns ABORT_FEAS/INFEAS + // — the same codes used for user interrupts. We disambiguate by checking + // whether an objective limit was set. + return LimitTerminationProto( + had_objective_limit ? LIMIT_OBJECTIVE : LIMIT_INTERRUPTED, + best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 115: // CPXMIP_OPTIMAL_INFEAS + return TerminateForReason(is_maximize, TERMINATION_REASON_IMPRECISE); + case 118: // CPXMIP_UNBOUNDED + if (solution_claims.primal_feasible_solution_exists) { + return UnboundedTerminationProto(is_maximize); + } + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE, + "Cplex status CPXMIP_UNBOUNDED"); + case 119: // CPXMIP_INForUNBD + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_UNDETERMINED, + "Cplex status CPXMIP_INForUNBD"); + case 109: // CPXMIP_FAIL_FEAS + // Numerical error but a feasible incumbent exists. Analogous to case 110 + // but with a solution — preserve it via IMPRECISE rather than INFEASIBLE. + return TerminateForReason(is_maximize, TERMINATION_REASON_IMPRECISE); + case 120: // CPXMIP_FEASIBLE_RELAXED_SUM + case 121: // CPXMIP_OPTIMAL_RELAXED_SUM + case 122: // CPXMIP_FEASIBLE_RELAXED_INF + case 123: // CPXMIP_OPTIMAL_RELAXED_INF + case 124: // CPXMIP_FEASIBLE_RELAXED_QUAD + case 125: // CPXMIP_OPTIMAL_RELAXED_QUAD + case 126: // CPXMIP_ABORT_RELAXED + // FeasOpt results — the solver relaxed the model to find feasibility. + // MathOpt does not invoke FeasOpt, so these should not arise in normal + // use. Map to OTHER_ERROR to surface the unexpected state. + return TerminateForReason(is_maximize, TERMINATION_REASON_OTHER_ERROR); + case 127: // CPXMIP_FEASIBLE + // CPLEX found a feasible solution but did not prove optimality. + return FeasibleTerminationProto(is_maximize, LIMIT_UNDETERMINED, + best_primal_bound, best_dual_bound); + case 128: // CPXMIP_POPULATESOL_LIM + return LimitTerminationProto( + LIMIT_SOLUTION, best_primal_bound, best_dual_bound, + solution_claims.dual_feasible_solution_exists); + case 129: // CPXMIP_OPTIMAL_POPULATED + case 130: // CPXMIP_OPTIMAL_POPULATED_TOL + return OptimalTerminationProto(best_primal_bound, best_dual_bound); + case 133: // CPXMIP_ABORT_RELAXATION_UNBOUNDED + return InfeasibleOrUnboundedTerminationProto( + is_maximize, + /*dual_feasibility_status=*/FEASIBILITY_STATUS_INFEASIBLE, + "Cplex status CPXMIP_ABORT_RELAXATION_UNBOUNDED"); + + default: + return absl::InternalError(absl::StrCat( + "Missing Cplex optimization status code case: ", cplex_status)); + } +} + +absl::StatusOr CplexSolver::IsMaximize() const { + CHECK(cplex_ != nullptr); + + ASSIGN_OR_RETURN(const int obj_sense, cplex_->GetObjSen()); + return obj_sense == CPX_MAX; +} + +absl::StatusOr CplexSolver::IsMIP() const { + CHECK(cplex_ != nullptr); + + ASSIGN_OR_RETURN(auto problem_type, cplex_->GetProbType()); + return problem_type == CPXPROB_MILP || problem_type == CPXPROB_FIXEDMILP; +} + +absl::StatusOr CplexSolver::Version() const { + CHECK(cplex_ != nullptr); + + return cplex_->Version(); +} + +template +void CplexSolver::CplexVectorToSparseDoubleVector( + const absl::Span cplex_values, const T& map, + SparseDoubleVectorProto& result, + const SparseVectorFilterProto& filter) const { + SparseVectorFilterPredicate predicate(filter); + for (const auto& [id, cplex_data] : map) { + const double value = cplex_values[get_model_index(cplex_data)]; + if (predicate.AcceptsAndUpdate(id, value)) { + result.add_ids(id); + result.add_values(value); + } + } +} + +absl::StatusOr CplexSolver::ExtractSolveResultProto( + const absl::Time start, const ModelSolveParametersProto& model_parameters, + const bool had_cutoff, const bool had_iteration_limit, + const bool had_objective_limit) { + SolveResultProto result; + + ASSIGN_OR_RETURN(auto cplex_stat, cplex_->GetStat()); + + SolutionClaims solution_claims; + ASSIGN_OR_RETURN(SolutionsAndClaims solution_and_claims, + GetSolutions(model_parameters)); + + ASSIGN_OR_RETURN(const double best_primal_bound, + GetBestPrimalBound(solution_and_claims.solutions)); + ASSIGN_OR_RETURN(const double best_dual_bound, + GetBestDualBound(solution_and_claims.solutions)); + solution_claims = solution_and_claims.solution_claims; + + for (SolutionProto& solution : solution_and_claims.solutions) { + *result.add_solutions() = std::move(solution); + } + + ASSIGN_OR_RETURN( + *result.mutable_termination(), + ConvertTerminationReason(cplex_stat, had_cutoff, had_iteration_limit, + had_objective_limit, solution_claims, + best_primal_bound, best_dual_bound)); + + ASSIGN_OR_RETURN(*result.mutable_solve_stats(), GetSolveStats(start)); + return result; +} + +absl::StatusOr CplexSolver::GetSolutions( + const ModelSolveParametersProto& model_parameters) { + ASSIGN_OR_RETURN(const bool is_mip, IsMIP()); + + if (is_mip) { + return GetMipSolutions(model_parameters); + } else { + return GetLpSolution(model_parameters); + } +} + +absl::StatusOr CplexSolver::GetSolveStats( + const absl::Time start) const { + SolveStatsProto solve_stats; + + CHECK_OK(util_time::EncodeGoogleApiProto(absl::Now() - start, + solve_stats.mutable_solve_time())); + + ASSIGN_OR_RETURN(auto problem_type, cplex_->GetProbType()); + + int simplex_iters = 0; + if (problem_type == CPXPROB_MILP) { + ASSIGN_OR_RETURN(simplex_iters, cplex_->GetMipItCnt()); + } else { + ASSIGN_OR_RETURN(simplex_iters, cplex_->GetItCnt()); + } + solve_stats.set_simplex_iterations(simplex_iters); + + ASSIGN_OR_RETURN(int barrier_iters, cplex_->GetBarItCnt()); + solve_stats.set_barrier_iterations(barrier_iters); + + ASSIGN_OR_RETURN(int node_count, cplex_->GetNodeCnt()); + solve_stats.set_node_count(node_count); + + return solve_stats; +} + +absl::StatusOr CplexSolver::GetMipSolutions( + const ModelSolveParametersProto& model_parameters) { + // Assumption: we did not touch CPX_PARAM_SOLNPOOLCAPACITY + ASSIGN_OR_RETURN(int num_solutions, cplex_->GetSolNPoolNumSolns()); + std::vector solutions; + solutions.reserve(num_solutions); + + for (int i = 0; i < num_solutions; ++i) { + PrimalSolutionProto primal_solution; + ASSIGN_OR_RETURN(const double sol_val, cplex_->GetSolnPoolObjVal(i)); + primal_solution.set_objective_value(sol_val); + + // Only the incumbent (solution 0) is vouched for by the termination + // status. Pool solutions passed feasibility checks when found, but no + // solver-level claim covers them — mark as UNDETERMINED (matches Gurobi). + primal_solution.set_feasibility_status( + i == 0 ? SOLUTION_STATUS_FEASIBLE : SOLUTION_STATUS_UNDETERMINED); + + ASSIGN_OR_RETURN(const std::vector cpx_var_values, + cplex_->GetSolnPoolX(i)); + CplexVectorToSparseDoubleVector(cpx_var_values, variables_map_, + *primal_solution.mutable_variable_values(), + model_parameters.variable_values_filter()); + + *solutions.emplace_back(SolutionProto()).mutable_primal_solution() = + std::move(primal_solution); + } + + ASSIGN_OR_RETURN(const int cpx_stat, cplex_->GetStat()); + + // Set solution claims + ASSIGN_OR_RETURN(const double best_dual_bound, GetCplexBestDualBound()); + + const SolutionClaims solution_claims = { + .primal_feasible_solution_exists = num_solutions > 0, + .dual_feasible_solution_exists = std::isfinite(best_dual_bound) || + cpx_stat == CPX_STAT_INFEASIBLE || + cpx_stat == CPXMIP_INFEASIBLE}; + + // Check consistency of solutions, bounds and statuses. + if ((cpx_stat == CPXMIP_OPTIMAL || cpx_stat == CPXMIP_OPTIMAL_TOL) && + num_solutions == 0) { + return absl::InternalError( + "CPX MIP Status is optimal, but solution pool is empty."); + } + + return SolutionsAndClaims{.solutions = std::move(solutions), + .solution_claims = solution_claims}; +} + +absl::StatusOr> +CplexSolver::GetConvexPrimalSolutionIfAvailable( + const ModelSolveParametersProto& model_parameters) { + ASSIGN_OR_RETURN(auto soln_info, cplex_->SolnInfo()); + + // Check solntype (index 1) to see if any solution exists (even infeasible) + if (std::get<1>(soln_info) == CPX_NO_SOLN) { + return SolutionAndClaim{ + .solution = std::nullopt, .feasible_solution_exists = false}; + } + + ASSIGN_OR_RETURN(double sol_val, cplex_->GetObjVal()); + + PrimalSolutionProto primal_solution; + primal_solution.set_objective_value(sol_val); + + // pfeasind (index 2): CPLEX's primal feasibility indicator from CPXsolninfo. + // When a solution exists (solntype != CPX_NO_SOLN, checked above), pfeasind + // is always computed: 1 = primal feasible, 0 = not primal feasible. + if (std::get<2>(soln_info)) { + primal_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + } else { + primal_solution.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE); + } + + ASSIGN_OR_RETURN(auto sol_x, cplex_->GetX()); + CplexVectorToSparseDoubleVector(sol_x, variables_map_, + *primal_solution.mutable_variable_values(), + model_parameters.variable_values_filter()); + + const bool primal_feasible_solution_exists = + (primal_solution.feasibility_status() == SOLUTION_STATUS_FEASIBLE); + return SolutionAndClaim{ + .solution = std::move(primal_solution), + .feasible_solution_exists = primal_feasible_solution_exists}; +} + +// We follow the gurobi wrapper here instead of trusting +// CPXgetobjval/CPXgetbestobjval +absl::StatusOr CplexSolver::GetBestPrimalBound( + absl::Span solutions) const { + ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize()); + double best_objective_value = is_maximize ? -kInf : kInf; + for (const SolutionProto& solution : solutions) { + if (solution.has_primal_solution() && + solution.primal_solution().feasibility_status() == + SOLUTION_STATUS_FEASIBLE) { + const double sol_val = solution.primal_solution().objective_value(); + best_objective_value = is_maximize + ? std::max(best_objective_value, sol_val) + : std::min(best_objective_value, sol_val); + } + } + return best_objective_value; +} + +absl::StatusOr CplexSolver::GetBestDualBound( + absl::Span solutions) const { + ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize()); + ASSIGN_OR_RETURN(double best_dual_bound, GetCplexBestDualBound()); + for (const SolutionProto& solution : solutions) { + if (solution.has_dual_solution() && + solution.dual_solution().feasibility_status() == + SOLUTION_STATUS_FEASIBLE && + solution.dual_solution().has_objective_value()) { + const double sol_val = solution.dual_solution().objective_value(); + best_dual_bound = is_maximize ? std::min(best_dual_bound, sol_val) + : std::max(best_dual_bound, sol_val); + } + } + return best_dual_bound; +} + +absl::StatusOr CplexSolver::GetCplexBestDualBound() const { + ASSIGN_OR_RETURN(const bool is_mip, IsMIP()); + if (is_mip) { + ASSIGN_OR_RETURN(auto best_obj_val, cplex_->GetBestObjVal()); + if (std::abs(best_obj_val) < CPX_INFBOUND) return best_obj_val; + } + + ASSIGN_OR_RETURN(const bool is_maximize, IsMaximize()); + return is_maximize ? kInf : -kInf; +} + +absl::StatusOr CplexSolver::GetLpSolution( + const ModelSolveParametersProto& model_parameters) { + ASSIGN_OR_RETURN(auto primal_solution_and_claim, + GetConvexPrimalSolutionIfAvailable(model_parameters)); + ASSIGN_OR_RETURN(auto dual_solution_and_claim, + GetConvexDualSolutionIfAvailable(model_parameters)); + + const SolutionClaims solution_claims = { + .primal_feasible_solution_exists = + primal_solution_and_claim.feasible_solution_exists, + .dual_feasible_solution_exists = + dual_solution_and_claim.feasible_solution_exists}; + + if (!primal_solution_and_claim.solution.has_value() && + !dual_solution_and_claim.solution.has_value()) { + return SolutionsAndClaims{.solution_claims = solution_claims}; + } + SolutionsAndClaims solution_and_claims{.solution_claims = solution_claims}; + SolutionProto& solution = + solution_and_claims.solutions.emplace_back(SolutionProto()); + if (primal_solution_and_claim.solution.has_value()) { + *solution.mutable_primal_solution() = + std::move(*primal_solution_and_claim.solution); + } + if (dual_solution_and_claim.solution.has_value()) { + *solution.mutable_dual_solution() = + std::move(*dual_solution_and_claim.solution); + } + return solution_and_claims; +} + +absl::StatusOr> +CplexSolver::GetConvexDualSolutionIfAvailable( + const ModelSolveParametersProto& model_parameters) { + ASSIGN_OR_RETURN(auto soln_info, cplex_->SolnInfo()); + + // Check solntype (index 1) to see if any solution exists + // CPX_PRIMAL_SOLN (3) does not contain duals + if (std::get<1>(soln_info) == CPX_NO_SOLN || + std::get<1>(soln_info) == CPX_PRIMAL_SOLN) { + return SolutionAndClaim{ + .solution = std::nullopt, .feasible_solution_exists = false}; + } + + ASSIGN_OR_RETURN(const int cpx_stat, cplex_->GetStat()); + + DualSolutionProto dual_solution; + bool dual_feasible_solution_exists = false; + + // Only set dual objective at optimality (strong duality: primal = dual). + // At non-optimal terminations (e.g., iteration limit), the primal objective + // from CPXgetobjval is not a valid dual objective value. + if (cpx_stat == CPX_STAT_OPTIMAL || cpx_stat == CPX_STAT_OPTIMAL_INFEAS) { + ASSIGN_OR_RETURN(double sol_val, cplex_->GetObjVal()); + dual_solution.set_objective_value(sol_val); + } + + ASSIGN_OR_RETURN(std::vector sol_dual, cplex_->GetPi()); + CplexVectorToSparseDoubleVector(sol_dual, linear_constraints_map_, + *dual_solution.mutable_dual_values(), + model_parameters.dual_values_filter()); + + ASSIGN_OR_RETURN(std::vector sol_reduced, cplex_->GetDj()); + CplexVectorToSparseDoubleVector(sol_reduced, variables_map_, + *dual_solution.mutable_reduced_costs(), + model_parameters.reduced_costs_filter()); + + // Check dfeasind (index 3) for feasibility + if (std::get<3>(soln_info)) { + dual_solution.set_feasibility_status(SOLUTION_STATUS_FEASIBLE); + dual_feasible_solution_exists = true; + } else { + dual_solution.set_feasibility_status(SOLUTION_STATUS_INFEASIBLE); + } + + return SolutionAndClaim{ + .solution = std::move(dual_solution), + .feasible_solution_exists = dual_feasible_solution_exists}; +} + +absl::Status CplexSolver::SetParameters( + const SolveParametersProto& parameters, + const ModelSolveParametersProto& model_parameters) { + ASSIGN_OR_RETURN(const bool is_mip, IsMIP()); + ASSIGN_OR_RETURN(const bool is_maximization, IsMaximize()); + + // Parameters vs. cplex versions + ASSIGN_OR_RETURN(const std::string cplex_version, Version()); + ASSIGN_OR_RETURN(const CplexVersion version_loaded, + ParseCplexVersion(cplex_version)); + const bool supports_objective_limit = + version_loaded >= CplexVersion{21, 1, 0}; // modern versions only! + + ASSIGN_OR_RETURN(const CplexParametersProto cplex_parameters, + MergeParameters(parameters, model_parameters, is_mip, + !is_maximization, supports_objective_limit)); + + std::vector parameter_errors; + for (const CplexParametersProto::Parameter& parameter : + cplex_parameters.parameters()) { + absl::Status param_status; + + auto FnGetParamNameOkayOrAddToErrors = + [&cplex_ = cplex_, + ¶meter_errors](const auto& name) -> absl::StatusOr { + const auto param_id_or_error = cplex_->GetParamNum(name); + if (!param_id_or_error.ok()) { + parameter_errors.emplace_back(param_id_or_error.status().message()); + return param_id_or_error.status(); + } + return param_id_or_error; + }; + + auto FnSetParamOkayOrAddToErrors = [&cplex_ = cplex_, ¶meter_errors]( + int id, const auto& value, + auto&& setter) -> void { + const auto status = ((*cplex_).*setter)(id, value); + if (!status.ok()) parameter_errors.emplace_back(status.message()); + }; + + // --- + + if (parameter.has_parameter_double()) { + const auto maybe_param_id = + FnGetParamNameOkayOrAddToErrors(parameter.parameter_double().name()); + if (maybe_param_id.ok()) + FnSetParamOkayOrAddToErrors(maybe_param_id.value(), + parameter.parameter_double().value(), + &Cplex::SetParamDouble); + } else if (parameter.has_parameter_bool()) { + const auto maybe_param_id = + FnGetParamNameOkayOrAddToErrors(parameter.parameter_bool().name()); + if (maybe_param_id.ok()) + FnSetParamOkayOrAddToErrors(maybe_param_id.value(), + parameter.parameter_bool().value(), + &Cplex::SetParamBool); + } else if (parameter.has_parameter_int32()) { + const auto maybe_param_id = + FnGetParamNameOkayOrAddToErrors(parameter.parameter_int32().name()); + if (maybe_param_id.ok()) + FnSetParamOkayOrAddToErrors(maybe_param_id.value(), + parameter.parameter_int32().value(), + &Cplex::SetParamInt32); + } else if (parameter.has_parameter_int64()) { + const auto maybe_param_id = + FnGetParamNameOkayOrAddToErrors(parameter.parameter_int64().name()); + if (maybe_param_id.ok()) + FnSetParamOkayOrAddToErrors(maybe_param_id.value(), + parameter.parameter_int64().value(), + &Cplex::SetParamInt64); + } else if (parameter.has_parameter_string()) { + const auto maybe_param_id = + FnGetParamNameOkayOrAddToErrors(parameter.parameter_string().name()); + if (maybe_param_id.ok()) + FnSetParamOkayOrAddToErrors(maybe_param_id.value(), + parameter.parameter_string().value(), + &Cplex::SetParamString); + } + } + if (!parameter_errors.empty()) { + return absl::InvalidArgumentError(absl::StrJoin(parameter_errors, "; ")); + } + return absl::OkStatus(); +} + +absl::Status CplexSolver::AddNewVariables(const VariablesProto& new_variables) { + VLOG(2) << "CplexSolver::AddNewVariables"; + + const int num_new_variables = new_variables.lower_bounds().size(); + + std::vector variable_types(num_new_variables, 'C'); + bool is_mip = false; + for (int i = 0; i < num_new_variables; ++i) { + const VariableId id = new_variables.ids(i); + gtl::InsertOrDie(&variables_map_, id, i + num_cplex_variables_); + + const bool is_integer = new_variables.integers(i); + if (is_integer) { + variable_types[i] = 'I'; + is_mip = true; + } + } + + ASSIGN_OR_RETURN( + auto checked_lbs, + CheckCopySpan>( + absl::MakeConstSpan(new_variables.lower_bounds()), SafeCplexDouble)); + ASSIGN_OR_RETURN( + auto checked_ubs, + CheckCopySpan>( + absl::MakeConstSpan(new_variables.upper_bounds()), SafeCplexDouble)); + + const std::vector names_maybe_truncated = + TruncateNames(new_variables.names()); + + // Cplex interprets anything other than nullptr as MIP + absl::Span variable_types_span_to_pass_on = variable_types; + if (!is_mip) variable_types_span_to_pass_on = absl::Span{}; + + RETURN_IF_ERROR(cplex_->NewCols(checked_lbs, checked_ubs, + variable_types_span_to_pass_on, + names_maybe_truncated)); + + num_cplex_variables_ += num_new_variables; + + return absl::OkStatus(); +} + +absl::Status CplexSolver::AddSingleObjective(const ObjectiveProto& objective) { + VLOG(2) << "CplexSolver::AddSingleObjective"; + + bool is_maximize = objective.maximize(); + + RETURN_IF_ERROR(cplex_->ChgObjSen(is_maximize ? CPX_MAX : CPX_MIN)); + + RETURN_IF_ERROR(cplex_->ChgObjOffset(objective.offset())); + + ASSIGN_OR_RETURN( + (auto [indices, values]), + PrepareLinearObjectiveNonzeros(objective.linear_coefficients().ids(), + objective.linear_coefficients().values())); + RETURN_IF_ERROR(cplex_->ChgObj(indices, values)); + + return absl::OkStatus(); +} + +absl::Status CplexSolver::AddNewLinearConstraints( + const LinearConstraintsProto& new_constraints) { + VLOG(2) << "CplexSolver::AddNewLinearConstraints"; + + const int num_new_constraints = new_constraints.lower_bounds().size(); + + // (parallel) dense vectors of size |num_new_constraints| + std::vector rhs; + std::vector sense; + rhs.reserve(num_new_constraints); + sense.reserve(num_new_constraints); + + // (single) sparse vector of size <= |num_new_constraints| + std::vector range_cons_index; + std::vector range_cons_diff; + + for (int i = 0; i < num_new_constraints; ++i) { + const int64_t id = new_constraints.ids(i); + LinearConstraintData& constraint_data = + gtl::InsertKeyOrDie(&linear_constraints_map_, id); + + const double lb = new_constraints.lower_bounds(i); + RETURN_IF_ERROR(SafeCplexDouble(lb)); + const double ub = new_constraints.upper_bounds(i); + RETURN_IF_ERROR(SafeCplexDouble(ub)); + + constraint_data.lower_bound = lb; + constraint_data.upper_bound = ub; + constraint_data.constraint_index = i + num_cplex_lin_cons_; + + if (IsFinite(lb) && IsFinite(ub)) { + if (lb == ub) { + // eq constraint + rhs.push_back(lb); + sense.push_back('E'); + } else { + // range-constraint + rhs.push_back(lb); + sense.push_back('R'); + + range_cons_index.push_back(i + num_cplex_lin_cons_); + range_cons_diff.push_back(ub - lb); + } + } else if (IsFinite(lb)) { + // greater equal constraint + rhs.push_back(lb); + sense.push_back('G'); + } else if (IsFinite(ub)) { + // less equal constraint + rhs.push_back(ub); + sense.push_back('L'); + } else { + // Free constraint: -∞ ≤ a·x ≤ +∞. Must still create a CPLEX row so + // that constraint indices stay synchronized. + rhs.push_back(-CPX_INFBOUND); + sense.push_back('G'); + } + } + + const std::vector names_maybe_truncated = + TruncateNames(new_constraints.names()); + + RETURN_IF_ERROR(cplex_->NewRows(rhs, sense, + absl::Span(), // rngval + names_maybe_truncated)); + + RETURN_IF_ERROR(cplex_->ChgRngVal(range_cons_index, range_cons_diff)); + + num_cplex_lin_cons_ += num_new_constraints; + + return absl::OkStatus(); +} + +absl::Status CplexSolver::ChangeCoefficients( + const SparseDoubleMatrixProto& matrix) { + for (const double coefficient : matrix.coefficients()) { + RETURN_IF_ERROR(SafeCplexDouble(coefficient)); + } + const int num_coefficients = matrix.row_ids().size(); + std::vector row_index(num_coefficients); + std::vector col_index(num_coefficients); + for (int k = 0; k < num_coefficients; ++k) { + row_index[k] = + linear_constraints_map_.at(matrix.row_ids(k)).constraint_index; + col_index[k] = variables_map_.at(matrix.column_ids(k)); + } + return cplex_->ChgCoefList(row_index, col_index, matrix.coefficients()); +} + +absl::Status CplexSolver::LoadModel(const ModelProto& input_model) { + CHECK(cplex_ != nullptr); + + RETURN_IF_ERROR(cplex_->ChgProbName(TruncateName(input_model.name()))); + + RETURN_IF_ERROR(AddNewVariables(input_model.variables())); + + RETURN_IF_ERROR(AddNewLinearConstraints(input_model.linear_constraints())); + + RETURN_IF_ERROR(ChangeCoefficients(input_model.linear_constraint_matrix())); + + if (input_model.auxiliary_objectives().empty()) { + RETURN_IF_ERROR(AddSingleObjective(input_model.objective())); + } else { + return absl::UnimplementedError( + "Multiple objectives are currently not supported in CPLEX interface"); + } + return absl::OkStatus(); +} + +absl::Status CplexSolver::UpdateLinearConstraints( + const LinearConstraintUpdatesProto& constraints_update, + std::vector& deleted_variables_index) { + const SparseDoubleVectorProto& constraint_lower_bounds = + constraints_update.lower_bounds(); + const SparseDoubleVectorProto& constraint_upper_bounds = + constraints_update.upper_bounds(); + + // If no update, just return. + if (constraint_lower_bounds.ids().empty() && + constraint_upper_bounds.ids().empty()) { + return absl::OkStatus(); + } + + struct UpdateConstraintData { + LinearConstraintId constraint_id; + LinearConstraintData& source; + double new_lower_bound; + double new_upper_bound; + UpdateConstraintData(const LinearConstraintId id, + LinearConstraintData& reference) + : constraint_id(id), + source(reference), + new_lower_bound(reference.lower_bound), + new_upper_bound(reference.upper_bound) {} + }; + const int upper_bounds_size = constraint_upper_bounds.ids().size(); + const int lower_bounds_size = constraint_lower_bounds.ids().size(); + std::vector update_vector; + update_vector.reserve(upper_bounds_size + lower_bounds_size); + // We exploit the fact that IDs are sorted in increasing order to merge + // changes into a vector of aggregated changes. + for (int lower_index = 0, upper_index = 0; + lower_index < lower_bounds_size || upper_index < upper_bounds_size;) { + VariableId lower_id = std::numeric_limits::max(); + if (lower_index < lower_bounds_size) { + lower_id = constraint_lower_bounds.ids(lower_index); + } + VariableId upper_id = std::numeric_limits::max(); + if (upper_index < upper_bounds_size) { + upper_id = constraint_upper_bounds.ids(upper_index); + } + const VariableId id = std::min(lower_id, upper_id); + DCHECK(id < std::numeric_limits::max()); + UpdateConstraintData update(id, linear_constraints_map_.at(id)); + if (lower_id == upper_id) { + update.new_lower_bound = constraint_lower_bounds.values(lower_index++); + update.new_upper_bound = constraint_upper_bounds.values(upper_index++); + } else if (lower_id < upper_id) { + update.new_lower_bound = constraint_lower_bounds.values(lower_index++); + } else { /* upper_id < lower_id */ + update.new_upper_bound = constraint_upper_bounds.values(upper_index++); + } + update_vector.emplace_back(update); + } + + std::vector sense_data; + std::vector rhs_data; + std::vector rhs_index; + + std::vector range_cons_index; + std::vector range_cons_diff; + + // Iterate on the changes, and populate the three possible changes. + for (UpdateConstraintData& update_data : update_vector) { + const bool same_lower_bound = + (update_data.source.lower_bound == update_data.new_lower_bound) || + ((update_data.source.lower_bound <= -CPX_INFBOUND) && + (update_data.new_lower_bound <= -CPX_INFBOUND)); + const bool same_upper_bound = + (update_data.source.upper_bound == update_data.new_upper_bound) || + ((update_data.source.upper_bound >= CPX_INFBOUND) && + (update_data.new_upper_bound >= CPX_INFBOUND)); + if (same_upper_bound && same_lower_bound) continue; + + // Validate the new bounds before mutating cached state, matching the + // initial-load validation in AddNewLinearConstraints(). + RETURN_IF_ERROR(SafeCplexDouble(update_data.new_lower_bound)); + RETURN_IF_ERROR(SafeCplexDouble(update_data.new_upper_bound)); + + // Save into linear_constraints_map_[id] the new bounds for the linear + // constraint. + update_data.source.lower_bound = update_data.new_lower_bound; + update_data.source.upper_bound = update_data.new_upper_bound; + // Detect the type of constraint to add and store RHS and bounds. + if (update_data.new_lower_bound <= -CPX_INFBOUND && + update_data.new_upper_bound < CPX_INFBOUND) { + rhs_index.emplace_back(update_data.source.constraint_index); + rhs_data.emplace_back(update_data.new_upper_bound); + sense_data.emplace_back('L'); + } else if (update_data.new_lower_bound > -CPX_INFBOUND && + update_data.new_upper_bound >= CPX_INFBOUND) { + rhs_index.emplace_back(update_data.source.constraint_index); + rhs_data.emplace_back(update_data.new_lower_bound); + sense_data.emplace_back('G'); + } else if (update_data.new_lower_bound == update_data.new_upper_bound) { + rhs_index.emplace_back(update_data.source.constraint_index); + rhs_data.emplace_back(update_data.new_lower_bound); + sense_data.emplace_back('E'); + } else if (update_data.new_lower_bound <= -CPX_INFBOUND && + update_data.new_upper_bound >= CPX_INFBOUND) { + // Free constraint: -∞ ≤ a·x ≤ +∞. + rhs_index.emplace_back(update_data.source.constraint_index); + rhs_data.emplace_back(-CPX_INFBOUND); + sense_data.emplace_back('G'); + } else { + // Range constraint (both bounds finite, lb != ub). + range_cons_index.emplace_back(update_data.source.constraint_index); + range_cons_diff.emplace_back(update_data.new_upper_bound - + update_data.new_lower_bound); + + rhs_index.emplace_back(update_data.source.constraint_index); + rhs_data.emplace_back(update_data.new_lower_bound); + sense_data.emplace_back('R'); + } + } + + // Pass down changes to Cplex. + if (!rhs_index.empty()) { + RETURN_IF_ERROR(cplex_->ChgSense(rhs_index, sense_data)); + RETURN_IF_ERROR(cplex_->ChgRhs(rhs_index, rhs_data)); + RETURN_IF_ERROR(cplex_->ChgRngVal(range_cons_index, range_cons_diff)); + } + + return absl::OkStatus(); +} + +void CplexSolver::UpdateCplexIndices(const DeletedIndices& deleted_indices) { + // Recover the updated indices of variables. + if (!deleted_indices.variables.empty()) { + const std::vector old_to_new = + IndexUpdateMap(num_cplex_variables_, deleted_indices.variables); + for (auto& [_, cpx_index] : variables_map_) { + cpx_index = old_to_new[cpx_index]; + CHECK_NE(cpx_index, kDeletedIndex); + } + } + // Recover the updated indices of linear constraints. + if (!deleted_indices.linear_constraints.empty()) { + const std::vector old_to_new = + IndexUpdateMap(num_cplex_lin_cons_, deleted_indices.linear_constraints); + for (auto& [_, lin_con_data] : linear_constraints_map_) { + lin_con_data.constraint_index = old_to_new[lin_con_data.constraint_index]; + CHECK_NE(lin_con_data.constraint_index, kDeletedIndex); + } + } +} + +absl::StatusOr CplexSolver::Update(const ModelUpdateProto& model_update) { + if (!undeletable_variables_.empty()) { + for (const VariableId id : model_update.deleted_variable_ids()) { + if (undeletable_variables_.contains(id)) { + return false; + } + } + } + if (!UpdateIsSupported(model_update, kCplexSupportedStructures)) { + return false; + } + + RETURN_IF_ERROR(AddNewVariables(model_update.new_variables())); + + RETURN_IF_ERROR( + AddNewLinearConstraints(model_update.new_linear_constraints())); + + RETURN_IF_ERROR( + ChangeCoefficients(model_update.linear_constraint_matrix_updates())); + + if (model_update.objective_updates().has_direction_update()) { + RETURN_IF_ERROR(cplex_->ChgObjSen( + model_update.objective_updates().direction_update() ? CPX_MAX + : CPX_MIN)); + } + + if (model_update.objective_updates().has_offset_update()) { + RETURN_IF_ERROR( + cplex_->ChgObjOffset(model_update.objective_updates().offset_update())); + } + + if (!model_update.objective_updates().linear_coefficients().ids().empty()) { + ASSIGN_OR_RETURN( + (auto [obj_indices, obj_values]), + PrepareLinearObjectiveNonzeros( + model_update.objective_updates().linear_coefficients().ids(), + model_update.objective_updates().linear_coefficients().values())); + RETURN_IF_ERROR(cplex_->ChgObj(obj_indices, obj_values)); + } + + // Update bounds + const auto& var_updates = model_update.variable_updates(); + + // Validate all new variable bounds before passing them to CPLEX, matching + // the initial-load validation in AddNewVariables(). + for (int i = 0; i < var_updates.lower_bounds().ids_size(); ++i) { + RETURN_IF_ERROR(SafeCplexDouble(var_updates.lower_bounds().values(i))); + } + for (int i = 0; i < var_updates.upper_bounds().ids_size(); ++i) { + RETURN_IF_ERROR(SafeCplexDouble(var_updates.upper_bounds().values(i))); + } + + if (!var_updates.lower_bounds().ids().empty()) { + std::vector indices; + std::vector values; + std::vector lu; + for (int i = 0; i < var_updates.lower_bounds().ids_size(); ++i) { + indices.push_back(variables_map_.at(var_updates.lower_bounds().ids(i))); + values.push_back(var_updates.lower_bounds().values(i)); + lu.push_back('L'); + } + RETURN_IF_ERROR(cplex_->Chgbds(indices, lu, values)); + } + if (!var_updates.upper_bounds().ids().empty()) { + std::vector indices; + std::vector values; + std::vector lu; + for (int i = 0; i < var_updates.upper_bounds().ids_size(); ++i) { + indices.push_back(variables_map_.at(var_updates.upper_bounds().ids(i))); + values.push_back(var_updates.upper_bounds().values(i)); + lu.push_back('U'); + } + RETURN_IF_ERROR(cplex_->Chgbds(indices, lu, values)); + } + + // Integers + if (model_update.variable_updates().has_integers()) { + const SparseBoolVectorProto& update = + model_update.variable_updates().integers(); + std::vector index; + index.reserve(update.ids_size()); + std::vector value; + value.reserve(update.values_size()); + for (int i = 0; i < update.ids_size(); ++i) { + index.push_back(variables_map_.at(update.ids(i))); + value.push_back(update.values(i) ? CPX_INTEGER : CPX_CONTINUOUS); + } + RETURN_IF_ERROR(cplex_->Chgctype(index, value)); + } + + DeletedIndices deleted_indices; + + RETURN_IF_ERROR(UpdateLinearConstraints( + model_update.linear_constraint_updates(), deleted_indices.variables)); + + for (const VariableId id : model_update.deleted_variable_ids()) { + deleted_indices.variables.emplace_back(variables_map_.at(id)); + variables_map_.erase(id); + } + + for (const LinearConstraintId id : + model_update.deleted_linear_constraint_ids()) { + LinearConstraintData& constraint_data = linear_constraints_map_.at(id); + deleted_indices.linear_constraints.push_back( + constraint_data.constraint_index); + linear_constraints_map_.erase(id); + } + + UpdateCplexIndices(deleted_indices); + + if (!deleted_indices.linear_constraints.empty()) { + RETURN_IF_ERROR(cplex_->DelSetRows(deleted_indices.linear_constraints)); + num_cplex_lin_cons_ -= deleted_indices.linear_constraints.size(); + } + + if (!deleted_indices.variables.empty()) { + RETURN_IF_ERROR(cplex_->DelSetCols(deleted_indices.variables)); + num_cplex_variables_ -= deleted_indices.variables.size(); + } + + return true; +} + +absl::StatusOr> CplexSolver::New( + const ModelProto& input_model, const SolverInterface::InitArgs& init_args) { + RETURN_IF_ERROR( + ModelIsSupported(input_model, kCplexSupportedStructures, "Cplex")); + + ASSIGN_OR_RETURN(std::unique_ptr cplex, CplexFromInitArgs(init_args)); + auto cplex_solver = absl::WrapUnique(new CplexSolver(std::move(cplex))); + RETURN_IF_ERROR(cplex_solver->LoadModel(input_model)); + return cplex_solver; +} + +absl::StatusOr CplexSolver::ListInvertedBounds() const { + InvertedBounds inverted_bounds; + ASSIGN_OR_RETURN(const int n_vars, cplex_->GetNumCols()); + + if (n_vars > 0) { + ASSIGN_OR_RETURN(const std::vector var_lbs, + cplex_->GetLb(0, n_vars - 1)); + ASSIGN_OR_RETURN(const std::vector var_ubs, + cplex_->GetUb(0, n_vars - 1)); + + for (const auto& [id, index] : variables_map_) { + if (var_lbs[index] > var_ubs[index]) { + inverted_bounds.variables.push_back(id); + } + } + } + + for (const auto& [id, cstr_data] : linear_constraints_map_) { + if (cstr_data.lower_bound > cstr_data.upper_bound) { + inverted_bounds.linear_constraints.push_back(id); + } + } + + std::sort(inverted_bounds.variables.begin(), inverted_bounds.variables.end()); + std::sort(inverted_bounds.linear_constraints.begin(), + inverted_bounds.linear_constraints.end()); + return inverted_bounds; +} + +absl::Status CplexSolver::ResetModelParameters( + const ModelSolveParametersProto& model_parameters) { + // No model-level attributes to reset. CPLEX does not yet implement + // branching priorities, lazy constraints, or other per-solve model + // modifications that Gurobi resets here. Solve parameters (threads, + // tolerances, etc.) are re-applied by SetParameters() on each Solve() call, + // so resetting them here is unnecessary. + return absl::OkStatus(); +} + +absl::StatusOr CplexSolver::Solve( + const SolveParametersProto& parameters, + const ModelSolveParametersProto& model_parameters, + const MessageCallback message_cb, + const CallbackRegistrationProto& callback_registration, const Callback cb, + const SolveInterrupter* absl_nullable const interrupter) { + RETURN_IF_ERROR(ModelSolveParametersAreSupported( + model_parameters, kCplexSupportedStructures, "Cplex")); + + // Solution hints are silently ignored — CPLEX's C API supports MIP starts + // (CPXaddmipstarts) but this integration does not implement them yet. + // Silently ignoring matches the behavior of Glop and GLPK. + if (model_parameters.has_initial_basis()) { + return absl::UnimplementedError( + "Initial basis is not currently supported for CPLEX."); + } + if (!model_parameters.branching_priorities().ids().empty()) { + return absl::UnimplementedError( + "Branching priorities are not currently supported for CPLEX."); + } + if (!model_parameters.lazy_linear_constraint_ids().empty()) { + return absl::UnimplementedError( + "Lazy linear constraints are not currently supported for CPLEX."); + } + + const absl::Time start = absl::Now(); + + ASSIGN_OR_RETURN(const InvertedBounds inverted_bounds, ListInvertedBounds()); + RETURN_IF_ERROR(inverted_bounds.ToStatus()); + + // Set output-routing parameters before applying solver parameters, so that + // console suppression is in effect when CPLEX processes parameter changes. + // Per the MathOpt spec (parameters.proto, enable_output): + // "if the solver supports message callback and the user registers a + // callback for it, then this parameter value is ignored and no traces + // are printed." + if (message_cb != nullptr) { + RETURN_IF_ERROR(cplex_->SetParamBool(CPXPARAM_ScreenOutput, false)); + RETURN_IF_ERROR(cplex_->SetParamBool(CPXPARAM_ParamDisplay, false)); + } else { + RETURN_IF_ERROR(cplex_->SetParamBool(CPXPARAM_ScreenOutput, + parameters.enable_output())); + RETURN_IF_ERROR(cplex_->SetParamBool(CPXPARAM_ParamDisplay, true)); + } + + // We use a volatile int as the terminate flag for CPLEX. + volatile int terminate_flag = 0; + + // Register the callback to update the terminate_flag when interruption is + // requested. The ScopedSolveInterrupterCallback ensures that the callback is + // unregistered when this scope exits. + const ScopedSolveInterrupterCallback scoped_interrupt_cb( + interrupter, [&terminate_flag]() { terminate_flag = 1; }); + + // Pass the address of the terminate flag to CPLEX. + RETURN_IF_ERROR(cplex_->SetTerminate(&terminate_flag)); + + // Ensure we clear the terminate pointer in CPLEX when we exit this function + // to avoid CPLEX accessing a dangling pointer (local variable on stack). + absl::Cleanup clear_terminate = [this]() { + const absl::Status status = cplex_->SetTerminate(nullptr); + if (!status.ok()) { + LOG(ERROR) << "Failed to clear CPLEX terminate flag: " << status; + } + }; + + // Channel state — declared before the cleanup so they are destroyed after it. + CPXCHANNELptr result_channel = nullptr; + CPXCHANNELptr warning_channel = nullptr; + CPXCHANNELptr error_channel = nullptr; + CPXCHANNELptr log_channel = nullptr; + std::unique_ptr buffered_message_callback; + + bool result_attached = false; + bool warning_attached = false; + bool error_attached = false; + bool log_attached = false; + + // IMPORTANT: The cleanup MUST be constructed BEFORE any AddFuncDest call. + // If an AddFuncDest call fails mid-way (via RETURN_IF_ERROR), the cleanup + // detaches every channel that was successfully attached, preventing a + // use-after-free when buffered_message_callback is destroyed on stack + // unwind. + absl::Cleanup clear_message_cb = [&]() { + if (buffered_message_callback == nullptr) return; + void* const handle = buffered_message_callback.get(); + auto detach = [&](const bool attached, CPXCHANNELptr channel, + const char* label) { + if (!attached) return; + const absl::Status status = + cplex_->DelFuncDest(channel, handle, MessageCallbackImpl); + if (!status.ok()) { + LOG(ERROR) << "Failed to remove CPLEX " << label + << " message callback: " << status; + } + }; + detach(result_attached, result_channel, "result"); + detach(warning_attached, warning_channel, "warning"); + detach(error_attached, error_channel, "error"); + detach(log_attached, log_channel, "log"); + }; + + // Register message callback destinations on all four CPLEX channels. + if (message_cb != nullptr) { + buffered_message_callback = + std::make_unique(message_cb); + ASSIGN_OR_RETURN( + (std::tie(result_channel, warning_channel, error_channel, log_channel)), + cplex_->GetChannels()); + + void* const handle = buffered_message_callback.get(); + + auto attach = [&](CPXCHANNELptr channel, bool& attached) -> absl::Status { + if (channel == nullptr) return absl::OkStatus(); + RETURN_IF_ERROR( + cplex_->AddFuncDest(channel, handle, MessageCallbackImpl)); + attached = true; + return absl::OkStatus(); + }; + + RETURN_IF_ERROR(attach(result_channel, result_attached)); + RETURN_IF_ERROR(attach(warning_channel, warning_attached)); + RETURN_IF_ERROR(attach(error_channel, error_attached)); + RETURN_IF_ERROR(attach(log_channel, log_attached)); + } + + // Set solver parameters (time limits, tolerances, algorithm choices, etc.) + // Applied after channel attachment so that any diagnostics CPLEX emits + // during parameter setup are routed through the user's message callback. + RETURN_IF_ERROR(SetParameters(parameters, model_parameters)); + + if (callback_registration.request_registration_size() > 0 || cb != nullptr) { + return absl::UnimplementedError( + "Callbacks are not currently supported for CPLEX."); + } + + ASSIGN_OR_RETURN(const bool is_mip, IsMIP()); + if (is_mip) { + RETURN_IF_ERROR(cplex_->MipOpt()); + } else { + RETURN_IF_ERROR(cplex_->LpOpt()); + } + + if (buffered_message_callback != nullptr) { + buffered_message_callback->Flush(); + } + + // Detach message callbacks before result extraction. CPLEX emits internal + // diagnostics (e.g., Error 1217) during stat queries that are harmless but + // would be forwarded to the user's message callback if still attached. + std::move(clear_message_cb).Invoke(); + + // Suppress CPLEX's default stderr output during result extraction. CPLEX + // prints error messages (e.g., "CPLEX Error 1217: No solution exists.") to + // stderr when querying stats that are inapplicable to the current solve + // state, even though the return code is handled correctly. Disabling screen + // output after the solve phase ensures all solve-time messages are visible + // while extraction noise is suppressed. + // + // This is unconditional: when message_cb was set, ScreenOutput is already + // false (set above); this handles the case where message_cb is null but + // enable_output was true. + RETURN_IF_ERROR(cplex_->SetParamBool(CPXPARAM_ScreenOutput, false)); + + const bool had_cutoff = parameters.has_cutoff_limit(); + const bool had_iteration_limit = parameters.has_iteration_limit(); + const bool had_objective_limit = parameters.has_objective_limit(); + + ASSIGN_OR_RETURN( + SolveResultProto solve_result, + ExtractSolveResultProto(start, model_parameters, had_cutoff, + had_iteration_limit, had_objective_limit)); + + // Reset CPLEX parameters so that settings from this Solve() call do not + // leak into subsequent Solve() calls (mirrors Gurobi's ResetParameters). + RETURN_IF_ERROR(cplex_->SetDefaults()); + RETURN_IF_ERROR(ResetModelParameters(model_parameters)); + + return solve_result; +} + +absl::StatusOr +CplexSolver::ComputeInfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + const SolveInterrupter* absl_nullable interrupter) { + return absl::UnimplementedError( + "ComputeInfeasibleSubsystem not implemented for CPLEX"); +} + +absl::StatusOr, std::vector>> +CplexSolver::PrepareLinearObjectiveNonzeros( + const absl::Span indices, + const absl::Span values) { + VLOG(2) << "CplexSolver::PrepareLinearObjectiveNonzeros"; + + if (indices.size() != values.size()) + return absl::InvalidArgumentError( + "CplexSolver::PrepareLinearObjectiveNonzeros: sizes of arguments don't " + "match"); + + std::vector res_indices; + std::vector res_values; + + for (size_t i = 0; i < indices.size(); ++i) { + RETURN_IF_ERROR(SafeCplexDouble(values[i])); + res_indices.push_back(variables_map_.at(indices[i])); + res_values.push_back(values[i]); + } + + return {{res_indices, res_values}}; +} + +bool CplexSolver::IsFinite(double value) { + return value < CPX_INFBOUND && value > -CPX_INFBOUND; +} + +MATH_OPT_REGISTER_SOLVER(SOLVER_TYPE_CPLEX, CplexSolver::New) + +} // namespace math_opt +} // namespace operations_research \ No newline at end of file diff --git a/ortools/math_opt/solvers/cplex_solver.h b/ortools/math_opt/solvers/cplex_solver.h new file mode 100644 index 00000000000..23436ffa4c0 --- /dev/null +++ b/ortools/math_opt/solvers/cplex_solver.h @@ -0,0 +1,273 @@ +// Copyright 2010-2026 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. + +#ifndef ORTOOLS_MATH_OPT_SOLVERS_CPLEX_SOLVER_H_ +#define ORTOOLS_MATH_OPT_SOLVERS_CPLEX_SOLVER_H_ + +#include +#include +#include +#include +#include +#include + +#include "absl/base/nullability.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/container/linked_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "ortools/math_opt/callback.pb.h" +#include "ortools/math_opt/core/inverted_bounds.h" +#include "ortools/math_opt/core/solver_interface.h" +#include "ortools/math_opt/infeasible_subsystem.pb.h" +#include "ortools/math_opt/model.pb.h" +#include "ortools/math_opt/model_parameters.pb.h" +#include "ortools/math_opt/model_update.pb.h" +#include "ortools/math_opt/parameters.pb.h" +#include "ortools/math_opt/result.pb.h" +#include "ortools/math_opt/solution.pb.h" +#include "ortools/math_opt/solvers/cplex.pb.h" +#include "ortools/math_opt/solvers/cplex/g_cplex.h" +#include "ortools/math_opt/solvers/message_callback_data.h" +#include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/third_party_solvers/cplex_environment.h" +#include "ortools/util/solve_interrupter.h" + +namespace operations_research { +namespace math_opt { + +struct CplexVersion { + int major = 0; + int minor = 0; + int revision = 0; + int subrevision = 0; + + bool operator>=(const CplexVersion& other) const { + return std::tie(major, minor, revision, subrevision) >= + std::tie(other.major, other.minor, other.revision, + other.subrevision); + } +}; + +absl::StatusOr ParseCplexVersion(absl::string_view version_str); + +// Returns true if the loaded CPLEX library supports objective limit. +// Returns false if the library cannot be loaded or the version is too old. +bool CplexSupportsObjectiveLimit(); // Could be generalized with a + // supported-features struct. + +class CplexSolver : public SolverInterface { + public: + static absl::StatusOr> New( + const ModelProto& input_model, + const SolverInterface::InitArgs& init_args); + + absl::StatusOr Solve( + const SolveParametersProto& parameters, + const ModelSolveParametersProto& model_parameters, + MessageCallback message_cb, + const CallbackRegistrationProto& callback_registration, Callback cb, + const SolveInterrupter* absl_nullable interrupter) override; + absl::StatusOr Update(const ModelUpdateProto& model_update) override; + absl::StatusOr + ComputeInfeasibleSubsystem( + const SolveParametersProto& parameters, MessageCallback message_cb, + const SolveInterrupter* absl_nullable interrupter) override; + + absl::StatusOr Version() const; + + private: + explicit CplexSolver(std::unique_ptr g_cplex); + + // For easing reading the code, we declare these types: + using VariableId = int64_t; + using AuxiliaryObjectiveId = int64_t; + using LinearConstraintId = int64_t; + using AnyConstraintId = int64_t; + using CplexVariableIndex = int; + using CplexLinearConstraintIndex = int; + using CplexGeneralConstraintIndex = int; + using CplexAnyConstraintIndex = int; + + static constexpr CplexVariableIndex kUnspecifiedIndex = -1; + static constexpr CplexAnyConstraintIndex kUnspecifiedConstraint = -2; + static constexpr double kInf = std::numeric_limits::infinity(); + + struct CplexModelElements { + std::vector variables; + std::vector linear_constraints; + }; + + // Data associated with each linear constraint. With it we know if the + // underlying representation is either: + // linear_terms <= upper_bound (if lower bound <= -CPX_INFBOUND) + // linear_terms >= lower_bound (if upper bound >= CPX_INFBOUND) + // linear_terms == xxxxx_bound (if upper_bound == lower_bound) + // lower_bound <= linear_term <= upper_bound + struct LinearConstraintData { + // Returns all Cplex elements related to this constraint (including the + // linear constraint itself). Will CHECK-fail if any element is unspecified. + CplexModelElements DependentElements() const; + + CplexLinearConstraintIndex constraint_index = kUnspecifiedConstraint; + double lower_bound = -kInf; + double upper_bound = kInf; + }; + + struct SolutionClaims { + bool primal_feasible_solution_exists; + bool dual_feasible_solution_exists; + }; + + struct SolutionsAndClaims { + std::vector solutions; + SolutionClaims solution_claims; + }; + + template + struct SolutionAndClaim { + std::optional solution; + bool feasible_solution_exists = false; + }; + + using IdHashMap = absl::linked_hash_map; + + absl::StatusOr ExtractSolveResultProto( + absl::Time start, const ModelSolveParametersProto& model_parameters, + bool had_cutoff, bool had_iteration_limit, bool had_objective_limit); + absl::StatusOr GetSolutions( + const ModelSolveParametersProto& model_parameters); + absl::StatusOr GetSolveStats(absl::Time start) const; + + absl::StatusOr GetCplexBestDualBound() const; + absl::StatusOr GetBestDualBound( + absl::Span solutions) const; + absl::StatusOr GetBestPrimalBound( + absl::Span solutions) const; + + absl::StatusOr IsMaximize() const; + + absl::StatusOr ConvertTerminationReason( + int cplex_status, bool had_cutoff, bool had_iteration_limit, + bool had_objective_limit, SolutionClaims solution_claims, + double best_primal_bound, double best_dual_bound); + + // Returns solution information appropriate and available for an LP (linear + // constraints + linear objective, only). + absl::StatusOr GetLpSolution( + const ModelSolveParametersProto& model_parameters); + // Returns solution information appropriate and available for a MIP + // (integrality on some/all decision variables). + absl::StatusOr GetMipSolutions( + const ModelSolveParametersProto& model_parameters); + + // return bool field should be true if a primal solution exists. + absl::StatusOr> + GetConvexPrimalSolutionIfAvailable( + const ModelSolveParametersProto& model_parameters); + absl::StatusOr> + GetConvexDualSolutionIfAvailable( + const ModelSolveParametersProto& model_parameters); + + absl::Status SetParameters( + const SolveParametersProto& parameters, + const ModelSolveParametersProto& model_parameters = {}); + absl::Status AddNewLinearConstraints( + const LinearConstraintsProto& constraints); + absl::Status AddNewVariables(const VariablesProto& new_variables); + absl::Status AddSingleObjective(const ObjectiveProto& objective); + absl::Status ChangeCoefficients(const SparseDoubleMatrixProto& matrix); + absl::Status LoadModel(const ModelProto& input_model); + + struct DeletedIndices { + std::vector variables; + std::vector linear_constraints; + }; + + void UpdateCplexIndices(const DeletedIndices& deleted_indices); + absl::Status UpdateLinearConstraints( + const LinearConstraintUpdatesProto& update, + std::vector& deleted_variables_index); + + // Fills in result with the values in cplex_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 + // the filter predicate are added. + template + void CplexVectorToSparseDoubleVector( + absl::Span cplex_values, const T& map, + SparseDoubleVectorProto& result, + const SparseVectorFilterProto& filter) const; + + int get_model_index(CplexVariableIndex index) const { return index; } + int get_model_index(const LinearConstraintData& index) const { + return index.constraint_index; + } + + // Returns true if the problem has any integrality constraints. + absl::StatusOr IsMIP() const; + + // Returns the ids of variables and linear constraints with inverted bounds. + absl::StatusOr ListInvertedBounds() const; + + absl::Status ResetModelParameters( + const ModelSolveParametersProto& model_parameters); + + const std::unique_ptr cplex_; + + // Note that we use linked_hash_map for the indices of the CPLEX model + // variables and linear constraints to ensure that iteration over the map + // maintains their insertion order (and, thus, the order in which they appear + // in the model). As of 2022-06-28 this property is necessary to ensure that + // duals and bases are deterministically ordered. + + // Internal correspondence from variable proto IDs to Cplex-numbered + // variables. + absl::linked_hash_map variables_map_; + // Internal correspondence from linear constraint proto IDs to + // Cplex-numbered linear constraint and extra information. + absl::linked_hash_map + linear_constraints_map_; + + // Fields to track the number of Cplex variables and constraints. These + // quantities are updated immediately after adding or removing to the model. + + // Number of Cplex variables. + int num_cplex_variables_ = 0; + // Number of Cplex linear constraints. + int num_cplex_lin_cons_ = 0; + + // Some MathOpt variables cannot be deleted without rendering the rest of the + // model invalid. We flag these variables to check in CanUpdate(). As of + // 2022-07-01 elements are not erased from this set, and so it may be overly + // conservative in rejecting updates. + absl::flat_hash_set undeletable_variables_; + + static constexpr int kCpxBasicConstraint = 0; + static constexpr int kCpxNonBasicConstraint = -1; + + // Respects solvers interpretation of finite values (CPX_INFBOUND) + static bool IsFinite(double value); + + absl::StatusOr, std::vector>> + PrepareLinearObjectiveNonzeros(const absl::Span indices, + const absl::Span values); +}; + +} // namespace math_opt +} // namespace operations_research + +#endif // ORTOOLS_MATH_OPT_SOLVERS_CPLEX_SOLVER_H_ diff --git a/ortools/math_opt/solvers/cplex_solver_test.cc b/ortools/math_opt/solvers/cplex_solver_test.cc new file mode 100644 index 00000000000..d59393c1024 --- /dev/null +++ b/ortools/math_opt/solvers/cplex_solver_test.cc @@ -0,0 +1,257 @@ +// Copyright 2010-2026 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. + +#include "ortools/math_opt/solvers/cplex_solver.h" + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/math_opt/cpp/matchers.h" +#include "ortools/math_opt/cpp/math_opt.h" +#include "ortools/math_opt/solver_tests/callback_tests.h" +#include "ortools/math_opt/solver_tests/generic_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/ip_multiple_solutions_tests.h" +#include "ortools/math_opt/solver_tests/ip_parameter_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/status_tests.h" +#include "ortools/math_opt/testing/param_name.h" + +namespace operations_research { +namespace math_opt { +namespace { + +using ::testing::HasSubstr; +using ::testing::Values; +using ::testing::ValuesIn; +using ::testing::status::IsOkAndHolds; +using ::testing::status::StatusIs; + +SimpleLpTestParameters CplexDefaults() { + return SimpleLpTestParameters( + SolverType::kCplex, SolveParameters(), /*supports_duals=*/true, + /*supports_basis=*/false, + /*ensures_primal_ray=*/false, /*ensures_dual_ray=*/false, + /*disallows_infeasible_or_unbounded=*/false); +} + +INSTANTIATE_TEST_SUITE_P(CplexSimpleLpTest, SimpleLpTest, + testing::Values(CplexDefaults())); + +INSTANTIATE_TEST_SUITE_P(CplexLpModelSolveParametersTest, + LpModelSolveParametersTest, + testing::Values(LpModelSolveParametersTestParameters( + SolverType::kCplex, + /*exact_zeros=*/true, + /*supports_duals=*/true, + /*supports_primal_only_warm_starts=*/false))); + +INSTANTIATE_TEST_SUITE_P( + CplexLpParameterTest, LpParameterTest, + testing::Values(LpParameterTestParams( + SolverType::kCplex, + /*supports_simplex=*/true, + /*supports_barrier=*/true, + /*supports_first_order=*/false, + /*supports_random_seed=*/true, + /*supports_presolve=*/true, + /*supports_cutoff=*/false, + /*supports_objective_limit=*/CplexSupportsObjectiveLimit(), + /*supports_best_bound_limit=*/false, + /*reports_limits=*/false))); + +StatusTestParameters StatusDefault() { + return StatusTestParameters(SolverType::kCplex, + SolveParameters{.cuts = Emphasis::kOff}, + /*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); +} + +INSTANTIATE_TEST_SUITE_P(CplexStatusTest, StatusTest, Values(StatusDefault())); + +INSTANTIATE_TEST_SUITE_P(CplexIncrementalLpTest, IncrementalLpTest, + testing::Values(SolverType::kCplex)); + +INSTANTIATE_TEST_SUITE_P(CplexSimpleMipTest, SimpleMipTest, + Values(SolverType::kCplex)); + +INSTANTIATE_TEST_SUITE_P(CplexIncrementalMipTest, IncrementalMipTest, + Values(SolverType::kCplex)); + +// so presolve is disabled in the tests. +INSTANTIATE_TEST_SUITE_P( + CplexPrimalSimplexLpIncompleteSolveTest, LpIncompleteSolveTest, + testing::Values(LpIncompleteSolveTestParams( + SolverType::kCplex, + /*lp_algorithm=*/LPAlgorithm::kPrimalSimplex, + /*supports_iteration_limit=*/true, /*supports_initial_basis=*/false, + /*supports_incremental_solve=*/true, /*supports_basis=*/false, + /*supports_presolve=*/true, /*check_primal_objective=*/true, + /*primal_solution_status_always_set=*/true, + /*dual_solution_status_always_set=*/true))); +INSTANTIATE_TEST_SUITE_P( + CplexDualSimplexLpIncompleteSolveTest, LpIncompleteSolveTest, + testing::Values(LpIncompleteSolveTestParams( + SolverType::kCplex, + /*lp_algorithm=*/LPAlgorithm::kDualSimplex, + /*supports_iteration_limit=*/true, /*supports_initial_basis=*/false, + /*supports_incremental_solve=*/true, /*supports_basis=*/false, + /*supports_presolve=*/true, /*check_primal_objective=*/true, + /*primal_solution_status_always_set=*/true, + /*dual_solution_status_always_set=*/true))); + +INSTANTIATE_TEST_SUITE_P( + CplexInvalidInputTest, InvalidInputTest, + Values(InvalidInputTestParameters(SolverType::kCplex, + /*use_integer_variables=*/true))); + +SolveParameters StopBeforeOptimal() { + return {.node_limit = 1, + .presolve = Emphasis::kOff, + .cuts = Emphasis::kOff, + .heuristics = Emphasis::kOff}; +} + +SolveParameters CplexLargeInstanceParams() { + return {.presolve = Emphasis::kOff, + .cuts = Emphasis::kOff, + .heuristics = Emphasis::kOff}; +} + +ParameterSupport CplexParameterSupport() { + return {.supports_iteration_limit = true, + .supports_node_limit = true, + .supports_cutoff = true, + .supports_objective_limit = CplexSupportsObjectiveLimit(), + .supports_solution_limit_one = true, + .supports_one_thread = true, + .supports_n_threads = true, + .supports_random_seed = true, + .supports_absolute_gap_tolerance = true, + .supports_lp_algorithm_simplex = true, + .supports_lp_algorithm_barrier = true, + .supports_presolve = true, + .supports_cuts = true, + .supports_heuristics = true, + .supports_scaling = true}; +} + +SolveResultSupport CplexSolveResultSupport() { + return { + .termination_limit = true, .iteration_stats = true, .node_count = true}; +} + +// NOTE: we should also be able to use the LP tests, but many of them don't work +// for CPLEX. +INSTANTIATE_TEST_SUITE_P( + CplexIpParameterTest, IpParameterTest, + Values(IpParameterTestParameters{ + .name = "default", + .solver_type = SolverType::kCplex, + .parameter_support = CplexParameterSupport(), + .hint_supported = false, + .solve_result_support = CplexSolveResultSupport(), + .presolved_regexp = R"regexp(MIP Presolve eliminated)regexp", + .stop_before_optimal = StopBeforeOptimal()}), + ParamName{}); + +INSTANTIATE_TEST_SUITE_P(CplexLargeInstanceIpParameterTest, + LargeInstanceIpParameterTest, + Values(LargeInstanceTestParams{ + .name = "default", + .solver_type = SolverType::kCplex, + .base_parameters = CplexLargeInstanceParams(), + .parameter_support = CplexParameterSupport()}), + ParamName{}); + +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TimeLimitTest); + +InvalidParameterTestParams MakeCplexBadParams() { + SolveParameters parameters; + parameters.cplex.param_bool_values["dog"] = false; + + // TODO(b/168069105): for solver specific errors, we should collect all + // errors, not just the first. Then set int_param "parallel/maxnthreads" to + // -4 (an invalid value). + return InvalidParameterTestParams(SolverType::kCplex, std::move(parameters), + {"CPLEX Error 1028"}); +} + +INSTANTIATE_TEST_SUITE_P(CplexInvalidParameterTest, InvalidParameterTest, + Values(MakeCplexBadParams())); + +INSTANTIATE_TEST_SUITE_P(CplexIpModelSolveParametersTest, + IpModelSolveParametersTest, + Values(SolverType::kCplex)); + +INSTANTIATE_TEST_SUITE_P( + CplexIpMultipleSolutionsTest, IpMultipleSolutionsTest, + Values(IpMultipleSolutionsTestParams(SolverType::kCplex, {}))); + +INSTANTIATE_TEST_SUITE_P( + CplexMessageCallbackTest, MessageCallbackTest, + Values(MessageCallbackTestParams(SolverType::kCplex, + /*support_message_callback=*/true, + /*support_interrupter=*/true, + /*integer_variables=*/true, "Gap", + {.presolve = Emphasis::kOff, + .heuristics = Emphasis::kOff}))); + +// Cplex wrapper does not support lazy constraints at this point. +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(LazyConstraintsTest); + +INSTANTIATE_TEST_SUITE_P( + CplexGenericTest, GenericTest, + Values(GenericTestParameters(SolverType::kCplex, + /*support_interrupter=*/true, + /*integer_variables=*/false, + /*expected_log=*/"[optimal solution found]"), + GenericTestParameters(SolverType::kCplex, + /*support_interrupter=*/true, + /*integer_variables=*/true, + /*expected_log=*/"[optimal solution found]"))); + +TEST(CplexSolverTest, InvalidCoefficient) { + Model model; + const Variable x = model.AddVariable("x"); + model.Maximize(x); + model.AddLinearConstraint(1.0e123 * x <= 2.0, "broken constraint"); + EXPECT_THAT(Solve(model, SolverType::kCplex), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("will be treated as infinite by CPLEX"))); +} + +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(CallbackTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(InfeasibleSubsystemTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MipSolutionHintTest); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(BranchPrioritiesTest); + +} // namespace +} // namespace math_opt +} // namespace operations_research diff --git a/ortools/third_party_solvers/BUILD.bazel b/ortools/third_party_solvers/BUILD.bazel index 7f3fb6351e6..198cbbe4920 100644 --- a/ortools/third_party_solvers/BUILD.bazel +++ b/ortools/third_party_solvers/BUILD.bazel @@ -1,4 +1,4 @@ -# Copyright 2010-2025 Google LLC +# Copyright 2010-2026 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 @@ -59,3 +59,17 @@ cc_library( "@abseil-cpp//absl/synchronization", ], ) + +cc_library( + name = "cplex_environment", + srcs = ["cplex_environment.cc"], + hdrs = ["cplex_environment.h"], + deps = [ + ":dynamic_library", + "@abseil-cpp//absl/base", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:string_view", + ], +) \ No newline at end of file diff --git a/ortools/third_party_solvers/cplex_environment.cc b/ortools/third_party_solvers/cplex_environment.cc new file mode 100644 index 00000000000..51bde0e0b83 --- /dev/null +++ b/ortools/third_party_solvers/cplex_environment.cc @@ -0,0 +1,406 @@ +// Copyright 2010-2026 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. + +#include "ortools/third_party_solvers/cplex_environment.h" + +// NOLINTNEXTLINE(build/c++17) +#include + +#include "absl/base/call_once.h" +#include "absl/log/log.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "ortools/third_party_solvers/dynamic_library.h" + +namespace operations_research { + +// This was generated with the parse_header.py script. +// See the comment at the top of the script. +// Let's not reformat the rest of the file. +// clang-format off + +// This is the 'define' section. + +std::function + CPXopenCPLEX = nullptr; + +std::function + CPXcloseCPLEX = nullptr; + +std::function + CPXcreateprob = nullptr; + +std::function + CPXfreeprob = nullptr; + +std::function + CPXchgprobname = nullptr; + +std::function + CPXnewcols = nullptr; + +std::function + CPXchgrngval = nullptr; + +std::function + CPXlpopt = nullptr; + +std::function + CPXmipopt = nullptr; + +std::function + CPXgetstat = nullptr; + +std::function + CPXgetobjval = nullptr; + +std::function + CPXgetbestobjval = nullptr; + +std::function + CPXgetx = nullptr; + +std::function + CPXgetpi = nullptr; + +std::function + CPXgetdj = nullptr; + +std::function + CPXgetnumcols = nullptr; + +std::function + CPXgetnumrows = nullptr; + +std::function + CPXchgobjsen = nullptr; + +std::function + CPXchgobjoffset = nullptr; + +std::function + CPXchgobj = nullptr; + +std::function + CPXgetprobtype = nullptr; + +std::function + CPXsolninfo = nullptr; + +std::function + CPXgetobjsen = nullptr; + +std::function + CPXchgcoeflist = nullptr; + +std::function + CPXchgbds = nullptr; + +std::function + CPXchgctype = nullptr; + +std::function + CPXgeterrorstring = nullptr; + +std::function + CPXgetitcnt = nullptr; + +std::function + CPXgetmipitcnt = nullptr; + +std::function + CPXgetbaritcnt = nullptr; + +std::function + CPXgetnodecnt = nullptr; + +std::function + CPXgetsolnpoolnumsolns = nullptr; + +std::function + CPXgetsolnpoolobjval = nullptr; + +std::function + CPXgetsolnpoolx = nullptr; + +std::function + CPXsetdblparam = nullptr; + +std::function + CPXsetintparam = nullptr; + +std::function + CPXsetlongparam = nullptr; + +std::function + CPXsetstrparam = nullptr; + +std::function + CPXsetdefaults = nullptr; + +std::function + CPXchgsense = nullptr; + +std::function + CPXchgrhs = nullptr; + +std::function + CPXdelsetcols = nullptr; + +std::function + CPXdelsetrows = nullptr; + +std::function + CPXgetparamnum = nullptr; + +std::function + CPXgetlb = nullptr; + +std::function + CPXgetub = nullptr; + +std::function + CPXsetterminate = nullptr; + +std::function + CPXgetchannels = nullptr; + +std::function + CPXaddfuncdest = nullptr; + +std::function + CPXdelfuncdest = nullptr; + +std::function + CPXnewrows = nullptr; + +std::function + CPXversion = nullptr; + +// ----- + +std::vector CplexDynamicLibraryPotentialPaths() { + std::vector potential_paths; + std::vector kCplexVersions = {"2212", "2211", "2210", "2010"}; + + // Look for libraries pointed by CPLEXDIR first. + const char* cplexdir_from_env = getenv("CPLEXDIR"); + if (cplexdir_from_env != nullptr) { + for (const absl::string_view version : kCplexVersions) { +#if defined(_MSC_VER) // Windows + potential_paths.push_back( + absl::StrCat(cplexdir_from_env, "\\bin\\x64_win64\\cplex", version, ".dll")); +#elif defined(__APPLE__) // macOS +#if defined(__arm64__) || defined(__aarch64__) + potential_paths.push_back( + absl::StrCat(cplexdir_from_env, "/bin/arm64_osx/libcplex", version, ".dylib")); +#endif + potential_paths.push_back( + absl::StrCat(cplexdir_from_env, "/bin/x86-64_osx/libcplex", version, ".dylib")); +#elif defined(__GNUC__) // Linux + potential_paths.push_back( + absl::StrCat(cplexdir_from_env, "/bin/x86-64_linux/libcplex", version, ".so")); +#else + LOG(ERROR) << "OS Not recognized by cplex_environment.cc." + << " You won't be able to use CPLEX."; +#endif + } + } + + // Search for canonical places. + for (const absl::string_view version : kCplexVersions) { + + absl::string_view version_maybe_wo_trailing_0 = version; + if(version.back() == '0') + version_maybe_wo_trailing_0 = version.substr(0, version.size()-1); + +#if defined(_MSC_VER) // Windows + potential_paths.push_back(absl::StrCat( + "C:\\Program Files\\IBM\\ILOG\\CPLEX_Studio", version_maybe_wo_trailing_0, + "\\cplex\\bin\\x64_win64\\cplex", version, ".dll")); +#elif defined(__APPLE__) // macOS +#if defined(__arm64__) || defined(__aarch64__) + potential_paths.push_back(absl::StrCat( + "/Applications/CPLEX_Studio", version_maybe_wo_trailing_0, + "/cplex/bin/arm64_osx/libcplex", version, ".dylib")); +#endif + potential_paths.push_back(absl::StrCat( + "/Applications/CPLEX_Studio", version_maybe_wo_trailing_0, + "/cplex/bin/x86-64_osx/libcplex", version, ".dylib")); +#elif defined(__GNUC__) // Linux + potential_paths.push_back(absl::StrCat( + "/opt/ibm/ILOG/CPLEX_Studio", version_maybe_wo_trailing_0, + "/cplex/bin/x86-64_linux/libcplex", version, ".so")); +#else + LOG(ERROR) << "OS Not recognized by cplex_environment.cc." + << " You won't be able to use CPLEX."; +#endif + } + return potential_paths; +} + +void LoadCplexFunctions(DynamicLibrary* cplex_dynamic_library) { + + // This is the 'assign' section. + + cplex_dynamic_library->GetFunction(&CPXopenCPLEX, + "CPXopenCPLEX"); + cplex_dynamic_library->GetFunction(&CPXcloseCPLEX, + "CPXcloseCPLEX"); + cplex_dynamic_library->GetFunction(&CPXcreateprob, + "CPXcreateprob"); + cplex_dynamic_library->GetFunction(&CPXfreeprob, + "CPXfreeprob"); + cplex_dynamic_library->GetFunction(&CPXchgprobname, + "CPXchgprobname"); + cplex_dynamic_library->GetFunction(&CPXnewcols, + "CPXnewcols"); + cplex_dynamic_library->GetFunction(&CPXchgrngval, + "CPXchgrngval"); + cplex_dynamic_library->GetFunction(&CPXlpopt, + "CPXlpopt"); + cplex_dynamic_library->GetFunction(&CPXmipopt, + "CPXmipopt"); + cplex_dynamic_library->GetFunction(&CPXgetstat, + "CPXgetstat"); + cplex_dynamic_library->GetFunction(&CPXgetobjval, + "CPXgetobjval"); + cplex_dynamic_library->GetFunction(&CPXgetbestobjval, + "CPXgetbestobjval"); + cplex_dynamic_library->GetFunction(&CPXgetx, + "CPXgetx"); + cplex_dynamic_library->GetFunction(&CPXgetpi, + "CPXgetpi"); + cplex_dynamic_library->GetFunction(&CPXgetdj, + "CPXgetdj"); + cplex_dynamic_library->GetFunction(&CPXgetnumcols, + "CPXgetnumcols"); + cplex_dynamic_library->GetFunction(&CPXgetnumrows, + "CPXgetnumrows"); + cplex_dynamic_library->GetFunction(&CPXchgobjsen, + "CPXchgobjsen"); + cplex_dynamic_library->GetFunction(&CPXchgobjoffset, + "CPXchgobjoffset"); + cplex_dynamic_library->GetFunction(&CPXchgobj, + "CPXchgobj"); + cplex_dynamic_library->GetFunction(&CPXgetprobtype, + "CPXgetprobtype"); + cplex_dynamic_library->GetFunction(&CPXsolninfo, + "CPXsolninfo"); + cplex_dynamic_library->GetFunction(&CPXgetobjsen, + "CPXgetobjsen"); + cplex_dynamic_library->GetFunction(&CPXchgcoeflist, + "CPXchgcoeflist"); + cplex_dynamic_library->GetFunction(&CPXchgbds, + "CPXchgbds"); + cplex_dynamic_library->GetFunction(&CPXchgctype, + "CPXchgctype"); + cplex_dynamic_library->GetFunction(&CPXgeterrorstring, + "CPXgeterrorstring"); + cplex_dynamic_library->GetFunction(&CPXgetitcnt, + "CPXgetitcnt"); + cplex_dynamic_library->GetFunction(&CPXgetmipitcnt, + "CPXgetmipitcnt"); + cplex_dynamic_library->GetFunction(&CPXgetbaritcnt, + "CPXgetbaritcnt"); + cplex_dynamic_library->GetFunction(&CPXgetnodecnt, + "CPXgetnodecnt"); + cplex_dynamic_library->GetFunction(&CPXgetsolnpoolnumsolns, + "CPXgetsolnpoolnumsolns"); + cplex_dynamic_library->GetFunction(&CPXgetsolnpoolobjval, + "CPXgetsolnpoolobjval"); + cplex_dynamic_library->GetFunction(&CPXgetsolnpoolx, + "CPXgetsolnpoolx"); + cplex_dynamic_library->GetFunction(&CPXsetdblparam, + "CPXsetdblparam"); + cplex_dynamic_library->GetFunction(&CPXsetintparam, + "CPXsetintparam"); + cplex_dynamic_library->GetFunction(&CPXsetlongparam, + "CPXsetlongparam"); + cplex_dynamic_library->GetFunction(&CPXsetstrparam, + "CPXsetstrparam"); + cplex_dynamic_library->GetFunction(&CPXsetdefaults, + "CPXsetdefaults"); + cplex_dynamic_library->GetFunction(&CPXchgsense, + "CPXchgsense"); + cplex_dynamic_library->GetFunction(&CPXchgrhs, + "CPXchgrhs"); + cplex_dynamic_library->GetFunction(&CPXdelsetcols, + "CPXdelsetcols"); + cplex_dynamic_library->GetFunction(&CPXdelsetrows, + "CPXdelsetrows"); + cplex_dynamic_library->GetFunction(&CPXgetparamnum, + "CPXgetparamnum"); + cplex_dynamic_library->GetFunction(&CPXgetlb, + "CPXgetlb"); + cplex_dynamic_library->GetFunction(&CPXgetub, + "CPXgetub"); + cplex_dynamic_library->GetFunction(&CPXsetterminate, + "CPXsetterminate"); + cplex_dynamic_library->GetFunction(&CPXgetchannels, + "CPXgetchannels"); + cplex_dynamic_library->GetFunction(&CPXaddfuncdest, + "CPXaddfuncdest"); + cplex_dynamic_library->GetFunction(&CPXdelfuncdest, + "CPXdelfuncdest"); + cplex_dynamic_library->GetFunction(&CPXnewrows, + "CPXnewrows"); + cplex_dynamic_library->GetFunction(&CPXversion, + "CPXversion"); +} + +absl::Status LoadCplexDynamicLibrary(std::string& cplexpath) { + static std::string* cplex_lib_path = new std::string; + static absl::once_flag cplex_loading_done; + static absl::Status* cplex_load_status = new absl::Status; + static DynamicLibrary* cplex_library = new DynamicLibrary; + static absl::Mutex mutex(absl::kConstInit); + + absl::MutexLock lock(&mutex); + + absl::call_once(cplex_loading_done, []() { + const std::vector canonical_paths = + CplexDynamicLibraryPotentialPaths(); + for (const std::string& path : canonical_paths) { + if (cplex_library->TryToLoad(path)) { + VLOG(1) << "Found the CPLEX library in " << path << "."; + cplex_lib_path->clear(); + std::filesystem::path p(path); + p.remove_filename(); + cplex_lib_path->append(p.string()); + break; + } + } + + if (cplex_library->LibraryIsLoaded()) { + VLOG(1) << "Loading all CPLEX functions"; + LoadCplexFunctions(cplex_library); + *cplex_load_status = absl::OkStatus(); + } + else { + *cplex_load_status = absl::NotFoundError( + absl::StrCat("Could not find the CPLEX shared library. Looked in: [", + absl::StrJoin(canonical_paths, "', '"), + "]. Please check environment variable CPLEXDIR")); + } + }); + cplexpath.clear(); + cplexpath.append(*cplex_lib_path); + return *cplex_load_status; +} + +} // namespace operations_research diff --git a/ortools/third_party_solvers/cplex_environment.h b/ortools/third_party_solvers/cplex_environment.h new file mode 100644 index 00000000000..f1f28013cb2 --- /dev/null +++ b/ortools/third_party_solvers/cplex_environment.h @@ -0,0 +1,337 @@ +// Copyright 2010-2026 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. + +#ifndef ORTOOLS_THIRD_PARTY_SOLVERS_CPLEX_ENVIRONMENT_H_ +#define ORTOOLS_THIRD_PARTY_SOLVERS_CPLEX_ENVIRONMENT_H_ + +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/string_view.h" + +namespace operations_research { + +#ifndef CPXPUBLIC +#ifdef _WIN32 +#define CPXPUBLIC __stdcall +#else +#define CPXPUBLIC +#endif +#endif + +typedef const char* CPXCCHARptr; /* to simplify CPXPUBLIC syntax */ + +#define CPX_BIGINT 2100000000 + +#define CPX_INFBOUND 1.0E+20 +#define CPX_MINBOUND 1.0E-13 + +#define CPX_MIN 1 +#define CPX_MAX -1 + +#define CPX_NO_SOLN 0 +#define CPX_BASIC_SOLN 1 +#define CPX_NONBASIC_SOLN 2 +#define CPX_PRIMAL_SOLN 3 + +// Solution Status Symbols +// ----------------------- +#define CPX_STAT_ABORT_DETTIME_LIM 25 +#define CPX_STAT_ABORT_DUAL_OBJ_LIM 22 +#define CPX_STAT_ABORT_IT_LIM 10 +#define CPX_STAT_ABORT_OBJ_LIM 12 +#define CPX_STAT_ABORT_PRIM_OBJ_LIM 21 +#define CPX_STAT_ABORT_TIME_LIM 11 +#define CPX_STAT_ABORT_USER 13 +#define CPX_STAT_BENDERS_MASTER_UNBOUNDED 40 +#define CPX_STAT_BENDERS_NUM_BEST 41 +#define CPX_STAT_CONFLICT_ABORT_CONTRADICTION 32 +#define CPX_STAT_CONFLICT_ABORT_DETTIME_LIM 39 +#define CPX_STAT_CONFLICT_ABORT_IT_LIM 34 +#define CPX_STAT_CONFLICT_ABORT_MEM_LIM 37 +#define CPX_STAT_CONFLICT_ABORT_NODE_LIM 35 +#define CPX_STAT_CONFLICT_ABORT_OBJ_LIM 36 +#define CPX_STAT_CONFLICT_ABORT_TIME_LIM 33 +#define CPX_STAT_CONFLICT_ABORT_USER 38 +#define CPX_STAT_CONFLICT_FEASIBLE 30 +#define CPX_STAT_CONFLICT_MINIMAL 31 +#define CPX_STAT_FEASIBLE 23 +#define CPX_STAT_FEASIBLE_RELAXED_INF 16 +#define CPX_STAT_FEASIBLE_RELAXED_QUAD 18 +#define CPX_STAT_FEASIBLE_RELAXED_SUM 14 +#define CPX_STAT_FIRSTORDER 24 +#define CPX_STAT_INFEASIBLE 3 +#define CPX_STAT_INForUNBD 4 +#define CPX_STAT_MULTIOBJ_INFEASIBLE 302 +#define CPX_STAT_MULTIOBJ_INForUNBD 303 +#define CPX_STAT_MULTIOBJ_NON_OPTIMAL 305 +#define CPX_STAT_MULTIOBJ_OPTIMAL 301 +#define CPX_STAT_MULTIOBJ_STOPPED 306 +#define CPX_STAT_MULTIOBJ_UNBOUNDED 304 +#define CPX_STAT_NUM_BEST 6 +#define CPX_STAT_OPTIMAL 1 +#define CPX_STAT_OPTIMAL_FACE_UNBOUNDED 20 +#define CPX_STAT_OPTIMAL_INFEAS 5 +#define CPX_STAT_OPTIMAL_RELAXED_INF 17 +#define CPX_STAT_OPTIMAL_RELAXED_QUAD 19 +#define CPX_STAT_OPTIMAL_RELAXED_SUM 15 +#define CPX_STAT_UNBOUNDED 2 + +// MIP prefix +#define CPXMIP_ABORT_FEAS 113 +#define CPXMIP_ABORT_INFEAS 114 +#define CPXMIP_ABORT_RELAXATION_UNBOUNDED 133 +#define CPXMIP_ABORT_RELAXED 126 +#define CPXMIP_DETTIME_LIM_FEAS 131 +#define CPXMIP_DETTIME_LIM_INFEAS 132 +#define CPXMIP_FAIL_FEAS 109 +#define CPXMIP_FAIL_FEAS_NO_TREE 116 +#define CPXMIP_FAIL_INFEAS 110 +#define CPXMIP_FAIL_INFEAS_NO_TREE 117 +#define CPXMIP_FEASIBLE 127 +#define CPXMIP_FEASIBLE_RELAXED_INF 122 +#define CPXMIP_FEASIBLE_RELAXED_QUAD 124 +#define CPXMIP_FEASIBLE_RELAXED_SUM 120 +#define CPXMIP_INFEASIBLE 103 +#define CPXMIP_INForUNBD 119 +#define CPXMIP_MEM_LIM_FEAS 111 +#define CPXMIP_MEM_LIM_INFEAS 112 +#define CPXMIP_NODE_LIM_FEAS 105 +#define CPXMIP_NODE_LIM_INFEAS 106 +#define CPXMIP_OPTIMAL 101 +#define CPXMIP_OPTIMAL_INFEAS 115 +#define CPXMIP_OPTIMAL_POPULATED 129 +#define CPXMIP_OPTIMAL_POPULATED_TOL 130 +#define CPXMIP_OPTIMAL_RELAXED_INF 123 +#define CPXMIP_OPTIMAL_RELAXED_QUAD 125 +#define CPXMIP_OPTIMAL_RELAXED_SUM 121 +#define CPXMIP_OPTIMAL_TOL 102 +#define CPXMIP_POPULATESOL_LIM 128 +#define CPXMIP_SOL_LIM 104 +#define CPXMIP_TIME_LIM_FEAS 107 +#define CPXMIP_TIME_LIM_INFEAS 108 +#define CPXMIP_UNBOUNDED 118 + +#define CPXPROB_LP 0 +#define CPXPROB_MILP 1 +#define CPXPROB_FIXEDMILP 3 +#define CPXPROB_NODELP 4 +#define CPXPROB_QP 5 +#define CPXPROB_MIQP 7 +#define CPXPROB_FIXEDMIQP 8 +#define CPXPROB_NODEQP 9 +#define CPXPROB_QCP 10 +#define CPXPROB_MIQCP 11 +#define CPXPROB_NODEQCP 12 + +#define CPXPARAM_Simplex_Limits_LowerObj 1025 +#define CPXPARAM_Simplex_Limits_UpperObj 1026 +#define CPXPARAM_Read_Scale 1034 +#define CPXPARAM_ScreenOutput 1035 +#define CPXPARAM_TimeLimit 1039 +#define CPXPARAM_LPMethod 1062 +#define CPXPARAM_Threads 1067 +// #define CPXPARAM_Emphasis_Numerical 1083 +#define CPXPARAM_RandomSeed 1124 +#define CPXPARAM_ParamDisplay 1163 +#define CPXPARAM_MIP_Tolerances_LowerCutoff 2006 +#define CPXPARAM_MIP_Tolerances_UpperCutoff 2007 +#define CPXPARAM_MIP_Tolerances_AbsMIPGap 2008 +#define CPXPARAM_MIP_Tolerances_MIPGap 2009 +#define CPXPARAM_MIP_Limits_Solutions 2015 +#define CPXPARAM_MIP_Limits_Nodes 2017 +#define CPXPARAM_MIP_Pool_Capacity 2103 + +#define CPX_ALG_PRIMAL 1 +#define CPX_ALG_DUAL 2 +#define CPX_ALG_NET 3 +#define CPX_ALG_BARRIER 4 + +#define CPXPARAM_MIP_Cuts_BQP 2195 +#define CPXPARAM_MIP_Cuts_Cliques 2003 +#define CPXPARAM_MIP_Cuts_Covers 2005 +#define CPXPARAM_MIP_Cuts_Disjunctive 2053 +#define CPXPARAM_MIP_Cuts_FlowCovers 2040 +#define CPXPARAM_MIP_Cuts_PathCut 2051 +#define CPXPARAM_MIP_Cuts_Gomory 2049 +#define CPXPARAM_MIP_Cuts_GUBCovers 2044 +#define CPXPARAM_MIP_Cuts_Implied 2041 +#define CPXPARAM_MIP_Cuts_LocalImplied 2181 +#define CPXPARAM_MIP_Cuts_LiftProj 2152 +#define CPXPARAM_MIP_Cuts_MIRCut 2052 +#define CPXPARAM_MIP_Cuts_ZeroHalfCut 2111 +#define CPXPARAM_MIP_Cut_MCFCut 2134 +#define CPXPARAM_MIP_Cuts_Nodecuts 2157 +#define CPXPARAM_MIP_Cuts_RLT 2196 + +#define CPXPARAM_MIP_Strategy_HeuristicEffort 2120 + +#define CPXPARAM_Preprocessing_Presolve 1030 +#define CPXPARAM_MIP_Strategy_Probe 2042 +#define CPXPARAM_Preprocessing_RepeatPresolve 2064 + +#define CPXPARAM_Simplex_Limits_Iterations 1020 +#define CPXPARAM_Barrier_Limits_Iteration 3012 + +#define CPXERR_SUBPROB_SOLVE 3019 + +#define CPXMESSAGEBUFSIZE 1024 + +#define CPX_ON 1 +#define CPX_OFF 0 + +#ifndef CPXLONG_DEFINED +#define CPXLONG_DEFINED 1 +#ifdef _MSC_VER +typedef __int64 CPXLONG; +#else +typedef long long CPXLONG; +#endif +#endif + +// *************************************************************************** +// * variable types * +// *************************************************************************** +#define CPX_CONTINUOUS 'C' +#define CPX_INTEGER 'I' + +struct cpxenv; +typedef struct cpxenv* CPXENVptr; +typedef struct cpxenv const* CPXCENVptr; + +struct cpxlp; +typedef struct cpxlp* CPXLPptr; +#ifndef CPXCLPptr +typedef const struct cpxlp* CPXCLPptr; +#endif + +struct cpxchannel; +typedef struct cpxchannel* CPXCHANNELptr; + +// Force the loading of the cplex dynamic library. It returns true if the +// library was successfully loaded. This method can only be called once. +// Successive calls are no-op. +absl::Status LoadCplexDynamicLibrary(std::string& cplexpath); + +// clang-format off + +extern std::function CPXopenCPLEX; +extern std::function CPXcloseCPLEX; + +extern std::function CPXcreateprob; +extern std::function CPXfreeprob; + +extern std::function CPXchgprobname; + +extern std::function CPXnewcols; + +extern std::function CPXchgrngval; + +extern std::function CPXlpopt; + +extern std::function CPXmipopt; + +extern std::function CPXgetstat; + +extern std::function CPXgetobjval; + +extern std::function CPXgetbestobjval; + +extern std::function CPXgetx; + +extern std::function CPXgetpi; + +extern std::function CPXgetdj; + +extern std::function CPXgetnumcols; + +extern std::function CPXgetnumrows; + +extern std::function CPXchgobjsen; + +extern std::function CPXchgobjoffset; + +extern std::function CPXchgobj; + +extern std::function CPXgetprobtype; + +extern std::function CPXsolninfo; + +extern std::function CPXgetobjsen; + +extern std::function CPXchgcoeflist; + +extern std::function CPXchgbds; + +extern std::function CPXchgctype; + +extern std::function CPXgeterrorstring; + +extern std::function CPXgetitcnt; + +extern std::function CPXgetmipitcnt; + +extern std::function CPXgetbaritcnt; + +extern std::function CPXgetnodecnt; + +extern std::function CPXgetsolnpoolnumsolns; + +extern std::function CPXgetsolnpoolobjval; + +extern std::function CPXgetsolnpoolx; + +extern std::function CPXsetdblparam; + +extern std::function CPXsetintparam; + +extern std::function CPXsetlongparam; + +extern std::function CPXsetstrparam; + +extern std::function CPXsetdefaults; + +extern std::function CPXchgsense; + +extern std::function CPXchgrhs; + +extern std::function CPXdelsetcols; + +extern std::function CPXdelsetrows; + +extern std::function CPXgetparamnum; + +extern std::function CPXgetlb; + +extern std::function CPXgetub; + +extern std::function CPXsetterminate; + +extern std::function CPXgetchannels; + +extern std::function CPXaddfuncdest; + +extern std::function CPXdelfuncdest; + +extern std::function CPXnewrows; + +extern std::function CPXversion; + +// clang-format on +} // namespace operations_research + +#endif // ORTOOLS_THIRD_PARTY_SOLVERS_CPLEX_ENVIRONMENT_H_