Refactor OETC as a `Solver` subclass
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 257
- Forks
- 87
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 29
Description
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.solvestill has a special-case forisinstance(remote, OetcHandler)(linopy/model.py:1626–1644) that bypasses the newSolverpipeline.- OETC users can't say
m.solve("oetc", ...); they have to construct anOetcHandlerand pass it viaremote=. - The returned solution is patched in field-by-field rather than going through
apply_result/ label-indexedSolution. - Aligning OETC with the
Solverinterface sets up the async-job seam coroa flagged in #682 (Gurobi batch, OETC).
Proposed design (TBD)
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 viaOetcSettings.from_env()if absent)- Runtime:
_handler: OetcHandler | None,_job_uuid: str | None,_solved_model: Model | None
Lifecycle:
_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_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.
SolverReport. runtime = job_result.duration_in_seconds, solver_runtime = job_result.solving_duration_in_seconds. The orchestrator doesn't expose mip_gap / dual_bound / iterations — leave None.
Registration. Add SolverName.OETC = "oetc"; append "oetc" to available_solvers when _oetc_deps_available.
Public API:
m.solve("oetc", settings=oetc_settings, options={"solver": "gurobi", "Method": 2})
# or explicit:
solver = Oetc.from_model(m, settings=oetc_settings, options={...})
result = solver.solve()
m.apply_result(result)
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).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the Solver interface and OetcHandler in linopy/solvers.py, then inspect the OetcHandler branch in linopy/model.py:1626–1644 and the changes from #682. Define how the Oetc subclass lifecycle and optional async seam fit the existing API, then verify registration, deprecated remote handling, label-indexed result assembly, and the m.solve("oetc") and explicit Solver usage described here.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- gcp, python
- Domain
- backend, cloud
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100