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\).

[1]:
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.

[2]:
m = build_model()
status, termination = m.solve(solver_name="highs", output_flag=False)
status, termination
[2]:
('ok', 'optimal')
[3]:
m.objective.value
[3]:
2.8620689655172415
[4]:
m.solution.to_pandas()
[4]:
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.

[5]:
m.solver
[5]:
Highs(name='highs', status='ok', io_api='lp', solver_model=loaded, objective=2.862, runtime=0.000321s)
[6]:
m.solver.report
[6]:
SolverReport(runtime=0.00032138824462890625, mip_gap=inf, dual_bound=0.0, barrier_iterations=None, simplex_iterations=None)

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:

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

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:

[8]:
from linopy.solvers import Solver

m = build_model()
solver = Solver.from_name("highs", m, io_api="direct", options={"output_flag": False})
solver
Running HiGHS 1.15.1 (git hash: 04024d7): Copyright (c) 2026 under MIT licence terms
Includes third-party software components, see THIRD_PARTY_NOTICES.md for full details
[8]:
Highs(name='highs', status='unsolved', io_api='direct', solver_model=loaded)

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

[9]:
solver.solver_model
[9]:
<highspy.highs.Highs at 0x7282e7588470>
[10]:
result = solver.solve()
result
[10]:
Status: ok
Termination condition: optimal
Solution: 2 primals, 2 duals
Objective: 2.86e+00
Solver: highs
Runtime: 0.00s
MIP gap: inf
Dual bound: 0.00e+00
Solver model: available
Solver message: Optimal

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:

[11]:
m.assign_result(result)
m.objective.value
[11]:
2.8620689655172415
[12]:
m.solution.to_pandas()
[12]:
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:

[13]:
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)
Running HiGHS 1.15.1 (git hash: 04024d7): Copyright (c) 2026 under MIT licence terms
Includes third-party software components, see THIRD_PARTY_NOTICES.md for full details
[13]:
['x']

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.

[14]:
list(linopy.available_solvers)
[14]:
['gurobi', 'highs']
[15]:
list(linopy.licensed_solvers)
Restricted license - for non-production use only - expires 2027-11-29
[15]:
['gurobi', 'highs']

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:

[16]:
from linopy.solvers import Highs

Highs.license_status()
[16]:
LicenseStatus(solver='highs', ok=True, message=None)