-
Notifications
You must be signed in to change notification settings - Fork 72
feat: add unified SolverMetrics #583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
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— 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.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 populatedCloses #428