Choosing a solver¶
Every backend exposes the same modeling interface, so the choice of solver is a two-line change. This page lists the available backends, how to switch between them, and what each one supports.
The backends¶
Each backend lives in mippp/solvers/<name>/all.hpp and provides an api class plus one model class per problem kind. The last column is the range of solver releases each binding has been validated on, see Solver version compatibility.
| Solver | Header (mippp/solvers/…) |
API class | Model classes | Validated releases |
|---|---|---|---|---|
| HiGHS | highs/all.hpp |
highs_api |
highs_lp, highs_milp, highs_qp |
1.8.1 – 1.15 |
| Gurobi | gurobi/all.hpp |
gurobi_api |
gurobi_lp, gurobi_milp |
10 – 13 |
| CPLEX | cplex/all.hpp |
cplex_api |
cplex_lp, cplex_milp |
22.1.0 – 22.2.0 |
| FICO Xpress | xpress/all.hpp |
xpress_api |
xpress_lp, xpress_milp |
45.1 – 47.1 (local runs) |
| COPT | copt/all.hpp |
copt_api |
copt_lp, copt_milp |
7.2 – 8.0 (local runs) |
| MOSEK | mosek/all.hpp |
mosek_api |
mosek_lp, mosek_milp |
11.0 (local run) |
| SCIP | scip/all.hpp |
scip_api |
scip_milp |
8.0.4 – 10.0.3 |
| Cbc (experimental) | cbc/all.hpp |
cbc_api |
cbc_milp |
2.10.9 – 2.10.13 |
| Clp | clp/all.hpp |
clp_api |
clp_lp |
1.17.4 – 1.17.11 |
| GLPK | glpk/all.hpp |
glpk_api |
glpk_lp, glpk_milp |
4.59 – 5.0 |
| SoPlex | soplex/all.hpp |
soplex_api |
soplex_lp |
7.1.1 – 8.0.3 |
Notes:
*_lpclasses model continuous problems;*_milpclasses add integer and binary variables (a*_milpmodel with only continuous variables is of course a valid LP). SCIP and Cbc expose only a MILP class; Clp and SoPlex only an LP class.- Quadratic objectives (
*_qp) are currently supported through HiGHS only. - SoPlex's C interface gained
SoPlex_setRealParam, the objective getter and the variable-bound and row-side entry points in 7.0, so on an older SoPlexset_time_limit,get_objective,get_objective_coefficient, the variable-bound setters and the row-side getters and setters throw although their concepts hold. It has no getter for real parameters:get_time_limit()returns the last limit SoPlex accepted. A value above 1e100 is stored as 1e100, and a negative one throws. soplex_lpreads column bounds from its own copy, since SoPlex misreads infinite bounds once a solve has scaled the LP. Change them through the model, never throughnative_model(): a solve that reloads the LP, as it does after a row's active side was freed, writes the copy back.- Cbc is experimental. The C API of the released Cbc rebuilds the matrix on every row added, so building a model is slow, and nothing on the MIP++ side can avoid it; the Cbc figures of Performance come from an unreleased build. Its known limitations are documented, and no workaround beyond those already in place is planned: native parameters that do not reach MIP solves below Cbc 3.0, and rows without terms or with all-zero coefficients on a
develbuild, in the next two notes; wrong answers and endless branching on some rows over unbounded integer columns, see Integrality proofs; and a time limit that did not bound the root LP on adevelbuild, see Deletion filter. - Below Cbc 3.0, which covers every validated release, each MIP solve of
cbc_milpruns on a private copy of the model, since Cbc carries a MIP solve's incumbent unchecked into the next one. A parameter set throughnative_api()onnative_model()therefore does not reach MIP solves, and a native read of the solution after a MIP solve sees the unsolved model. Use the MIP++ setters and getters there; an LP solve, without integer columns, still runs onnative_model(). - A Cbc
develbuild, outside the validated range, drops a row without terms, so therecbc_milpthrowssolver_errorwhen such a row is added. It keeps a row whose coefficients are all zero, but once the model has been solved, a re-solve can miss a change that makes that row's sides unsatisfiable and answeroptimal. - Xpress leaves a search that stopped at a limit in its presolved form, a MILP search on every release and an LP solve on 45.01, where the row and column counts, the row getters and setters and the additions would then address the presolved rows (47.01 leaves a stopped LP in its original form), so
xpress_milp::solve()andxpress_lp::solve()postsolve it before returning. The incumbent stays available, but a MILP's search tree is dropped: a secondsolve()starts the search over rather than resuming it.
Switching backends¶
The examples follow one convention: the backend appears in exactly two places — the include and one alias.
Change those to gurobi/gurobi_milp and recompile: the rest of the program is untouched. There is no linking step to adjust, because solver libraries are loaded at runtime.
One implementation per solver, and the releases it is validated on¶
A binding is one implementation that adapts at runtime to a range of solver releases — probing for entry points that appeared or disappeared along the way — and lives in an implementation namespace, mippp::gurobi::impl::v1 for Gurobi, which is what all.hpp aliases into mippp as gurobi_api, gurobi_lp and gurobi_milp. Two lists on the api class state what it drives:
gurobi_api::library_names— the library namesgurobi_api::load()searches for, newest first (libgurobi130.so,libgurobi120.so, …);gurobi_api::validated_versions— the release ranges that have passed the full test suite, half-open ({{10}, {14}}is every 10.x.y up to 13.x.y): a row of the compatibility matrix or, for the solvers the matrix cannot obtain or license, a maintainer's run recorded in that page's notes.
A loaded release outside the validated ranges is used anyway, with a warning on stderr naming the ranges and what the library reported; MIPPP_NO_VERSION_WARNING silences it. To load one particular release, pin its file with MIPPP_GUROBI_LIBRARY or an explicit path.
A new solver release the implementation still drives is a matrix run and a bump of the validated range (plus a new library name where the solver ships one per release); one it cannot adapt to gets impl::v2, and the mippp aliases move to it while impl::v1 stays available unchanged.
To choose the solver at runtime — for a --solver command-line flag, say — write the model-building code once as a template over the backend and dispatch on the flag; that pattern, and the capability checks that go with it, are the subject of Writing solver-generic code.
Only the solvers actually installed on the machine need to be present: a backend fails when its first model is constructed (with a descriptive exception), not at program startup.
How solver libraries are found¶
Each backend has an api object, gurobi_api say, holding the C entry points the wrapper uses. A model's default constructor obtains it through gurobi_api::load(), which opens the solver's shared library through the platform loader (dlopen on Linux and macOS, LoadLibrary on Windows) and resolves those entry points — nothing is linked, and MIP++ needs no third-party loader library. Resolution order (first match wins):
- an explicit path passed to
load—gurobi_milp model(gurobi_api::load("/opt/gurobi1201/linux64/lib/libgurobi120.so")); - the
MIPPP_<SOLVER>_LIBRARYenvironment variable, holding the full path of the exact file to load; - a search of the dynamic loader's directories (
LD_LIBRARY_PATHon Linux,DYLD_LIBRARY_PATHon macOS,PATHon Windows, then the system library directories) for the conventional names, accepting version-suffixed sonames (libhighs.so.1.10.0) when the plain name is absent. The first directory holding any of the names wins, as it would for the loader; when a binding drives several releases (libgurobi130.so,libgurobi120.so, …) and one directory holds more than one of them, the newest is taken.
An api object is one loaded library file: library_path() returns it and library_version() the release it reports (empty for a library whose C API has no version call, such as SoPlex, or reporting something that is not a number, such as a Cbc devel build). load returns the same object whenever it resolves to the same file, and that object lives for the rest of the process: every model of a backend shares one table of entry points, and model.native_api() is a reference that cannot dangle. Two explicit paths naming two different files give two independent api objects, each with its own global state, so two versions of the same solver can serve two models in one process. The directory search of step 3 is memoized per solver for the life of the process, so default-constructing many models is cheap; explicit paths and the environment variable are never cached.
A library that exists but lacks the expected entry points (a same-named build without the C API, say) is rejected with the loader's own message rather than half-loaded, and the exception lists every candidate tried. Every loading failure also states this precedence and quotes the current value of MIPPP_<SOLVER>_LIBRARY, so an empty value or a stray space shows. See Installation for per-solver environment setup.
Feature support¶
Core LP/MILP modeling works on every backend. Optional capabilities — dual solutions, callbacks, MIP starts, SOS and indicator constraints, parameter control — vary; each is a concept, so code that needs one states it and the compiler enforces it.
The matrices below record which features are implemented and tested per backend (generated from the test suites). Each column is one model class: the solver's *_milp class in the first matrix, its *_lp class in the second, and highs_qp in the HiGHS QP column.
MILP models¶

LP and QP models¶

Notable current limitations (see the roadmap for what's planned):
- Callbacks — candidate-solution callbacks are implemented and validated on Gurobi, CPLEX, COPT and Xpress; SCIP has none yet. Node-relaxation (user-cut) callbacks are specified but not yet implemented.
- Solve status —
get_status()is part oflp_model, so every backend reports one, but the set of tags a backend can return varies (it is part of the model type).refine_lp_status()— resolvinginfeasible_or_unboundedinto one of the two — exists only ongurobi_lpandcplex_lp, andglpk_milpreports an unbounded MIP asinfeasible_or_unbounded. See Status, limits and tolerances. - Quadratic objectives — HiGHS only. Quadratic constraints: none yet.
- SOS constraints and LP basis warm starts — specified as concepts, not yet implemented by any backend.
- Ranged constraints —
add_ranged_constrainton Clp and Cbc only (has_ranged_constraints). On every backend but Gurobi (Cbc, Clp, COPT, CPLEX, GLPK, HiGHS, MOSEK, SCIP, SoPlex and Xpress),has_modifiable_constraint_boundsmakes any row ranged by giving it two distinct finite sides, andhas_readable_constraint_boundsreads the sides back on every backend; on CPLEX and Xpress, sides that would cross throwstd::invalid_argument, since a ranged row is stored as a side and a width; Gurobi ranges a row through a slack column, so its models have no modifiable row bounds; see Special constraints. - Indicator constraints — Gurobi and CPLEX only (
has_indicator_constraints). The call returns no handle, so an indicator constraint cannot be read back or edited once added (details). The native IIS routines keep indicator constraints in the background: never members, and the members they name conflict against them. - Deletion filter —
compute_iis_by_deletion, one of the two ways to diagnose an infeasible model, runs on every model class but Gurobi's: Cbc, Clp, COPT, CPLEX, GLPK, HiGHS, MOSEK, SCIP, SoPlex and Xpress. Onclp_lp,glpk_lpandglpk_milp, which have no time limit, the deadline ofiis_limitsis checked between trials only, so one trial can overrun it. Oncbc_milpthe forwarded time limit does not bound the root LP, so one trial can overrun the deadline there too (measured on a Cbcdevelbuild; 2.10 was not measured). HiGHS silently repairs, in the model itself, a variable bound or row side that crosses the other by less than its primal feasibility tolerance, and solves the model, while the filter, whose test for crossed sides is exact, reports such a pair as an IIS. - Integrality proofs over unbounded integer columns — Cbc 2.10.11 and 2.10.12 report
x0 + x1 == 1.5over free integer columnsoptimal, withx0 = 1.5. A Cbcdevelbuild finds no proof for that row and branches until a time limit stops it. Cbc 2.10.11, a Cbcdevelbuild andglpk_milpall branch that way on2 x0 + 2 x1 == 1, andglpk_milp, which has no time limit, then never returns. The trials ofcompute_iis_by_deletioninherit these answers. - Deletion filter on SCIP binaries — SCIP types a column
BINARYwhen it comes fromadd_binary_variable, or fromadd_integer_variablewith bounds that SCIP rounds into {0, 1}, such as [0, 1] or [0.25, 0.75]. It accepts relaxing a bound of such a column toinfinity(), and the bound reads back as the infinity written, but the nextsolve()then throwsstd::runtime_error("scip_milp: error in input data").compute_iis_by_deletiontherefore throws on an infeasiblescip_milpwhenever a trial reaches such a bound, after restoring the model. A feasible model, and a run whose limits stop it before that trial, are unaffected. The same holds on SCIP 8.0.4, 9.2.1 and 10.0.2. - Native IIS on Gurobi —
compute_iis()ongurobi_lpandgurobi_milpnever names the bounds of a binary variable, which Gurobi treats as implicit in the type: a binaryzunder the rowz >= 2gives the row alone, while an integerzin [0, 1] gives the row and its upper bound. Ongurobi_lp, the iteration limit ofset_iteration_limitstops the routine with no answer and no reason, where the time limit reports its stop; see Limits. - Native IIS on CPLEX —
compute_iis()oncplex_milpdetaches a registered candidate-solution callback for the call and reattaches it afterwards, since CPLEX refuses to refine a conflict while a generic callback is registered, whatever the callback does (error 1811). After a stop ofcompute_iis()by the iteration limit ofcplex_lp, CPLEX keeps the stop on the unchanged model: the next calls stayundeterminedunder a raised limit until a bound or a side is written, even to its current value; see Limits. - Native IIS on Xpress — Xpress refuses the search on a model with a column whose bounds cross or hold no integer value, even when the conflict lies elsewhere:
compute_iis()throwssolver_errornaming that cause, andcompute_iis_by_deletionanswers there. A ranged row whose two sides are both needed under integrality is reported through its upper side only. Onxpress_milpthe candidate-solution callback is detached for the call and reattached afterwards, since the routine's MIP solves would run it on their candidates, where a rejection turns a feasible model into a whole-model conflict. Xpress 45.01 solves a model with such a column beside a row tooptimal, where 47.01 reportsinfeasible. - Native IIS on COPT —
compute_iis()solves the model as well as running COPT's routine, see Limits, and on acopt_milpwith integer columns, which COPT solves as a MIP, runs that solve up to its first incumbent and without the candidate-solution callback, detached for the call; acopt_milpwhose columns are all continuous behaves ascopt_lp. COPT documents that its IIS "may not be minimal or unique", and on 8.0.5 its answer on a MIP can leave out a bound the conflict needs: of 176 random binary models, 6 answers re-solved feasible without the bounds COPT had not flagged, and none with the flagged columns kept whole, and it flags one bound of a continuous column too where the MIP needs both (xin [0.5, 1.5] with an integeryunderx + 2 * y == 4), which is why such acopt_milpreports a column with two finite bounds as a plainmember, integer or continuous. COPT's routine also crashes on a model without rows whose bounds have a solution, leaks memory on a model without columns, and reports an integer column whose bounds hold no integer, a binary column moved outside [0, 1] included, as nothing or as feasible;compute_iis()answers all three without the routine, from the bounds or from the constant rows, a binary column through the bound beyond its domain. - Community licenses — the commercial backends are validated on community licenses, which cap a model: CPLEX Community Edition 22.1.2 at 1000 rows and 1000 columns, and the Xpress community license at 5000 rows plus columns (error 120). Above the cap the solver refuses the model.
-
Solver-specific parameters — the uniform interface covers limits and tolerances; there is no uniform passthrough for solver-specific knobs such as Gurobi's
MIPFocusor CPLEX's emphasis settings. The escape hatch is thehas_native_handlesconcept, which every model class satisfies:native_model()returns the solver's own objects andnative_api()the loaded*_apiobject, whose members are the solver's raw C functions, so the call is made directly:gurobi_milp model; auto [env, grb_model] = model.native_model(); model.native_api().setintparam(env, "MIPFocus", 2);What comes back depends on the backend: a
(env, model)pair where the solver has an environment —std::pair<GRBenv *, GRBmodel *>(Gurobi),std::pair<CPXENVptr, CPXLPptr>(CPLEX),std::pair<copt_env *, copt_prob *>(COPT),std::pair<MSKenv_t, MSKtask_t>(MOSEK) — and the single problem object otherwise:XPRSprob(Xpress),SCIP *(SCIP),glp_prob *(GLPK),Clp_Simplex *(Clp),Cbc_Model *(Cbc),void *(HiGHS, SoPlex). To address one variable or constraint through the raw API,native_id(v)andnative_id(c)translate a MIP++ handle into what the solver calls it: the column or row index (1-based on GLPK), or theSCIP_VAR */SCIP_CONS *on SCIP. Handles and native indices drift apart once variables have been removed on the backends that delete columns (HiGHS, Gurobi, CPLEX), which is what the translation is for.Modifying the model through the native handles invalidates MIP++ features
MIP++ does not see what is done through
native_model()andnative_api(). Adding, removing or reordering variables or constraints that way, or solving through the raw API, leaves MIP++'s bookkeeping out of date. Handles andnative_id()may then designate the wrong variable or constraint. The variable and constraint counts can disagree with the solver on the backends that keep them on the MIP++ side, such as SCIP and Cbc.get_status()keeps reporting the lastsolve()made through MIP++. Any feature built on this bookkeeping, including those that read or restore the model on your behalf, then loses its guarantees. Keep native calls to parameters and read-only queries. Oncbc_milpbelow Cbc 3.0, even those miss MIP solves, which run on a private copy (see the notes under The backends).
Next¶
Writing solver-generic code — one model builder, every backend, with capability differences resolved at compile time.