You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #682. That PR introduces the stateful Solver interface (Solver.from_name(...).solve() → Result, model.apply_result(...)) but left OETC on the old remote=OetcHandler(...) branch in Model.solve. This issue tracks folding OETC into the new shape.
Motivation
Model.solve still has a special-case for isinstance(remote, OetcHandler) (linopy/model.py:1626–1644) that bypasses the new Solver pipeline.
OETC users can't say m.solve("oetc", ...); they have to construct an OetcHandler and pass it via remote=.
The returned solution is patched in field-by-field rather than going through apply_result / label-indexed Solution.
Add class Oetc(Solver[OetcSettings]) in linopy/solvers.py. Treat netcdf-over-GCP as io_api="direct" — the "native model" is the linopy Model shipped as netcdf; don't introduce a new IO_APIS entry.
Dataclass fields (on top of inherited model/io_api/options):
settings: OetcSettings | None = None (resolve via OetcSettings.from_env() if absent)
_build_direct(...) — instantiate OetcHandler(self.settings), serialize self.model to a temp netcdf, upload to GCP, cache _vlabels/_clabels via _cache_model_labels(self.model). Do not submit the job here.
_run_direct(...) — submit, poll, download, read_netcdf into self._solved_model, assemble Solution/Status/SolverReport, return via self._make_result(...).
_run_file — not implemented.
Solution assembly. The round-tripped model shares labels with the source. Build dense label-indexed arrays of size _n_vars / _n_cons by iterating local self.model.variables / constraints, reading labels per name, and indexing primal[labels.ravel()] = solved.variables[name].solution.values.ravel() (and same for duals). Objective from solved.objective.value. Missing entries stay NaN.
Deprecation. In Model.solve, when remote is an OetcHandler, emit a DeprecationWarning and route through Solver.from_name("oetc", self, settings=remote.settings, options=solver_options). OetcHandler stays for one release.
Open design question
Async-job seam. coroa noted in #682 that the interface should be extensible to async solving (Gurobi batch, OETC) — return early with a job handle, retrieve later. Cleanest hook: split _run_direct into _submit() (sets self._job_uuid, returns) and _collect() (polls, downloads, builds Result). _run_direct calls both serially today; a future solve(blocking=False) returns after _submit(), and a later solver.solve() / solver.collect() finishes. Should this PR do the split, or stay synchronous and let the async PR carve it out? Recommendation: do the split now (two extra methods, locks in a usable seam).
Follow-up to #682. That PR introduces the stateful
Solverinterface (Solver.from_name(...).solve() → Result,model.apply_result(...)) but left OETC on the oldremote=OetcHandler(...)branch inModel.solve. This issue tracks folding OETC into the new shape.Motivation
Model.solvestill has a special-case forisinstance(remote, OetcHandler)(linopy/model.py:1626–1644) that bypasses the newSolverpipeline.m.solve("oetc", ...); they have to construct anOetcHandlerand pass it viaremote=.apply_result/ label-indexedSolution.Solverinterface sets up the async-job seam coroa flagged in refactor: stateful Solver instances and two-step solve API #682 (Gurobi batch, OETC).Proposed design (TBD)
Add
class Oetc(Solver[OetcSettings])inlinopy/solvers.py. Treat netcdf-over-GCP asio_api="direct"— the "native model" is the linopyModelshipped as netcdf; don't introduce a newIO_APISentry.Dataclass fields (on top of inherited
model/io_api/options):settings: OetcSettings | None = None(resolve viaOetcSettings.from_env()if absent)_handler: OetcHandler | None,_job_uuid: str | None,_solved_model: Model | NoneLifecycle:
_build_direct(...)— instantiateOetcHandler(self.settings), serializeself.modelto a temp netcdf, upload to GCP, cache_vlabels/_clabelsvia_cache_model_labels(self.model). Do not submit the job here._run_direct(...)— submit, poll, download,read_netcdfintoself._solved_model, assembleSolution/Status/SolverReport, return viaself._make_result(...)._run_file— not implemented.Solution assembly. The round-tripped model shares labels with the source. Build dense label-indexed arrays of size
_n_vars/_n_consby iterating localself.model.variables/constraints, readinglabelsper name, and indexingprimal[labels.ravel()] = solved.variables[name].solution.values.ravel()(and same for duals). Objective fromsolved.objective.value. Missing entries stayNaN.SolverReport.runtime = job_result.duration_in_seconds,solver_runtime = job_result.solving_duration_in_seconds. The orchestrator doesn't exposemip_gap/dual_bound/ iterations — leaveNone.Registration. Add
SolverName.OETC = "oetc"; append"oetc"toavailable_solverswhen_oetc_deps_available.Public API:
Deprecation. In
Model.solve, whenremoteis anOetcHandler, emit aDeprecationWarningand route throughSolver.from_name("oetc", self, settings=remote.settings, options=solver_options).OetcHandlerstays for one release.Open design question
Async-job seam. coroa noted in #682 that the interface should be extensible to async solving (Gurobi batch, OETC) — return early with a job handle, retrieve later. Cleanest hook: split
_run_directinto_submit()(setsself._job_uuid, returns) and_collect()(polls, downloads, buildsResult)._run_directcalls both serially today; a futuresolve(blocking=False)returns after_submit(), and a latersolver.solve()/solver.collect()finishes. Should this PR do the split, or stay synchronous and let the async PR carve it out? Recommendation: do the split now (two extra methods, locks in a usable seam).