diff --git a/ortools/java/com/google/ortools/mathopt/SolverType.java b/ortools/java/com/google/ortools/mathopt/SolverType.java
index 0c8520cb66..769466ae77 100644
--- a/ortools/java/com/google/ortools/mathopt/SolverType.java
+++ b/ortools/java/com/google/ortools/mathopt/SolverType.java
@@ -137,7 +137,15 @@ public enum SolverType {
*
The objective must be linear.
*
*/
- MIN_COST_FLOW(SolverTypeProto.SOLVER_TYPE_MIN_COST_FLOW);
+ MIN_COST_FLOW(SolverTypeProto.SOLVER_TYPE_MIN_COST_FLOW),
+
+ /**
+ * IBM ILOG CPLEX solver (third party).
+ *
+ * Supports LP and MIP problems (other types are unimplemented). A fast option, but has special
+ * licensing.
+ */
+ CPLEX(SolverTypeProto.SOLVER_TYPE_CPLEX);
private static class ProtoMap {
private static final EnumMap map =
diff --git a/ortools/math_opt/BUILD.bazel b/ortools/math_opt/BUILD.bazel
index d35358a68c..5b12f7695e 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 6c73fdef5c..6ee60e659a 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
@@ -90,6 +90,8 @@ std::optional Enum::ToOptString(
return "xpress";
case SolverType::kMinCostFlow:
return "min_cost_flow";
+ case SolverType::kCplex:
+ return "cplex";
}
return std::nullopt;
}
@@ -100,6 +102,7 @@ absl::Span Enum::AllValues() {
SolverType::kCpSat, SolverType::kPdlp, SolverType::kGlpk,
SolverType::kEcos, SolverType::kScs, SolverType::kHighs,
SolverType::kSantorini, SolverType::kXpress, SolverType::kMinCostFlow,
+ SolverType::kCplex,
};
return absl::MakeConstSpan(kSolverTypeValues);
}
@@ -240,6 +243,59 @@ absl::StatusOr 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);
@@ -293,6 +349,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;
}
@@ -355,6 +412,7 @@ absl::StatusOr SolveParameters::FromProto(
OR_ASSIGN_OR_RETURN3(result.xpress,
XpressParameters::FromProto(proto.xpress()),
_ << "invalid xpress parameters");
+ 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 42bf373171..00f0fd59cd 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
@@ -133,6 +134,12 @@ enum class SolverType {
// * All variables and constraints must have integer bounds and costs.
// * The objective must be linear.
kMinCostFlow = SOLVER_TYPE_MIN_COST_FLOW,
+
+ // 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);
@@ -312,6 +319,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
@@ -486,6 +510,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 e4bcc73ddc..e765f9663b 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
@@ -20,6 +20,7 @@ import "google/protobuf/duration.proto";
import "ortools/glop/parameters.proto";
import "ortools/math_opt/solvers/gscip/gscip.proto";
import "ortools/pdlp/solvers.proto";
+import "ortools/math_opt/solvers/cplex.proto";
import "ortools/math_opt/solvers/glpk.proto";
import "ortools/math_opt/solvers/gurobi.proto";
import "ortools/math_opt/solvers/highs.proto";
@@ -133,6 +134,12 @@ enum SolverTypeProto {
// * All variables and constraints must have integer bounds and costs.
// * The objective must be linear.
SOLVER_TYPE_MIN_COST_FLOW = 14;
+
+ // IBM ILOG CPLEX solver (third party).
+ //
+ // Supports LP, MIP. (other problem-types are unimplemented)
+ // A fast option, but has special licensing.
+ SOLVER_TYPE_CPLEX = 15;
}
// Selects an algorithm for solving linear programs.
@@ -190,6 +197,7 @@ message SolverInitializerProto {
GurobiInitializerProto gurobi = 1;
reserved 2;
XpressInitializerProto xpress = 3;
+ CplexInitializerProto cplex = 4;
}
// Parameters to control a single solve.
@@ -392,5 +400,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 789d445986..e4e142fcff 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 b649432bdc..a67c30aee1 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 96442f1772..491c8a5d94 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
@@ -91,6 +91,8 @@ class SolverType(enum.Enum):
* All variable lower bounds must be 0.
* All variables and constraints must have integer bounds and costs.
* The objective must be linear.
+ CPLEX: IBM ILOG CPLEX solver (third party). Supports LP, MIP. (other
+ problem-types are unimplemented) A fast option, but has special licensing.
""" # fmt: skip
GSCIP = math_opt_parameters_pb2.SOLVER_TYPE_GSCIP
@@ -106,6 +108,7 @@ class SolverType(enum.Enum):
SANTORINI = math_opt_parameters_pb2.SOLVER_TYPE_SANTORINI
XPRESS = math_opt_parameters_pb2.SOLVER_TYPE_XPRESS
MIN_COST_FLOW = math_opt_parameters_pb2.SOLVER_TYPE_MIN_COST_FLOW
+ CPLEX = math_opt_parameters_pb2.SOLVER_TYPE_CPLEX
def solver_type_from_proto(
@@ -308,6 +311,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.
@@ -418,6 +515,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
@@ -461,6 +559,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."""
@@ -480,6 +581,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)
@@ -528,6 +630,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 b7cb67e592..fbf109a01e 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 1defe8b6f0..4ecc6f9038 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 660ce25f5e..2aa8be2302 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);
}
}
@@ -479,7 +483,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) {
@@ -496,7 +504,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);
+ }
}
}
@@ -567,7 +581,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.
}
@@ -686,10 +705,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());
@@ -725,9 +744,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);
@@ -749,8 +769,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,
@@ -763,7 +784,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);
}
@@ -792,9 +816,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);
@@ -816,15 +841,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);
}
@@ -875,9 +904,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 5000ebe315..0458ac307a 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 62cdb2f32e..d51ffd14d4 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
@@ -1185,9 +1185,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).
@@ -1204,9 +1210,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
@@ -1219,9 +1228,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);
@@ -1231,9 +1243,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 267281d9bf..9d6945a6b8 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 408ffa6466..450cb09652 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
@@ -796,6 +796,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 = [
@@ -871,3 +888,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/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:status_macros",
+ "@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 540dcd7eec..5ba3c85c23 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
@@ -64,6 +64,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 0000000000..d7b8c37920
--- /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 0000000000..1af43af113
--- /dev/null
+++ b/ortools/math_opt/solvers/cplex/BUILD.bazel
@@ -0,0 +1,43 @@
+# 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:status_builder",
+ "//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:status_macros",
+ "@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 0000000000..216726beed
--- /dev/null
+++ b/ortools/math_opt/solvers/cplex/g_cplex.cc
@@ -0,0 +1,593 @@
+// 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/status_macros.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/status_builder.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;
+ ABSL_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) {
+ ABSL_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() {
+ ABSL_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() {
+ ABSL_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() {
+ ABSL_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) {
+ ABSL_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) {
+ ABSL_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 0000000000..e050b7de9b
--- /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 0000000000..e64811bb92
--- /dev/null
+++ b/ortools/math_opt/solvers/cplex_solver.cc
@@ -0,0 +1,1967 @@
+// 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/status_macros.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/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) {
+ ABSL_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()) {
+ ABSL_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 ortools::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