refactor: stateful Solver instances and two-step solve API#682
refactor: stateful Solver instances and two-step solve API#682FabianHofmann wants to merge 28 commits into
Conversation
Phase B of solver refactor (issue #628). Makes the Solver instance the canonical owner of solver-side state. - Base Solver.__init__ now initializes options, status, solution, report, solver_model, io_api, env, capability, _env_stack. - Adds to_solver_model / update_solver_model / resolve / close / __del__ on the base class; resolve dispatches to per-subclass _resolve. - Adds _make_result helper that populates instance state and stamps solver_name and report onto Result. - Gurobi: env creation moved off per-call ExitStack onto self._env_stack so the env remains valid after solve returns; to_solver_model and _resolve overrides wired. - Highs / Mosek / cuPDLPx: to_solver_model + _resolve overrides; Mosek task is now kept alive via self._env_stack instead of being closed at function exit. - CBC / GLPK / Cplex / SCIP / Xpress / Knitro / COPT / MindOpt: minimal wiring — populate self.status/self.solution/self.solver_model/self.io_api via _make_result and pass solver_name + report (where readily available) into the returned Result. solve_problem dispatcher and the public solve_problem_from_model / solve_problem_from_file signatures are unchanged. Model.solve is untouched (Phase C).
Surfaces solver name, status, io_api, and solution/report summary.
Move SolverFeature and _xpress_supports_gpu into linopy.solvers; declare features/display_name as ClassVars on each Solver subclass with a Solver.supports() classmethod. solver_capabilities becomes a back-compat shim with a lazy SOLVER_REGISTRY mapping. Model.solve uses the class API directly; SolverFeature is re-exported at the package top level.
Stash `sense` on the Solver instance in `to_solver_model` and make `Solver.resolve()` take no args. Add `Model.to_solver_model(name)` and `Model.resolve()` wrappers so the two-step direct-API flow lives on the model. Update the direct-API test and re-run the piecewise notebook.
Model.to_solver_model -> prepare_solver and Model.resolve -> run_solver (plus Solver.resolve/_resolve -> run/_run). Avoids the awkward "resolve on first call" reading. Solver.to_solver_model is kept since it accurately produces the native solver model.
Replace Xpress-specific _xpress_supports_gpu with a generic _installed_version_in helper, and add Solver.runtime_features() as an override hook for version/env-conditional capabilities. Xpress now declares its GPU support via runtime_features() instead of inline frozenset arithmetic on the class body.
…method Move the full parameter docstring onto Solver.solve_problem_from_model and drop the per-subclass duplicates on Mosek and cuPDLPx; subclasses now inherit the abstract method's docstring.
Unify per-solver _translate_to_* methods under a common _build_solver_model name, hoist their local imports to module top-level, drop dead params from cuPDLPx (moving its UserWarning into the public to_solver_model), and add TYPE_CHECKING stubs. Expand to_* deprecation messages with step-by-step migration paths, wrap existing tests in pytest.warns, and cover the unknown-solver-name branch in prepare_solver.
for more information, see https://pre-commit.ci
|
@FabianHofmann I suggest checking the new class |
|
Can we make the solver class into a data class, too? And get rid of this strange instantiate solver_class(**solver_options) pattern. I am not sure whether there is a benefit to holding a solver class instance without a model attached, so what about constructors like: which dispatches to which dispatches according to io_api. I'd say this then gets rid of any case for prepare_solver or some such. |
|
Benchmark: OETC should be able to become a solver class. class Model:
...
def solve(solver_name, io_api, **solver_options):
self.solver = None
self.solver = solver = Solver.from_name(solver_name, model=self, io_api=io_api, options=solver_options)
result = solver.solve()
self.apply_result(result) |
|
Solvers should get a class method: We then use a derived Collection pattern similar to the following for available_solvers so not to grab licenses prematurely. |
|
ah yes, and OETC and some Gurobi compute like instances can have an asynchronous solving option. Where you do not want to block the process, but return early and only give back some sort of job identifier in hand with which to retrieve the solution later. Gurobi docs: https://docs.gurobi.com/projects/optimizer/en/current/features/batchoptimization.html And i don't mean implement this now, here, but the interface should be extensible to allow for that. |
|
i am unsure how to effectively keep track of the state that was communicated to the solver. ie. when you then do modifications on the linopy model data after it was communicated to the solver. when do you send those updates (and which updates) the most promising way in my mind would be the following: the solver object holds a shallow copy of the linopy model (up until the individual constraint and variable objects). EDIT: probably only of the constraints and variable objects. when you make an update to a variable bound or constraint you use some sort of cow to create a copy in the linopy model (this mostly means something like a v.data = v.data.assign pattern and maybe on mutable constraints and rhs an explicit cow numpy flag). this means you share variable data/constraint data until you make the first modification. then you do several nice characteristics that way:
|
|
@FabianHofmann i reread the pr description. don't use vlabels, clabels through m.matrices. m.matrices is expensive when constraints are not frozen. but I think you know and that's why they are stored on solver, isn't it |
thanks @FBumann for raising this. I pulled over the dual_bound feature from your pr. should cover all if it then |
Replace pd.Series with dense NaN-padded np.ndarray keyed by integer model labels. Adds values_to_lookup_array helper; simplifies Model.apply_result; migrates every solver to build the lookup array directly (direct-API solvers via cached _vlabels/_clabels, file-based solvers via a shared _names_to_labels helper). Drops the now-unused series_to_lookup_array and all name-based solution fallbacks.
Adds dual_bound field to SolverReport, populated by HiGHS, Gurobi and Knitro. Declared via new SolverFeature.MIP_DUAL_BOUND_REPORT so tests gate assertions on capability instead of solver names.
for more information, see https://pre-commit.ci
Mirror VariableLabelIndex on the constraint side: add ConstraintBase.active_labels (cheap on both Constraint and CSRConstraint) and a cached ConstraintLabelIndex on Constraints. Model.apply_result and IIS extraction now read vlabels/clabels from these caches instead of self.matrices, which previously rebuilt the full A matrix on every solve.
…rder Restore the 43a82aa contract: Solution.primal and Solution.dual are dense ndarrays indexed by linopy label with NaN at gaps. Each solver builds the label-indexed form itself; apply_result shrinks to a direct lookup_vals. This fixes two classes of bugs the intermediate 'primal/dual in build order' contract couldn't handle: - File-based LP solvers iterate variables in objective-encounter order, which can interleave entries from multiple add_variables calls (e.g. non-aligned coords producing x0, x10, x1, x11, ...). Positional alignment with vlabels broke for every test exercising masked or non-aligned models on CBC / GLPK / Cplex / SCIP / Xpress. - CPLEX drops entirely-unconstrained unused variables on LP read, so get_values() returns fewer values than the linopy model has labels; positional alignment errored at apply_result. Implementation: - _solution_from_names(values, names): file-based path. Parses linopy labels via the existing _names_to_labels helper and scatters values into a label-indexed array. Used by CBC, GLPK, Cplex, SCIP, Xpress, Knitro, COPT, MindOpt, and the from_file branches of Highs/Gurobi. - _solution_from_labels(values, labels): direct-API path. Uses cached vlabels/clabels populated on the Solver instance at to_solver_model time. Used by Highs, Gurobi, Mosek, cuPDLPx. - Highs and Gurobi from_file branches converge on _solution_from_names, replacing the inline keep + argsort pattern.
Solver is now a dataclass; subclasses inherit init. Construction routes through Solver.from_name(name, model, io_api=..., options=...) which builds at construction (direct API or LP/MPS file). solver.solve() returns a Result; model.apply_result(result) writes the solution back. Drops Model.prepare_solver/run_solver; to_* io helpers no longer warn.
c5a798a to
fe18860
Compare
for more information, see https://pre-commit.ci
closes #628 #583
Changes proposed in this Pull Request
Refactor of the solver layer to put solver state on a stateful
Solverinstance and expose a clean construct-then-solve workflow.SolveronModel.solver. Solver state (native model, results) now lives on aSolverinstance attached toModel.solver.Model.solver_modelandModel.solver_namebecome read-only properties delegating tomodel.solver(assigning anything butNoneraises; settingNonecloses the solver).Model.solver_namemay beNonebefore a solve. These two properties are candidates for future deprecation.Solver.from_name(name, model, io_api=..., options=...)(orSolverClass.from_model(model, ...)), then callsolver.solve()to run and obtain aResult, andmodel.apply_result(result)to write the solution back to the model.Solveris a dataclass; subclasses no longer need__init__overrides._build_direct(**kwargs)(build the native model fromself.model),_run_direct(**kwargs)(run the prebuilt native model), and_run_file(**kwargs)(invoke the solver onself._problem_fn). File-only solvers (CBC, GLPK, Cplex, SCIP, Xpress, Knitro, COPT, MindOpt) override only_run_file. Direct-API solvers (Highs, Gurobi, Mosek, cuPDLPx) override all three.Solver.solve_problem,Solver.solve_problem_from_model, andSolver.solve_problem_from_fileare kept on the base class as thin shims that route through the new pipeline and emitDeprecationWarning. To be removed in a future release.Model-bound helpers (model.to_gurobipy(),model.to_highspy(),to_cupdlpx(model)), or directly viaSolver.from_model(model, io_api="direct").solver_model. The previous publicSolver.to_solver_modelmethod is removed and folded into the internal_build_directhook to avoid exposing a third redundant path.features: frozenset[SolverFeature]ClassVars on eachSolversubclass; query withSolver.supports(feature).SolverFeatureis exported fromlinopy(andlinopy.solvers);linopy.solver_capabilitiesremains as a back-compat shim with a lazySOLVER_REGISTRYmapping.Resultcarries solver report.Resultgainssolver_nameandreport: SolverReport | None(runtime, MIP gap, dual bound, iteration counts) and prints them in__repr__. CBC, HiGHS, Gurobi, Knitro, and cuPDLPx populatereport; when possible also populate the MIPdual_bound.Solution.primalandSolution.dualare now densenp.ndarrays indexed by linopy label (length =max_label + 1); masked or solver-dropped slots areNaN. Previouslypd.Serieskeyed by name. Each solver emits arrays in this label-indexed form — direct-API solvers via cached_vlabels/_clabelspopulated at_build_directtime, file-based solvers via the shared_solution_from_nameshelper. Robust to solver iteration order and to solvers dropping unused variables (e.g. CPLEX on LP read).Constraintsexposes a cachedlabel_index(mirroringVariables.label_index), soapply_resultand IIS extraction no longer trigger a fullmodel.matricesrebuild. Invalidated on add/remove.Reviewer requests
SolverReportwithSolverMetrics(feat: add unified SolverMetrics #583); exposedual_bound(best bound)Solvera dataclass; drop thesolver_class(**solver_options)patternSolver.from_name(name, model, io_api=..., options=...)static constructor +SolverClass.from_model(...)classmethodprepare_solver/run_solvertwo-step APIvlabels/clabelson the solver instead of going throughmodel.matrices, see vlabels-clabels-flow.html for more detailsModel.solve = self.solver = Solver.from_name(...); apply_result(...)pattern inModel.solveis_available()classmethod per solver + lazySOLVER_REGISTRYsoavailable_solversdiscovery doesn't grab licenses prematurelySolversubclasssolver.update(model)diffs and pushes only changed bounds/rhs/coefficients (CoW on variable/constraint data)Checklist
doc.doc/release_notes.rstof the upcoming release is included.