Note

You can download this example as a Jupyter notebook or start it in interactive mode.

Using Solvers#

Linopy hands the model off to a solver backend — HiGHS, Gurobi, CPLEX, CBC, GLPK, SCIP, Xpress, MOSEK, MindOpt, COPT, Knitro, or the GPU solver cuPDLPx. This notebook walks through:

  • the standard model.solve(...) workflow,

  • inspecting the solver afterwards via model.solver and SolverReport,

  • the lower-level construct-then-solve API for advanced use,

  • listing installed and licensed solvers.

A Small Example Model#

We’ll use a tiny LP throughout the notebook. Minimize \(x + 2y\) subject to \(x, y \ge 0\), \(3x + 7y \ge 10\), \(5x + 2y \ge 3\).

[ ]:
import linopy
from linopy import Model


def build_model():
    m = Model()
    x = m.add_variables(lower=0, name="x")
    y = m.add_variables(lower=0, name="y")
    m.add_constraints(3 * x + 7 * y >= 10)
    m.add_constraints(5 * x + 2 * y >= 3)
    m.add_objective(x + 2 * y)
    return m

One-step Solving#

model.solve picks the first available solver, runs it, writes the solution back into the variables, and returns a (status, termination_condition) tuple. You can specify which solver you want.

[ ]:
m = build_model()
status, termination = m.solve(solver_name="highs", output_flag=False)
status, termination
[ ]:
m.objective.value
2.862068965517241
[ ]:
m.solution.to_pandas()
x    0.034483
y    1.413793
dtype: float64

After Solving#

After model.solve(...) the solver instance stays attached to the model as model.solver. You can read off the solver name, the native solver model, the status and — new in this release — a SolverReport with runtime, MIP gap, dual (best) bound, and iteration counts.

[ ]:
m.solver
[ ]:
m.solver.report

Note that not every backend fills in every field of SolverReport — if a solver doesn’t expose a value it stays None. mip_gap and dual_bound are most informative on MIPs.

Some solvers (Gurobi, MOSEK, …) hold a license while the underlying handle is alive. You can release it explicitly:

[ ]:
m.solver.close()  # frees the native handle (and license)
# or, to also detach the wrapper:
m.solver = None
m.solver, m.solver_name

Building the Solver#

For most users model.solve(...) is enough. If you want more control — e.g. to adjust the native solver model before running it, or to obtain the Result object directly — you can build the solver in one step and run it in another:

[ ]:
from linopy.solvers import Solver

m = build_model()
solver = Solver.from_name("highs", m, io_api="direct", options={"output_flag": False})
solver

solver.solver_model is the native solver handle — here a highspy.Highs instance. You could tweak it directly before running:

[ ]:
solver.solver_model
<highspy.highs.Highs at 0x129f17290>
[ ]:
result = solver.solve()
result

Result carries the status, solution, solver name, and report. Writing it back into the Model, combining numeric values with labels and coordinates, is a separate call:

[ ]:
m.assign_result(result)
m.objective.value
[ ]:
m.solution.to_pandas()
x    0.034483
y    1.413793
dtype: float64

Model Transformations#

Model transformations live on the ``Model``, not the ``Solver``. A Solver only declares which features it supports and raises during _build() if it can’t handle the model it’s been handed; it never mutates the model. The transformations currently exposed:

Transformation

Methods on Model

Reversible?

SOS reformulation (rewrite SOS constraints as Big-M binary + linear)

model.apply_sos_reformulation() / model.undo_sos_reformulation()

yes

Drop zero-coefficient terms

model.constraints.sanitize_zeros()

one-way

Replace ±inf bounds in constraints

model.constraints.sanitize_infinities()

one-way

model.solve(reformulate_sos=True, sanitize_zeros=True, sanitize_infinities=True) is a convenience that brackets these around the one-shot solve (and undoes the SOS reformulation afterwards). The two-step Solver API does not do this for you — when you go through Solver.from_name(...).solve(), you call the transformations yourself first, and use try/finally to keep the model in a known state if the solve raises:

[ ]:
import pandas as pd

m = Model()
i = pd.Index([0, 1, 2], name="i")
x = m.add_variables(lower=0, upper=1, coords=[i], name="x")
m.add_sos_constraints(x, sos_type=1, sos_dim="i")
m.add_objective(x.sum(), sense="max")

m.constraints.sanitize_zeros()
m.constraints.sanitize_infinities()

m.apply_sos_reformulation()
try:
    solver = Solver.from_name(
        "highs", m, io_api="direct", options={"output_flag": False}
    )
    result = solver.solve()
    m.assign_result(result)
finally:
    m.undo_sos_reformulation()  # restore original SOS form on the Model

list(m.variables.sos)

Available Solvers#

Two registries are exposed at the top level:

  • linopy.available_solvers — solvers whose Python package or binary is installed. Cheap; does not acquire a license.

  • linopy.licensed_solvers — the subset that currently passes a license probe. Useful in tests or to pick a solver at runtime.

[ ]:
list(linopy.available_solvers)
['gurobi', 'highs', 'cbc', 'scip', 'cplex', 'xpress', 'mosek', 'mindopt']
[ ]:
list(linopy.licensed_solvers)
Set parameter Username
Academic license - for non-commercial use only - expires 2026-12-18
MindOpt 2.2.0 | 2e28db43, Aug 29 2025, 14:27:12 | arm64 - macOS 26.2
['gurobi', 'highs', 'cbc', 'scip', 'cplex', 'xpress', 'mosek']
Start license validation (current time : 18-MAY-2026 11:46:49 UTC+0200).
[WARN ] No license file is found.
[ERROR] No valid license was found. Please visit https://opt.aliyun.com/doc/latest/en/html/installation/license.html to apply for and set up your license.
License validation terminated. Time : 0.000s

Both are lazy and refreshable — call linopy.available_solvers.refresh() after installing or licensing a new solver in the same process. For a per-solver probe use SolverClass.license_status(), which returns a LicenseStatus dataclass:

[ ]:
from linopy.solvers import Highs

Highs.license_status()