refactor(remote): OETC/SSH as standalone transports (closes #683)#697
refactor(remote): OETC/SSH as standalone transports (closes #683)#697FBumann wants to merge 20 commits into
Conversation
|
@FBumann I think the concept makes sense. I would like to make sure that it is flexible enough for interfaces like gurobi instant cloud. could you take that into consideration? |
|
@FabianHofmann Yes, I already did think about it. But i can sketch it out a bit more, maybe even create a Remote parent class... |
It fits well for async instant cloud. For the sync instan cloud it doesnt fit as cleanly. But I think it can be wired in without adjusting public api. Probably just refactor some internal helpers for collection |
I guess async is the most important one |
I thin the regular gurobi cloud can be used by just using the regular gurobi solver anyway...? It fully dispatches to gurobipy, and gets a gurobipy solution back, right? SO no extra class needed... |
b221cc3 to
80e8be1
Compare
|
@FabianHofmann SO the new architecture should cover all cases. THe only issue i see is thst "remote" isnt a precise name. "worker" or "offload" might be better. But thats not as important for now i think |
Closes #683. The issue framed OETC as a `Solver` subclass to fold the `remote=` branch in `Model.solve` into the unified Solver pipeline. Trying that, the fit was wrong: remote handlers aren't solvers — they ship a netcdf elsewhere and let someone else solve. Forcing them through `Solver` required workarounds (a non-colliding `inner_solver` field name, property-vs-field collisions on `solver_name`, `SolverName` enum entries for things that aren't algorithms). Going standalone instead: - `linopy.remote.Oetc(settings, solver_name, options)` — standalone class with `upload(model)` / `submit()` / `collect(model)` / `solve(model)` lifecycle. The submit/collect split is in the right shape for future async work (a `blocking=False` solve, Gurobi-batch, etc.) without baking the seam into the Solver hierarchy. - `linopy.remote.SSH(settings, solver_name, options)` — synchronous ship-and-run handler. - Both produce a label-indexed `Result` via the shared `_scatter_solution_from_solved_model` helper in `linopy/remote/_common.py`. - Both validate the inner solver locally via `_validate_inner_solver` (unknown name raises; known-but-incapable raises before the round-trip). Settings dataclasses now pure transport. `OetcSettings.solver` and `OetcSettings.solver_options` are removed — those config axes live on the outer `Model.solve` call now, mirroring the local-solve API. New `SshSettings` follows the same shape. `Model.solve` changes: - `remote=<Settings>` → standalone-handler dispatch via the new `_solve_with_remote_settings` method. - `remote=OetcHandler/RemoteHandler` → legacy shim, emits `DeprecationWarning`, builds equivalent settings, routes to the same new pipeline. - New `model.remote` slot — set to the `Oetc`/`SSH` instance after a remote solve, lets callers introspect `model.remote._job_uuid` etc. `model.solver` is None during remote solves. The reformulation lifecycle (from #690) wraps the remote dispatch via `sos_reformulation_context` + `suppress_serialization_warning`, the same context managers the local-solve path uses. The `to_netcdf` UserWarning is suppressed for the handler's internal serialization. `OetcHandler.solve_on_oetc` emits a `DeprecationWarning` when called directly, pointing at the new API. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
80e8be1 to
daadcae
Compare
|
@FabianHofmann I argue that the final form of API would be removing
This reflects the 2-level state of regular solvers: Pass name + options in Do you agree? |
…eprecate legacy handlers - `OetcHandler.__init__` / `RemoteHandler.__post_init__` emit `DeprecationWarning` pointing at `Oetc` / `SSH` and `Model.solve(remote=...)`. An `_internal=True` kwarg suppresses the warning when the new classes construct the handler themselves. - `OetcHandler.solve_on_oetc` delegates to `Oetc.solve` so the upload→submit→poll→download orchestration lives in one place. Legacy `Model` return shape preserved by reading `oetc._solved_model` after `collect`. - `Oetc.upload` / `SSH.solve` no-op handler construction when one is already attached, so the deprecated handler can be reused as the underlying transport without re-running auth. - Validation moved into `Oetc.solve` (was in `upload`) so the legacy handler path is unchanged for users. Two `TestSolveOnOetc` tests grow a few mock attrs (`_xCounter=0`, empty `.items()`, `termination_condition`) so the bare `Mock()` model flows through `Oetc.collect`'s scatter step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the new `Oetc` / `SSH` standalone classes, the `Model.solve(remote=<Settings>)` entry point, and the deprecation of `OetcHandler` / `RemoteHandler`. Migration examples show both the recommended `Model.solve(remote=...)` path and the direct `Oetc.solve(m)` + `assign_result` path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`SshSettings.setup_commands` is a list of shell commands run on the remote interactive session before the inner solver is invoked — e.g. `setup_commands=["conda activate linopy-env"]`. Replaces the old pattern of holding a `RemoteHandler` instance and manually calling `.execute(...)`. The `examples/solve-on-remote.ipynb` notebook is rewritten to: - use `Model.solve(remote=SshSettings(...))` as the primary path, - demonstrate `setup_commands` for env activation, - show `SSH(settings, solver_name, options).solve(m)` as the advanced "drive the transport directly" path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drops `OetcHandler` cells (deprecated) — primary path is now
`Model.solve("gurobi", remote=OetcSettings(...), **opts)`.
- Removes the settings-level `solver=` / `solver_options=` cell;
inner solver name and options live at the call site, matching the
local-solve shape.
- Replaces the retry/error-handling cell with an "Advanced" section
that walks through `Oetc.upload` / `Oetc.submit` / `Oetc.collect`
— the async-friendly seam that motivates the standalone class.
- Trims to essentials.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rewritten notebooks dropped the notebook-level
`"nbsphinx": {"execute": "never"}` metadata, which both prior
versions had. Without it, the docs build tries to execute the cells
and fails on `os.environ["OETC_EMAIL"]` / a live SSH connect.
Restore the original metadata so the docs build returns to rendering
the notebooks as static content.
OetcCredentials was a 2-field wrapper (email, password) that added an extra construction layer with no functional payoff. Inline the two fields onto OetcSettings so the construction shape matches SshSettings (which takes username/password directly). OetcCredentials stays importable and emits a DeprecationWarning on construction; OetcSettings(credentials=...) is still accepted and copies the values through. To be removed in a future release. Note: the positional argument order on OetcSettings shifts because credentials is no longer the first required field. Existing keyword-arg callers (the typical case) are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… guide The two notebooks duplicated their model-creation cells and "Advanced: drive the transport directly" sections, while users picking a remote transport read one or the other — not both. Merge into a single `remote-machines.ipynb` with parallel SSH / OETC sections and a shared advanced section, plus a brief "which to pick?" table. Rename keeps the file out of the "solve-on-*" namespace (the docs section is already "Solving"); `remote-machines` describes what the page is about, not what you do with it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Switch the manual OetcSettings example from os.environ[...] to literal placeholder strings. Mixing os.environ access with the manual-construction example was confusing — environment loading is what from_env() is for. - Drop the SSH-vs-OETC comparison table at the end. The information is obvious from each section's 'What you need' bullets.
The `remote` extra installed only `paramiko` — i.e., the SSH transport deps. With OETC as a parallel transport (own `linopy[oetc]` extra), the `remote` name was misleading and asymmetric. Rename to `ssh` to match what it installs. Drop the old `remote` extra (rather than alias it) because: - It only shipped in v0.7.0 (recent, narrow adoption). - Pip extras have no runtime deprecation mechanism, so the alias would just defer an inevitable break. - Aliasing leaves a redundant extra in the API surface. Documented under "Breaking Changes" in the release notes; the merged remote-machines notebook is updated to use `linopy[ssh]`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `test/remote/test_remotes.py` covering the new public surface
that `test_oetc.py` and `test_ssh.py` don't (those still focus
on the deprecated Handler classes):
- `Oetc.solve` happy path with a mocked `OetcHandler`.
- `Oetc.upload` / `submit` / `collect` as separable steps.
- `SSH.solve` happy path; `SshSettings.setup_commands` runs on the
remote shell on first handler construction.
- Inner-solver validation (unknown name raises in both transports).
- `Model.solve(remote=OetcSettings(...))` / `Model.solve(remote=SshSettings(...))`
end-to-end with `Oetc.solve` / `SSH.solve` monkeypatched.
- Deprecation warnings on `OetcHandler`, `RemoteHandler`,
`OetcCredentials`, and `Model.solve(remote=<Handler>)`.
- `_internal=True` suppresses the handler deprecation warnings on
the construction path used internally by `Oetc` / `SSH`.
Also updates `test-notebooks` skip-list for the renamed merged
notebook (`remote-machines.ipynb` replaces `solve-on-{remote,oetc}.ipynb`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The API page only documented the deprecated `RemoteHandler`. Add the new public classes (`SSH`, `Oetc`, `SshSettings`, `OetcSettings`) and the remaining deprecated entries (`OetcHandler`, `OetcCredentials`) so autosummary generates a stub for each. The new entries link to the merged `remote-machines` user guide. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@FabianHofmann Ready for a review |
Resolved conflict in examples/solve-on-oetc.ipynb: the PR intentionally deletes this notebook (replaced by examples/remote-machines.ipynb), while master only applied cosmetic nbstripout/unicode normalization. Kept the deletion.
…ng text
A user passing m.solve("gurobi", remote=...) only ever supplies one
solver, and remotes are transports rather than solvers, so "inner" has
no "outer" to contrast with. Drop it from docstrings, validation error
messages, and release notes. Internal symbols (inner_solver param,
_validate_inner_solver) keep the name — there it disambiguates the
shipped solver string from the transport object.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@FabianHofmann ready. Do you wait for feedback of oetc users? |
The :class: target pointed at linopy.solvers.SSH, but SSH is exported from linopy.remote and is a transport, not a solver. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #683 — with a different design than the issue proposed.
Why standalone, not
SolversubclassThe issue framed OETC as a
Solversubclass soModel.solvecould fold theremote=branch into the unified Solver pipeline. Trying that, the fit was wrong:Solverrequired workarounds: a non-collidinginner_solverfield, property-vs-field collisions onsolver_name,SolverNameenum entries for things that aren't algorithms.Solver's feature-flag plumbing is about local solver capabilities; for remotes we want to validate the inner solver's flags.So
OetcandSSHare standalone classes inlinopy/remote/, parallel toSolver, not subclasses of it.What's new
solver_nameand**solver_optionsare the same axes as for local solves;remote=selects where to run.model.remoteattribute mirrorsmodel.solverfor post-solve introspection.Oetc.upload(model)/submit()/collect(model)exposed for async-style workflows (drive the seam manually).SshSettings.setup_commands: list[str]runs shell commands on the remote before the inner solver — replaces the old "build a RemoteHandler and call.execute(...)" pattern.remote→ssh(matches what it installs:paramiko). OETC has its ownlinopy[oetc]extra; both are now symmetric.linopy[remote]is dropped (not aliased) — see Breaking Changes in the release notes.API surface — what the new transport classes do (and don't) cover
OetcandSSHare intentionally narrower than the deprecated Handler classes. They cover solving a model on a remote machine. They don't try to be a general remote-shell wrapper.RemoteHandlerSSHsolve_on_remote(m)solve(m)handler.execute("conda activate …")SshSettings.setup_commands=[…]SSH._handleris cached)handler.execute(any cmd)RemoteHandler(deprecated) or use paramiko directlywrite_model_on_remoteetc.RemoteHandler(deprecated) or paramiko/SFTP directlyAsymmetry with
Oetc:Oetcexposesupload/submit/collectbecause OETC has a genuine async seam (submit a job, walk away, collect hours later). SSH doesn't — the shell session is held open and the solve is synchronous, so there's no useful split.The narrowing is intentional: the old
RemoteHandlerblurred two concerns — be a paramiko shell wrapper and solve linopy models on a remote. The newSSHis just the second. If you want the first, paramiko itself is a perfectly good API.Deprecations
OetcHandler(...)/RemoteHandler(...)construction emitsDeprecationWarning. Theirsolve_on_oetc/solve_on_remotereturn contracts are unchanged during deprecation;solve_on_oetcnow internally delegates toOetc.solve.Model.solve(remote=<Handler>)is deprecated — pass the settings dataclass instead.OetcCredentialsis deprecated — passemail=andpassword=directly toOetcSettings. TheOetcSettings(credentials=OetcCredentials(...))shape still works during the deprecation period.Other designs considered (rejected)
Remote returns
Modelinstead ofResult. Tried in a separate commit (f049999, since reverted):Oetc.solve/SSH.solvewould return the round-tripped solved Model, with a newModel._assign_from_solved_model(solved)helper for in-place writeback. The pitch: remote workers natively produce inflated Models, so theResultshape forces an xarray → flat → xarray round-trip via_scatter_solution_from_solved_model. Rejected in favor of uniformity: every solve path (local Solver or Remote) returns aResultand goes throughModel.assign_result. One assign-path, one mental model. Cost: the scatter pass — ~few ms + ~32 MB transient per 1M model variables, dominated by the network round-trip in practice.Keep
OetcCredentialsas a separate class long-term. Considered keeping it (cloud-SDK convention, visual flag for secrets in code review) and addingOetcCredentials.from_env()for symmetry withOetcSettings.from_env(). Folded instead because:email+password), so the wrapper adds no structural payoff.SshSettingsalready takesusername/passwordinline.OetcHandler, so the extra one-line migration (credentials=OetcCredentials(email=..., password=...)→email=..., password=...) is essentially free.Follow-ups (not in this PR)
OetcHandler/RemoteHandler. Their private transport methods (_upload_file_to_gcp,_submit_job_to_compute_service,wait_and_get_job_data,_download_file_from_gcp,__sign_in, paramiko shell management) still live on the Handler classes;Oetc/SSHreach intoself._handler._upload_file_to_gcp(...)to use them. A follow-up PR will migrate that code intoOetc/SSHand port the ~50 OETC tests that exercise the handler internals.OetcCredentials. After one release cycle of deprecation warnings.Oetc.solve(mocked OetcHandler),SSH.solve(mocked RemoteHandler), deprecation-warning assertions,upload/submit/collectseparability).Test plan
pytest test/remote/ test/test_sos_reformulation.py test/test_oetc_settings.py— 166 pass.pytest --ignore=test/remote— full broader suite (3495 passed, 29 skipped).ruff check,ruff format— clean.🤖 Generated with Claude Code
@FabianHofmann:
OetcCredentialsas a long-term, top-level class im fine. I went with fold because the deprecation cost happens to be near-free given the Handler is also deprecated, and .from_env() handles the credentials issue cleanly.