PyPSA / PyPSA/linopy

Refactor OETC as a `Solver` subclass

Open
#683 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

refactor solver interface
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.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.
  • Aligning OETC with the Solver interface 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 via OetcSettings.from_env() if absent)
  • Runtime: _handler: OetcHandler | None, _job_uuid: str | None, _solved_model: Model | None

Lifecycle:

  • _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.

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.