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.solverandSolverReport,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 |
Reversible? |
|---|---|---|
SOS reformulation (rewrite SOS constraints as Big-M binary + linear) |
|
yes |
Drop zero-coefficient terms |
|
one-way |
Replace ±inf bounds in constraints |
|
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()