Conversation
- 7 optional fields: solver_name, solve_time, objective_value, best_bound, mip_gap, node_count, iteration_count — all default to None - Custom __repr__ that only shows non-None fields - Added as metrics field on Result (backward-compatible — defaults to None) Solver-specific metric extraction (linopy/solvers.py) - Base Solver class: _extract_metrics() returns solver_name + objective_value - Gurobi: extracts Runtime, ObjBound, MIPGap, NodeCount, IterCount - HiGHS: extracts getRunTime(), mip_node_count, simplex_iteration_count, mip_gap, mip_objective_bound - SCIP: extracts getSolvingTime(), getDualbound(), getGap(), getNNodes(), getNLPIterations() - CBC: uses already-parsed mip_gap and runtime from log output - All other solvers (GLPK, Cplex, Xpress, Mosek, COPT, MindOpt, cuPDLPx): use base class default - All 12 return Result(...) sites updated to pass metrics - Every attribute access is wrapped in try/except so extraction never breaks the solve Model integration (linopy/model.py) - _solver_metrics slot, initialized to None - solver_metrics property - Stored from result.metrics after solve() - Set to basic metrics in _mock_solve() - Reset to None in reset_solution() Package export (linopy/__init__.py) - SolverMetrics added to imports and __all__ Tests (test/test_solver_metrics.py) - 13 tests covering: dataclass defaults, partial values, repr, Result backward compat, Model integration (before/after solve, reset), parametrized solver-specific tests for both direct and file-IO solvers
- Added mock-based unit tests for all 10 solver overrides (CBC, Highs, Gurobi, SCIP, Cplex, Xpress, Mosek, COPT, MindOpt, cuPDLPx) - Added test_extract_metrics_graceful_on_missing_attr — verifies _safe_get degrades gracefully - Tests skip for unavailable solvers using @pytest.mark.skipif
Remove all mock/patch-based _extract_metrics tests. The parametrized integration tests (test_solver_metrics_direct, test_solver_metrics_file_io) now assert solve_time >= 0 for every available solver, ensuring attribute names are correct against real solver objects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only parametrize over solvers with _extract_metrics overrides (gurobi, highs, scip, cplex, xpress, mosek), so solvers with base-only metrics (glpk, copt, cbc) don't fail on solve_time.
|
Covereage fails, as not all solvers are in CI. But this is a change which might affect CI in general. |
lkstrp
left a comment
There was a problem hiding this comment.
I would really like to see something like this. Not sure though if this would be enough to reliable benchmark the model in automated runs, but with a fixed node/ environment I don't see why this wouldn't already be helpful. Any thoughts on getting memory usage as well? I think gurobi gives you peak memory, not sure for the other solvers
I tried to design this in a way to be extensible, add new attributes and populate them for solvers that provide them. |
@lkstrp My concern is not benchmarking here. Im mostly solving MILP programs, and having the MIP GAP is very relevant. I need to save this alongside the solution. I think there are others that would benefit from such. Memory usage on the other hand is purely benchmarking imo. |
Populate peak memory usage (MB) for Gurobi (MaxMemUsed) and Xpress (peakmemory). Other solvers gracefully return None. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@lkstrp I checked solvers for peak memory infos. I added `peak_memory' for those providing it: gurobi and xpress |
Summary
Add a unified
SolverMetricsdataclass that provides solver-independent access to performance metrics after solving. Accessible viaModel.solver_metrics.Fields:
solver_name,solve_time,objective_value,dual_bound,mip_gap,peak_memory— all default toNone; solvers populate what they can.Design:
Solver._extract_metrics()populatessolver_nameandobjective_valuedataclasses.replace()_safe_get()with debug logging so extraction never breaks the solve_extract_metrics()and use_safe_get(). PRs adding metrics for additional solvers are welcome!Solver coverage:
*CBC parses
solve_timeandmip_gapfrom log output via regex. These fields depend on the CBC log format, which varies across versions.peak_memory(MB) is only available for solvers that expose it via their Python API. Verified that HiGHS, SCIP, CPLEX, Mosek, and CBC do not expose peak memory. Gurobi provides it viaMaxMemUsed(GB, converted to MB) and Xpress viapeakmemory(bytes, converted to MB).Solvers without a tested
_extract_metricsoverride still getsolver_nameandobjective_valuefrom the base class. We intentionally did not add solver-specific overrides for untested solvers — incorrect attribute names would silently returnNone, giving a false sense of coverage.Bugs fixed along the way:
mip_objective_bound— fixed tomip_dual_bound. Also fixed status comparison (== 0→== highspy.HighsStatus.kOk).miprelgapattribute doesn't exist — compute gap manually frommipbestobjvalandbestbound.m.solution.progress.get_time()doesn't exist — usetime.perf_counter()around the solve call.Test plan
SolverMetricsdataclass unit tests (defaults, partial, repr, frozen)Resultbackward compatibility tests (with/without metrics)Modelintegration tests (metrics before solve, after mock solve, after reset)mip_gapanddual_boundare populatedpeak_memorytest for Gurobi and XpressCloses #428