Skip to content

math-opt: add CPLEX solver backend#5125

Open
sschnug wants to merge 21 commits intogoogle:mainfrom
sschnug:math_opt_add_cplex_support
Open

math-opt: add CPLEX solver backend#5125
sschnug wants to merge 21 commits intogoogle:mainfrom
sschnug:math_opt_add_cplex_support

Conversation

@sschnug
Copy link
Copy Markdown

@sschnug sschnug commented Apr 5, 2026

UPDATE: 14.04.26

  • VALIDATED Windows/MSVC/CMake ➡️ python-wheel ➡️ basic_example.py

This PR adds support for the commercial solver IBM CPLEX in MathOpt, following the discussion in #4748.

Note for reviewers: This PR was initially carefully structured into 5 clean, logically separated commits. After that, the size and impact of individual commits has been decreased. While the original recommendation would have been unsquashed, this is debatable right now.

Scope

This is an initial implementation targeting the most common LP/MIP workflows. CPLEX supports more features than this PR currently exposes — extending coverage is planned as future work.

Supported

  • Linear programming (LP)
  • Mixed-integer programming (MIP)
  • Incremental solves
  • Message callbacks
  • Solve interruption
  • Solution pool
  • Solver-specific parameters
  • Dynamic library loading (Linux, macOS, Windows) for CPLEX versions 22.1.2, 22.1.1, 22.1.0, and 20.1.0
  • Python bindings
  • Bazel and CMake build systems

Not yet supported

  • Indicator constraints
  • SOS constraints
  • Solution hints (MIP starts)
  • Initial basis
  • Branching priorities
  • Lazy linear constraints
  • Solver callbacks
  • Infeasible subsystem (IIS) computation
  • Primal / dual ray extraction
  • Multiple objectives
  • Quadratic objectives and constraints
  • Second-order cone constraints
  • Java bindings

Caveats / Unsupported by CPLEX

  • best_bound_limit solve parameter (explicitly rejected)
  • FIRST_ORDER LP algorithm (explicitly rejected)
  • objective_limit requires CPLEX 21.1.0 or newer (will return an error for CPLEX 20.1.0)

Implementation

Architecture

The implementation follows the existing three-layer pattern used by Gurobi, GScip, Xpress et al.:

Layer Files Purpose
1 — Dynamic loader third_party_solvers/cplex_environment.{h,cc} dlopen-based loading with filesystem search
2 — RAII wrapper solvers/cplex/g_cplex.{h,cc} Safe C++ wrapper around CPLEX's C API
3 — MathOpt interface solvers/cplex_solver.{h,cc} Solver backend implementing SolverInterface

Additionally: solvers/cplex.proto for solver-specific parameters.

The solver layer is intentionally structured (and ordered) like the Gurobi implementation, enabling diff-based comparison when adding features in the future.

Notable differences to other solver backend implementations

Unlike Gurobi, gSCIP, and Xpress, CPLEX requires message callbacks (channels) to be explicitly detached and screen output (CPXPARAM_ScreenOutput) disabled before extracting solve results.

This workaround is necessary because, unlike Gurobi—which provides a mechanism to check if an attribute like node_count is available before querying it (e.g., IsAttrAvailable)—CPLEX will emit internal diagnostic errors (e.g., "CPLEX Error 1217: No solution exists.") to the message callback and stderr when querying statistics that happen to be inapplicable to the current solve state.

While there is likely a way to avoid these errors in a perfect world by querying only valid attributes, doing so reliably would require reverse-engineering CPLEX's internal state machine to know exactly which attributes are valid at any given time. Disabling output during the extraction phase ensures that valid solve-time messages are captured properly, while cleanly suppressing these noisy, expected errors during result extraction.

CMake note on USE_CPLEX

Unlike Gurobi and Xpress, the legacy linear_solver backend for CPLEX (cplex_interface.cc) was never ported to dynamic loading. It still links directly against the CPLEX library and requires CPLEX headers at build time. Thus, the CMake flag USE_CPLEX=ON implies a hard build-time dependency on CPLEX.

MathOpt's new CPLEX backend, however, does use dynamic loading (dlopen/LoadLibrary) and requires no build-time dependencies. To ensure users can build and use MathOpt's CPLEX backend without needing the CPLEX headers required by linear_solver, the MathOpt CPLEX wrappers are always compiled unconditionally. They are intentionally not gated by USE_CPLEX in math_opt/solvers/CMakeLists.txt (this differs from USE_GUROBI and USE_XPRESS, which do gate their respective MathOpt wrappers because their linear_solver counterparts also use dynamic loading and don't force header dependencies).

The CPLEX test target (cplex_solver_test.cc), however, is still gated by USE_CPLEX=ON since the tests require a functional CPLEX installation to pass.

Testing

Shared test-suite bug fixes

While adapting the shared test infrastructure, three pre-existing bugs were discovered and fixed:

  1. Use-after-free in LPForIterationLimit / SolveForGapLimit: Model created on the stack inside a helper, SolveResult returned with dangling references. Fix: caller owns the Model, helper takes Model&.
  2. Missing guard in BestBoundLimitMinimize: test ran unconditionally but requires supports_best_bound_limit.
  3. Lost parameters in IncompleteIpSolve: SolveArguments was constructed from scratch, discarding GetParam().parameters (solver-specific settings).

CPLEX test coverage

All applicable shared test suites are instantiated in cplex_solver_test.cc. Several tests require CPLEX-specific skip guards due to solver behavior quirks on small models (e.g., presolver solving before barrier starts, zero-iteration warm re-solves, root-only node counts reported as 0). Each exception is documented inline.

Validation performed

Method Result
Bazel --config=ci (all math_opt + third_party_solvers) All tests pass (with a few expected solver-specific skips)
Bazel --config=asan (all math_opt + third_party_solvers) All tests pass (with a few expected solver-specific skips)
CMake USE_CPLEX=ON (CPLEX test binary) All CPLEX tests pass (expected skips for unsupported features)
CMake USE_CPLEX=OFF Configures and builds cleanly
CMake Python Wheel (Win 👍 Linux 👍 Mac ❓) Manual test successful

Note: MacOS CPLEX dynamic library search paths have not been validated (author currently lacks access to this environment but is working on it).

Future work

Higher priority (author's personal opinion)

  • Warm-starting (MIP starts via CPXaddmipstarts)
  • Solver callbacks (Intermediate result acquisition important in industrial applications)
  • Convex quadratic (mixed-integer) programming

Lower priority (author's personal opinion)

  • Code generation for cplex_environment.{h,cc} (CPLEX API is stable, manual approach is adequate for now)

Unrelated follow-up work

  • linear_solver/cplex_interface.cc uses removed ortools/base/logging.h — does not compile with USE_CPLEX=ON
    • Looks like some core changes haven't been synced here
    • Might actually be a merge/release-blocker as python wheels are built using CMake
  • init_arguments.py is missing Xpress Python init argument wiring (XpressInitializerProto.extract_names cannot be set)

sschnug added 21 commits April 3, 2026 17:03
This commit adds dynamic loading support for CPLEX following the Gurobi/Xpress pattern.
Filesystem search-paths are provided for Win/Linux/Mac. Additionally CPLEXDIR env-var is given higher priority.
CPLEX versions supported are 20.10, 22.10, 22.11 and 22.12.

These files have been manually ported and there is no corresponding code-generation script available for now.
This commit adds layer 2 and 3 of CPLEX-specific code following the three-layer approach of other solvers:

- layer 1: dynamic loader (previous commit) -> cplex_environment.h/cc
- layer 2: RAII wrapper -> g_cplex.h/cc
- layer 3: math-opt interface -> cplex_solver.h/cc

Additionally: add cplex-proto for solver-specific parameters.

This commit is NOT self-contained and won't compile without follow-up commits related to solver-registration.
Adds SOLVER_TYPE_CPLEX to parameters.proto, CplexParameters C++ struct,
and Python bindings (SolverType.CPLEX, SolveParameters, init_arguments).

CMakeLists.txt did NOT get any USE_CPLEX filter as this math-opt work is based on dynamic-loading and
should not be disabled on systems missing CPLEX install. This is different from other solvers due to
NOT porting cplex in the legacy linear_solver framework to dynamic loading (like gurobi did).
… missing supports_best_bound_limit check + discard of solver-specific params

Test-suite creates Model model on stack, builds the model, calls Solve(), and returns
SolveResult. The SolveResult contains references to variable/constraint data owned by the Model.
When the function returns, Model is destroyed → dangling references. ASAN catches this.
The fix: Move Model to each caller's scope so it outlives SolveResult. The function takes Model& instead.
Due to some "special" solver-behaviour we need to add some cplex-based
branching to some of those tests as already being done by other solvers
although CPLEX is now (sadly) leading in this regard.
Those exceptions are documented in every case.
This solves three issues:x
- high prio: use-after-free on partial channel attachment failure
- medium prio: duplicate output when message callback is registered
- low prio: CPXPARAM_ParamDisplay leaks across Solve() calls
…thon API (proto/c++ uses int32/int64 for safety)
@Mizux Mizux self-assigned this Apr 9, 2026
@Mizux Mizux added Feature Request Missing Feature/Wrapper Solver: CPLEX CPLEX Solver related issue Solver: MathOpt MathOpt related issue Build: CMake CMake based build issue Build: Bazel Bazel based build issue labels Apr 9, 2026
@Mizux Mizux added this to the v10.0 milestone Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Build: Bazel Bazel based build issue Build: CMake CMake based build issue Feature Request Missing Feature/Wrapper Solver: CPLEX CPLEX Solver related issue Solver: MathOpt MathOpt related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants