Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Release Notes
Upcoming Version
----------------

* Add unified ``SolverMetrics`` dataclass accessible via ``Model.solver_metrics`` after solving. Provides ``solver_name``, ``solve_time``, ``objective_value``, ``best_bound``, and ``mip_gap`` in a solver-independent way. All solvers populate solver-specific fields where available.

Version 0.6.3
--------------

Expand Down
175 changes: 132 additions & 43 deletions examples/create-a-model.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@
},
{
"cell_type": "code",
"execution_count": null,
"id": "dramatic-cannon",
"metadata": {},
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:38.164407Z",
"start_time": "2026-02-11T21:42:38.162992Z"
}
},
"source": [],
"outputs": [],
"source": []
"execution_count": null
},
{
"attachments": {},
Expand All @@ -49,15 +54,20 @@
},
{
"cell_type": "code",
"execution_count": null,
"id": "technical-conducting",
"metadata": {},
"outputs": [],
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.360058Z",
"start_time": "2026-02-11T21:42:38.171827Z"
}
},
"source": [
"from linopy import Model\n",
"\n",
"m = Model()"
]
],
"outputs": [],
"execution_count": null
},
{
"attachments": {},
Expand All @@ -83,14 +93,19 @@
},
{
"cell_type": "code",
"execution_count": null,
"id": "protecting-power",
"metadata": {},
"outputs": [],
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.387467Z",
"start_time": "2026-02-11T21:42:39.384712Z"
}
},
"source": [
"x = m.add_variables(lower=0, name=\"x\")\n",
"y = m.add_variables(lower=0, name=\"y\");"
]
],
"outputs": [],
"execution_count": null
},
{
"attachments": {},
Expand All @@ -103,13 +118,18 @@
},
{
"cell_type": "code",
"execution_count": null,
"id": "virtual-anxiety",
"metadata": {},
"outputs": [],
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.393709Z",
"start_time": "2026-02-11T21:42:39.390438Z"
}
},
"source": [
"x"
]
],
"outputs": [],
"execution_count": null
},
{
"attachments": {},
Expand All @@ -127,13 +147,18 @@
},
{
"cell_type": "code",
"execution_count": null,
"id": "fbb46cad",
"metadata": {},
"outputs": [],
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.405691Z",
"start_time": "2026-02-11T21:42:39.396625Z"
}
},
"source": [
"3 * x + 7 * y >= 10"
]
],
"outputs": [],
"execution_count": null
},
{
"attachments": {},
Expand All @@ -146,13 +171,18 @@
},
{
"cell_type": "code",
"execution_count": null,
"id": "60f41b76",
"metadata": {},
"outputs": [],
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.416325Z",
"start_time": "2026-02-11T21:42:39.409117Z"
}
},
"source": [
"3 * x + 7 * y - 10 >= 0"
]
],
"outputs": [],
"execution_count": null
},
{
"attachments": {},
Expand All @@ -167,14 +197,19 @@
},
{
"cell_type": "code",
"execution_count": null,
"id": "hollywood-production",
"metadata": {},
"outputs": [],
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.431755Z",
"start_time": "2026-02-11T21:42:39.420977Z"
}
},
"source": [
"m.add_constraints(3 * x + 7 * y >= 10)\n",
"m.add_constraints(5 * x + 2 * y >= 3);"
]
],
"outputs": [],
"execution_count": null
},
{
"attachments": {},
Expand All @@ -189,13 +224,18 @@
},
{
"cell_type": "code",
"execution_count": null,
"id": "overall-exhibition",
"metadata": {},
"outputs": [],
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.438865Z",
"start_time": "2026-02-11T21:42:39.434328Z"
}
},
"source": [
"m.add_objective(x + 2 * y)"
]
],
"outputs": [],
"execution_count": null
},
{
"attachments": {},
Expand All @@ -210,13 +250,18 @@
},
{
"cell_type": "code",
"execution_count": null,
"id": "pressing-copying",
"metadata": {},
"outputs": [],
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.532619Z",
"start_time": "2026-02-11T21:42:39.441886Z"
}
},
"source": [
"m.solve(solver_name=\"highs\")"
]
],
"outputs": [],
"execution_count": null
},
{
"attachments": {},
Expand All @@ -229,23 +274,67 @@
},
{
"cell_type": "code",
"execution_count": null,
"id": "electric-duration",
"metadata": {},
"outputs": [],
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.560199Z",
"start_time": "2026-02-11T21:42:39.553844Z"
}
},
"source": [
"x.solution"
]
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"execution_count": null,
"id": "e6d31751",
"metadata": {},
"outputs": [],
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.577784Z",
"start_time": "2026-02-11T21:42:39.573362Z"
}
},
"source": [
"y.solution"
]
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"id": "9zgzuhvo1b8",
"source": [
"### Solver Metrics\n",
"\n",
"After solving, you can inspect performance metrics reported by the solver via `solver_metrics`. This includes solve time, objective value, and for MIP problems, the dual bound and MIP gap (available for most solvers."
],
"metadata": {}
},
{
"cell_type": "code",
"id": "bdfxi7haoc",
"source": "m.solver_metrics",
"metadata": {
"ExecuteTime": {
"end_time": "2026-02-11T21:42:39.592065Z",
"start_time": "2026-02-11T21:42:39.589851Z"
}
},
"outputs": [
{
"data": {
"text/plain": [
"SolverMetrics(solver_name='highs', solve_time=0.0019101661164313555, objective_value=2.862068965517241, dual_bound=0.0, mip_gap=inf)"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"execution_count": null
},
{
"attachments": {},
Expand Down
3 changes: 2 additions & 1 deletion linopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import linopy.monkey_patch_xarray # noqa: F401
from linopy.common import align
from linopy.config import options
from linopy.constants import EQUAL, GREATER_EQUAL, LESS_EQUAL
from linopy.constants import EQUAL, GREATER_EQUAL, LESS_EQUAL, SolverMetrics
from linopy.constraints import Constraint, Constraints
from linopy.expressions import LinearExpression, QuadraticExpression, merge
from linopy.io import read_netcdf
Expand All @@ -34,6 +34,7 @@
"OetcHandler",
"QuadraticExpression",
"RemoteHandler",
"SolverMetrics",
"Variable",
"Variables",
"available_solvers",
Expand Down
48 changes: 47 additions & 1 deletion linopy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Linopy module for defining constant values used within the package.
"""

import dataclasses
import logging
from dataclasses import dataclass, field
from enum import Enum
Expand Down Expand Up @@ -211,6 +212,46 @@ class Solution:
objective: float = field(default=np.nan)


@dataclass(frozen=True)
class SolverMetrics:
"""
Unified solver performance metrics.

All fields default to ``None``. Solvers populate what they can;
unsupported fields remain ``None``. Access via
:attr:`Model.solver_metrics` after calling :meth:`Model.solve`.

Attributes
----------
solver_name : str or None
Name of the solver used.
solve_time : float or None
Wall-clock time spent solving (seconds).
objective_value : float or None
Objective value of the best solution found.
dual_bound : float or None
Best bound on the objective from the MIP relaxation (also known as
"best bound"). Only populated for integer programs.
mip_gap : float or None
Relative gap between the objective value and the dual bound.
Only populated for integer programs.
"""

solver_name: str | None = None
solve_time: float | None = None
objective_value: float | None = None
dual_bound: float | None = None
mip_gap: float | None = None

def __repr__(self) -> str:
fields = []
for f in dataclasses.fields(self):
val = getattr(self, f.name)
if val is not None:
fields.append(f"{f.name}={val!r}")
return f"SolverMetrics({', '.join(fields)})"


@dataclass
class Result:
"""
Expand All @@ -220,6 +261,7 @@ class Result:
status: Status
solution: Solution | None = None
solver_model: Any = None
metrics: SolverMetrics | None = None

def __repr__(self) -> str:
solver_model_string = (
Expand All @@ -232,12 +274,16 @@ def __repr__(self) -> str:
)
else:
solution_string = "Solution: None\n"
metrics_string = ""
if self.metrics is not None:
metrics_string = f"Solver metrics: {self.metrics}\n"
return (
f"Status: {self.status.status.value}\n"
f"Termination condition: {self.status.termination_condition.value}\n"
+ solution_string
+ f"Solver model: {solver_model_string}\n"
f"Solver message: {self.status.legacy_status}"
+ metrics_string
+ f"Solver message: {self.status.legacy_status}"
)

def info(self) -> None:
Expand Down
Loading