Skip to content

refactor: stateful Solver instances and two-step solve API#682

Merged
FabianHofmann merged 44 commits into
masterfrom
solver-refac
May 18, 2026
Merged

refactor: stateful Solver instances and two-step solve API#682
FabianHofmann merged 44 commits into
masterfrom
solver-refac

Conversation

@FabianHofmann

@FabianHofmann FabianHofmann commented May 13, 2026

Copy link
Copy Markdown
Collaborator

closes #628 #583

Changes proposed in this Pull Request

Refactor of the solver layer to put solver state on a stateful Solver instance and expose a clean construct-then-solve workflow.

  • Stateful Solver on Model.solver. Solver state (native model, results) now lives on a Solver instance attached to Model.solver. Model.solver_model and Model.solver_name become read-only properties delegating to model.solver (assigning anything but None raises; setting None closes the solver). Model.solver_name may be None before a solve. These two properties are candidates for future deprecation. The solver exposes an explicit close() (also auto-invoked from __del__) that releases the native handle and any held license.
  • Construct-then-solve API. Build a solver via Solver.from_name(name, model, io_api=..., options=...) (or SolverClass.from_model(model, ...)), then call solver.solve() to run and obtain a Result, and model.assign_result(result) to write the solution back. Solver is a dataclass; subclasses no longer need __init__ overrides.
  • Clear lifecycle hooks per solver. Each subclass overrides at most three methods: _build_direct(**kwargs) (build the native model from self.model), _run_direct(**kwargs) (run the prebuilt native model), and _run_file(**kwargs) (invoke the solver on self._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.
  • Legacy entry points deprecated. Solver.solve_problem, Solver.solve_problem_from_model, and Solver.solve_problem_from_file are kept on the base class as thin shims that route through the new pipeline and emit DeprecationWarning. To be removed in a future release.
  • Two ways to get the native solver model. Either via the module-level / Model-bound helpers (model.to_gurobipy(), model.to_highspy(), to_cupdlpx(model)), or directly via Solver.from_model(model, io_api=\"direct\").solver_model. The previous public Solver.to_solver_model method is removed and folded into the internal _build_direct hook to avoid exposing a third redundant path.
  • Declarative solver capabilities. Capabilities are declared as features: frozenset[SolverFeature] ClassVars on each Solver subclass (with an optional runtime_features() classmethod for version-dependent ones); query with Solver.supports(feature). SolverFeature is exported from linopy (and linopy.solvers); linopy.solver_capabilities remains as a back-compat shim with a lazy SOLVER_REGISTRY mapping and solver_supports() helper.
  • License-aware solver discovery. linopy.available_solvers is now lazy and no longer probes license-managed solvers (Gurobi, Mosek, Knitro, MindOpt, COPT, cuPDLPx) at import time — membership now means "package/binary installed", not "working license". Same lazy behaviour for quadratic_solvers. New linopy.licensed_solvers returns the installed solvers that currently pass a license probe. New linopy.solvers.check_solver_licenses(*names), plus per-class SolverClass.is_available() and SolverClass.license_status() returning a LicenseStatus dataclass (name, ok, message). All caches are refreshable via .refresh().
  • Result carries solver report. Result gains solver_name and report: SolverReport | None (runtime, MIP gap, dual bound, iteration counts) and prints them in __repr__. CBC, HiGHS, Gurobi, Knitro, and cuPDLPx populate report; when possible also populate the MIP dual_bound. SolverReport is exported from linopy.constants.
  • Positional-indexed solution mapping. Solution.primal and Solution.dual are now dense np.ndarrays indexed by linopy label (length = max_label + 1); masked or solver-dropped slots are NaN. Previously pd.Series keyed by name. Each solver emits arrays in this label-indexed form — direct-API solvers via cached _vlabels/_clabels populated at _build_direct time, file-based solvers via the shared _solution_from_names helper. Robust to solver iteration order and to solvers dropping unused variables (e.g. CPLEX on LP read). The dense numpy arrays are assigned to Model.solution by simple reshaping.
  • Cached constraint labels. Constraints exposes a cached label_index (mirroring Variables.label_index), so assign_result and IIS extraction no longer trigger a full model.matrices rebuild. Invalidated on add/remove. ConstraintBase gains active_labels() and a range property, and CSRConstraint exposes coords to support this without forcing a CSR build.
  • Common helpers. linopy.common gains values_to_lookup_array and ConstraintLabelIndex; the legacy pandas-keyed helpers series_to_lookup_array and lookup_vals are removed.

Reviewer requests

  • @FBumann: align SolverReport with SolverMetrics (feat: add unified SolverMetrics #583); expose dual_bound (best bound)
  • @coroa: make Solver a dataclass; drop the solver_class(**solver_options) pattern
  • @coroa: add Solver.from_name(name, model, io_api=..., options=...) static constructor + SolverClass.from_model(...) classmethod
  • @coroa: drop prepare_solver / run_solver two-step API
  • @coroa: cache vlabels / clabels on the solver instead of going through model.matrices, see vlabels-clabels-flow.html for more details
  • @coroa: adopt the Model.solve = self.solver = Solver.from_name(...); assign_result(...) pattern in Model.solve
  • @coroa: add is_available() classmethod per solver + lazy SOLVER_REGISTRY so available_solvers discovery doesn't grab licenses prematurely
  • @coroa: refactor OETC as a Solver subclass (Refactor OETC as a Solver subclass #683)
  • @coroa: keep the interface extensible for asynchronous solving (Gurobi batch optimization, OETC) — return early with a job handle and retrieve later (Refactor OETC as a Solver subclass #683)
  • @coroa: incremental update path — solver holds a shallow copy of the linopy model, solver.update(model) diffs and pushes only changed bounds/rhs/coefficients (CoW on variable/constraint data) (defer to follow-up pr)

Checklist

  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in `doc`.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes `doc/release_notes.rst` of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

FabianHofmann and others added 16 commits May 12, 2026 06:24
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.
@FBumann

FBumann commented May 13, 2026

Copy link
Copy Markdown
Collaborator

@FabianHofmann I suggest checking the new class SolverReport against my SolverMetrics in #583, the purpose is pretty much the same. This will also probably close #583.
One thing we definitely need is SolverReport.dual_bound (also known as best bound).
Peak memory was also added in #583, but might be less commonly exposed by the solver. I also dont really need it...

@coroa

coroa commented May 13, 2026

Copy link
Copy Markdown
Member

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:

Solver.from_name("gurobi", linopy_model, io_api=..., options=options)
# a staticmethod

which dispatches to

Gurobi.from_model(linopy_model, io_api=..., options=options)
# the specific classmethod, even though that is also implemented on the main Solver class

which dispatches according to io_api.

I'd say this then gets rid of any case for prepare_solver or some such.

@coroa

coroa commented May 13, 2026

Copy link
Copy Markdown
Member

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)

@coroa

coroa commented May 13, 2026

Copy link
Copy Markdown
Member

Solvers should get a class method:

class Gurobi:
    @classmethod
    def is_available():
        try:
            from gurobipy import Model
            with Model():
                  return True
         except ImportError, LicenseError?:
             return False

We then use a derived Collection pattern similar to the following for available_solvers so not to grab licenses prematurely.

https://github.com/snakemake/snakemake/pull/3900/changes#diff-857b2ff1aff916efdddad6bcd645a90f388276460cf75e6c9a0362745a70371fR13-R58

@coroa

coroa commented May 13, 2026

Copy link
Copy Markdown
Member

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.

@coroa

coroa commented May 14, 2026

Copy link
Copy Markdown
Member

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 solver.update(linopy_model) which does diffing and sends changes to solver.

several nice characteristics that way:

  1. you don't send changes prematurely (ie. immediately communicate any change on rhs assignment)
  2. sending changes is minimal/ incremental
  3. its compatible with putting solver connections into a separate thread
  4. update does not need to care whether it is effectively the same model or a completely newly constructed one
  5. memory use is efficient

@coroa

coroa commented May 14, 2026

Copy link
Copy Markdown
Member

@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

@FabianHofmann

Copy link
Copy Markdown
Collaborator Author

@FabianHofmann I suggest checking the new class SolverReport against my SolverMetrics in #583, the purpose is pretty much the same. This will also probably close #583. One thing we definitely need is SolverReport.dual_bound (also known as best bound). Peak memory was also added in #583, but might be less commonly exposed by the solver. I also dont really need 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.
FabianHofmann and others added 12 commits May 16, 2026 15:07
- Rename Model.apply_result() to Model.assign_result()
- Update all test function names to reflect new method name
- Update release notes documentation
Filters available_solvers by Solver.license_status().ok so tests and
runtime selection can skip installed-but-unlicensed solvers cleanly.
Exported from linopy. Tests parametrize over it instead of
available_solvers. Also fixes a case-mismatch in a knitro test regex.
doctest collection probed __wrapped__ via inspect.unwrap, triggering
imports of optional solver packages (pyscipopt, cupdlpx). Restrict
_LazyModule.__getattr__ to non-dunder names. Add narrowing asserts and
type fixes uncovered by mypy.
Comment thread doc/release_notes.rst

*Inspect the solver after solving*

* After ``model.solve()``, the solver object stays available on ``model.solver``. You can inspect it, reuse it, or release the underlying solver (and its license) by calling ``model.solver.close()`` or assigning ``model.solver = None``. It is also released automatically when the model is garbage-collected.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a great and important note for commercial usage, as the license is blocked otherwise. We might want to add a notebook about the new solver stuff, as an example with highs or gurobi...? If you want i can craf one, exploring the new feature on the way and you can add what i missed?

@FabianHofmann FabianHofmann May 18, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, sounds good!

@FabianHofmann

Copy link
Copy Markdown
Collaborator Author

@FBumann I am done with my features for this pr. another will follow for the persistent solve. since jonas is on holiday he won't be able to make another review here. do you want to take another look? focus could be breaking changes and whether flexopt tests pass with it (pypsa does). feel free to pull in the notebook here or to raise another pr in case you are able to quickly approve (no pressure anyway)

@FBumann

FBumann commented May 18, 2026

Copy link
Copy Markdown
Collaborator

@FabianHofmann No, i think a followup for the notebook is better anyway.
I'll let the flixopt test suite run on this PR now

@FBumann

FBumann commented May 18, 2026

Copy link
Copy Markdown
Collaborator

@FabianHofmann All tests pass.

@FabianHofmann

Copy link
Copy Markdown
Collaborator Author

wonderful! thanks @FBumann

@FabianHofmann
FabianHofmann merged commit 4daf611 into master May 18, 2026
21 checks passed
@FabianHofmann
FabianHofmann deleted the solver-refac branch May 18, 2026 08:10
FBumann added a commit that referenced this pull request May 18, 2026
Covers `model.solver` inspection, `SolverReport`, the construct-then-solve
API (`Solver.from_name(...).solve()` + `model.assign_result`), solver
capabilities via `SolverFeature`, and `available_solvers` vs
`licensed_solvers` (see #682). Also extends `create-a-model.ipynb` with
a small tail demoing `model.solver`, the report, and `solver.close()`,
linking forward to the new page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FabianHofmann added a commit that referenced this pull request May 19, 2026
* refactor(sos): add Model.apply/undo_sos_reformulation methods

Introduce a stateful pair of methods on Model that own the SOS
reformulation lifecycle:

- apply_sos_reformulation() stashes the reformulation token on the
  model (new _sos_reformulation_state attribute). Raises if already
  applied.
- undo_sos_reformulation() reads the stashed token and restores the
  original SOS form. No-op if nothing is applied.

Model.solve(reformulate_sos=...) now delegates to these methods rather
than threading the token through local state. The Solver path (which
was previously raising via Model.solve's pre-flight check) now gets a
clean ValueError directly from Solver._build() when an SOS-bearing
model is handed to a solver without native SOS support — making the
low-level API safe to use independently of Model.solve.

Persistence:
- copy() (and copy.copy / copy.deepcopy) carry the reformulation token
  with a deepcopy, so the copy is independently undoable.
- to_netcdf() raises if a reformulation is active; users must undo
  first to serialize a stable model state.

Context: motivated by the same investigation as #688 —
while reviewing the new Solver.from_model() API surface introduced by
#682, the SOS reformulation lifecycle stood out as load-bearing
orchestration that the Solver path couldn't reproduce.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(solver): validation, sanitize kwargs, and result wiring on Solver path (#691)

* refactor(solver): lift feature checks + sanitize/wiring to Solver path

Make Solver.from_name(...).solve() a real first-class entry point that
doesn't lose Model.solve()'s safety nets:

- Lift solver-feature gates into Solver._build() via a new
  _validate_model() hook: quadratic models against LP-only solvers and
  semi-continuous variables against solvers that don't support them.
  Removed the duplicate checks from Model.solve().
- Add sanitize_zeros / sanitize_infinities kwargs to Solver.from_model()
  (default True). The kwargs are processed in _build() before dispatch,
  so both file and direct io_apis honor them. Model.solve() forwards the
  kwargs through instead of pre-mutating the constraints itself.
- Extend Model.assign_result(result, solver=None) so the Solver-path
  canonical pattern works: solver = Solver.from_name(...); result =
  solver.solve(); model.assign_result(result, solver=solver). When the
  solver kwarg is provided, model.solver gets wired the same way
  Model.solve() wires it, so compute_infeasibilities() and friends keep
  working through the low-level API.

The empty-objective check stays on Model.solve() — to_gurobipy() /
to_highspy() and similar build-only converters legitimately work
against objectiveless models (gurobi/highs default to a zero
objective), so the check belongs at the actual submit point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* move empty-objective check to Solver.solve() for entry-point parity

The empty-objective UX guardrail was previously only on Model.solve(),
leaving the lower-level Solver.from_name(...).solve() path with a
silent gap. Move it to Solver.solve() — the actual submit primitive
that both entry points go through — so the same check fires regardless
of which API the user reaches for.

Build-time translate-only paths (to_gurobipy(), to_highspy(),
to_file()) are unaffected since they don't call solve(). The cost of
catching the error after build instead of before is bounded and only
hits a programming-error case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: parametrize empty-objective check across both entry points

Consolidate the Model.solve() and Solver.from_name(...).solve() tests
into one parametrized case — same check, two callers, one assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: collapse parametrize to a single test with two raises blocks

Same property tested twice — no need for separate test IDs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* preserve empty-objective check for remote-solve path in Model.solve()

The remote-solve branch in Model.solve() short-circuits to a
RemoteHandler before reaching Solver.solve(), so the check now in
Solver.solve() doesn't cover it. Restore the early raise in
Model.solve() so behavior is unchanged for all Model.solve() callers
(mock, remote, local) while Solver.solve() still covers direct-Solver
callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* move remote-path empty-objective check inside the remote branch

The early-position check was a workaround: the remote branch
short-circuits before Solver.solve() (where the canonical check now
lives), so empty-objective with remote=... wouldn't raise. Moving it
into the remote branch itself makes the intent local to where it's
needed, with a comment pointing at #683 where this duplication
disappears once OETC becomes a Solver subclass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* keep sanitize on Model; Solver.from_model() stays mutation-free

Remove the sanitize_zeros / sanitize_infinities kwargs from
Solver.from_model(). The Solver builder now never mutates the model.
Sanitization is exposed where it has always lived —
model.constraints.sanitize_zeros() / .sanitize_infinities() — and
Model.solve() calls them inline as part of its orchestration.

Rationale: model-state transformations should be Model-level primitives
(matches the SOS reformulation pattern from #690). The Solver's job is
to translate the model and run; it should not silently change the
caller's model on the way in. Users who go through the lower-level
Solver path apply sanitize explicitly when they want it.

Replaces TestSanitizeKwargs with TestSolverDoesNotMutateModel, pinning
the mutation-free invariant: building a Solver against a model with a
near-zero coefficient leaves model.constraints["c"].coeffs unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* address review: SOS hint, lp_only_solver fixture, assign_result doc

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Fabian <fab.hof@gmx.de>

* refactor(sos): tighten undo semantics and error hints

- undo_sos_reformulation() now raises if no state is applied (fail-fast)
- to_netcdf error no longer suggests poking the private state slot
- Solver._build runs _validate_model before _check_sos_unmasked so SOS on an
  LP-only solver surfaces the reformulate-first hint
- reformulate_sos_constraints docstring points at the stateful API

* fix(sos): auto-undo SOS reformulation when build/solve raises

`Model.solve(reformulate_sos=...)` left `_sos_reformulation_state` set if
`Solver.from_name`, `solver.solve`, or the file-cleanup `finally`
raised, since the undo lived in a second `try` around `assign_result`
that those failures never reached. The next solve then hit
`RuntimeError: SOS reformulation has already been applied`.

Wrap sanitize, build/solve, file cleanup, and assign_result in a single
outer try/finally so the undo always runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sos): support reformulation through remote/oetc netcdf path

`to_netcdf` previously raised when the model had an active SOS
reformulation, blocking `Model.solve(remote=...)` for users who passed
`reformulate_sos`. Beyond the raise, the remote branch was silently
ignoring `reformulate_sos` entirely.

Changes:
- `to_netcdf` warns (UserWarning) instead of raising; the reformulated
  MILP form is what gets serialized. The `_sos_reformulation_state`
  token is not persisted — it lives only on the in-memory caller's
  Model, where the apply/undo bracket keeps its lifecycle intact.
- `Model.solve(remote=...)` now brackets the remote dispatch with
  `apply_sos_reformulation` / `undo_sos_reformulation`, exactly like
  the local path. The `to_netcdf` warning emitted inside the remote
  helper is suppressed via `warnings.catch_warnings`.
- New `Model._resolve_sos_reformulation(solver_name, reformulate_sos)`
  helper deduplicates the should-reformulate decision between the local
  and remote branches and uses `solver_supports(...)` instead of the
  ad-hoc `getattr(solvers, SolverName(...).name)` pattern.
- `solver_name=None` with `reformulate_sos="auto"` now raises a sharp
  error pointing users at either passing `solver_name=...` or using
  `True`/`False` to skip the lookup. The local path is unaffected
  because its existing default (`solver_name = available_solvers[0]`)
  runs before the helper sees None.

Addresses the open thread on #690 from FabianHofmann.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(sos): fix mypy errors on remote-bracket and resolve tests

- Drop now-unused type: ignore on _resolve_sos_reformulation call
  where mypy correctly narrows (True, False, "auto") to bool |
  Literal["auto"].
- Type _fake_handler as RemoteHandler via cast so the three
  Model.solve(remote=handler, ...) calls satisfy the
  RemoteHandler | OetcHandler | None signature.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(sos): move reformulation lifecycle into remote handlers

* fix(types): tighten reformulate_sos to bool | Literal["auto"]

* test(ssh): cover SOS bracket in RemoteHandler.solve_on_remote

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Fabian <fab.hof@gmx.de>
FabianHofmann added a commit that referenced this pull request Jul 16, 2026
…685)

* docs: silence HiGHS console output in tutorial notebooks

HiGHS prints a banner + progress lines to the Python REPL on every
m.solve() call by default.  In a tutorial that calls solve many times,
this drowns the actual lesson in solver chatter.  Pass output_flag=False
(a HiGHS solver option forwarded via **solver_options) to suppress it.

Touches the four notebooks where solver_name="highs" is the only solver
invoked:

- create-a-model.ipynb
- create-a-model-with-coordinates.ipynb
- manipulating-models.ipynb (9 solves)
- transport-tutorial.ipynb

Left alone:
- infeasible-model.ipynb (uses Gurobi, kwarg is OutputFlag there;
  also showing solver feedback may be pedagogically relevant for
  infeasibility detection).
- solve-on-remote.ipynb / solve-on-oetc.ipynb (remote handler manages
  its own logging).
- piecewise-*.ipynb (already addressed in #677).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: silence HiGHS console output in piecewise tutorials too

Extends the log-silencing scope to the two piecewise tutorials, which
together call m.solve() nine times.  Same transformation as the other
notebooks — output_flag=False as a HiGHS-specific kwarg forwarded via
**solver_options.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: fix broken toctree, refresh API reference, and clean up references

- Add doc/coordinate-alignment.nblink so the index.rst toctree entry
  resolves to examples/coordinate-alignment.ipynb.
- Update api.rst to match the current public API: add the missing
  solver classes (COPT, Knitro, MindOpt, PIPS, cuPDLPx), expose
  top-level helpers (align, merge, options, EvolvingAPIWarning,
  PerformanceWarning), add the missing Model methods
  (add_sos_constraints, reformulate_sos_constraints,
  compute_infeasibilities, format_infeasibilities), add Variable
  methods (to_linexpr, fix/unfix, relax/unrelax), add sections for
  QuadraticExpression, Objective, and RemoteHandler, remove the
  duplicate Variables.integers, and fix the "hook" -> "hood" typo.
- contributing.rst: replace stale Black reference with ruff, correct
  the nblink example (proper JSON, right path, fixed RST indentation
  that was breaking pygments), and use pre-commit run --all-files.
- benchmark.rst: fix the rendered objective, which read as a product
  of two variables; corrected to the actual linear benchmark
  (2x + y with x - y >= i-1, matching benchmark_linopy.py).
- prerequisites.rst: add SCIP, give MOSEK a description, drop the
  dangling "-" after MindOpt, remove the outdated HiGHS-platforms
  claim, and clarify what the [solvers] extra actually pulls in.
- conf.py + index.rst: bump copyright to 2026 and fix the
  "contnuous" typo on the landing page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: reorganize toctree into basic→advanced sections and rewrite user-guide landing page

Split the previously flat 16-item User Guide bag into focused
sections so users move from install to advanced features in a clear
order:

  Getting Started → User Guide (core building blocks) →
  Advanced Features → Tutorials → Solving → Troubleshooting →
  Benchmarking → Reference

Rewrite user-guide.rst from a one-paragraph stub into a roadmap
landing page: it groups the core notebooks (variables, expressions,
constraints, coordinate alignment, manipulating models) into a
recommended reading order and points outward to advanced topics,
tutorials, remote/GPU solving, and troubleshooting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: fix broken admonitions in notebooks and configure intersphinx

Three RST admonitions in notebook markdown cells had a blank line
between the directive and a tab-indented body. CommonMark then ate
the indented block as a code block, so nbsphinx saw an empty
directive ("Content block expected for the …" build errors). Fix by
removing the blank line and using a 3-space indent — the convention
already used by the working admonitions in the same notebooks
(e.g. creating-variables cell `..note::\n   Since we did not …`).

- creating-expressions.ipynb cell 13: restored `.. important::` on
  coordinate-determination semantics.
- creating-expressions.ipynb cell 17: restored `.. tip::` pointing
  at `.add/.sub/.mul/.div` with the `join` parameter and the
  coordinate-alignment guide.
- creating-variables.ipynb cell 42: re-added a corrected `.. note::`
  on `coords=` being ignored when supplied alongside pandas objects.
  Dropped the stale "New in version 0.3.6" framing and the broken
  "is ignored is passed" wording from the original.

conf.py: configure intersphinx_mapping for python, numpy, pandas,
xarray, scipy, and dask. The intersphinx extension was already
loaded but had no mapping, so cross-references like
:class:`xarray.DataArray` or :func:`numpy.ndarray` were silently
unresolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: downgrade coordinate-determination admonition from important to note

The coordinate-determination behaviour is regular alignment semantics,
not a sharp pitfall — note is the right level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "docs: downgrade coordinate-determination admonition from important to note"

This reverts commit e44cbd3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: empty commit to retrigger CI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: rename toctree captions and reorder Examples below Solving

- Benchmarking → Comparisons. The two children (performance and
  syntax) both compare linopy with JuMP and Pyomo, not "benchmark"
  in the regression-tracking sense.
- benchmark.rst H1: Benchmarks → Performance comparison, so the
  page title matches the section framing (syntax.rst was already
  "Syntax comparison").
- Tutorials → Examples. The contents are end-to-end worked problems
  and a migration guide; "Tutorials" overloads with the rest of the
  docs (every notebook is tutorial-style).
- Move Examples below Solving so the section flow is
  Getting Started → User Guide → Advanced Features → Solving →
  Examples → Troubleshooting → Comparisons → Reference. The user
  now knows how to both build and run a model before being handed a
  full worked problem.

Update the user-guide.rst cross-reference accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: move Examples directly under User Guide

Sit Examples right after the core building blocks so users move from
"I learned the mechanics" to "show me a complete worked problem"
before tackling Advanced Features and Solving.

Final sidebar order:
  Getting Started → User Guide → Examples → Advanced Features →
  Solving → Troubleshooting → Comparisons → Reference

Reorder the corresponding bullets in user-guide.rst so the prose
matches the sidebar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: bridge Getting Started → User Guide and rename landing page H1

Three small fixes that tighten the boundary between the two top
sections without changing what each one covers:

- Append a "Where to next" cell to create-a-model-with-coordinates,
  pointing into the five User Guide notebooks. The coordinates
  notebook is deliberately a shallow tour, so the handoff is now
  explicit rather than implied by toctree order.
- Rewrite the user-guide.rst opening to acknowledge what the reader
  just did in Getting Started, framing the User Guide as the depth
  pass on the same surface.
- Rename the user-guide.rst H1 from "User Guide" to "Overview" so
  the sidebar entry under the "User Guide" caption no longer
  duplicates the caption name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: fix malformed first cell of solve-on-remote notebook

Cell 0 had two lines that contained literal "\n" sequences as text
(plus a stray trailing double-quote), so the markdown rendered as
one long line and Sphinx emitted four warnings about
"SSH:nbsphinx-math" file-not-found and inline interpreted text.

Rewrite the cell with proper newline-separated lines and turn the
inline reference to solve-on-oetc.ipynb into a :doc: cross-reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: use markdown links for cross-refs in notebook markdown cells

:doc: only resolves inside RST contexts (rst files and the body of
RST directives like .. tip::). In plain markdown cells nbsphinx
needs markdown links to the .ipynb files, which it then rewrites to
the rendered .html targets.

- create-a-model-with-coordinates: the new "Where to next" cell now
  uses [text](other.ipynb) for the five User Guide notebooks.
- solve-on-remote: the inline :doc:`solve-on-oetc` introduced in the
  previous fix is now a markdown link too.

Verified the build rewrites all five links to .html targets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: collapse "Where to next" to a single pointer at the User Guide overview

Listing the five User Guide notebooks at the end of the coordinates
notebook duplicated the bullet list the overview page already
maintains. Single forward pointer keeps the User Guide overview as
the source of truth and pushes the reader through the deliberate
intro on that page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: restructure api.rst — task-oriented top, classes under the hood, advanced bottom

Replace the flat alphabetic per-class dump with a layered structure
that puts the surface 90% of users reach for at the top, the
supporting classes in the middle, and the genuinely rare-use surface
at the bottom.

Task-oriented top sections (Model methods grouped by what the user
is doing):

  Creating a model
  Inspecting a model
  Modifying a model
  Solving
  Post-solve access     (Model post-solve accessors + status enums)
  Diagnostics
  IO
  Top-level helpers     (align, options)

Classes under the hood:

  Variable / Variables / LinearExpression / Constraint /
  Constraints / Objective / Piecewise

Each gets a small thematic split inside (Attributes / Operations /
Conversion / Post-solve / etc.) rather than an alphabetic dump.

Advanced section at the bottom for surface that most users will not
reach for:

  QuadraticExpression
  CSRConstraint
  Bulk variable operations (Variables.fix/unfix/relax/unrelax)
  Auto-reformulation (Model.reformulate_sos_constraints)
  Remote solving (RemoteHandler)
  Warnings (EvolvingAPIWarning, PerformanceWarning)

Curated to drop internal escape hatches and helpers:
- Model.to_gurobipy/to_highspy/to_mosek/to_cupdlpx (power-user
  escape hatches)
- Model.linexpr (arithmetic on Variables is the natural path)
- Constraints.format_labels, .coefficientrange (internal diagnostics)
- LinearExpression.from_rule, .from_constant (niche constructors)
- Constraint.freeze / .mutable, CSRConstraint.freeze / .mutable
  (CSR backend plumbing)
- ScalarVariable, ScalarLinearExpression (internal types)
- solvers.PIPS (stub that raises NotImplementedError)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: add docstrings for properties surfaced in api.rst autosummary tables

The new api.rst structure exposed 14 properties whose autosummary
table cell was blank because they had no docstring. Add a single-line
description to each:

- Model.is_linear / is_quadratic / type
- BaseExpression.vars / coeffs / const (fixes both LinearExpression
  and QuadraticExpression entries)
- Objective.is_linear / is_quadratic
- PiecewiseFormulation.method / convexity (added as inline attribute
  docstrings on the dataclass fields)
- OptionSettings class docstring (so the top-level `options` instance
  picks up a description)

PiecewiseFormulation: also dropped the duplicated literal value lists
from the class-level Attributes block and from the new attribute
docstrings, referencing the PWL_METHOD / PWL_CONVEXITY type aliases
instead so there is a single source of truth for the allowed values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: surface PWL_METHOD / PWL_CONVEXITY type aliases in api.rst

The :data:`PWL_METHOD` and :data:`PWL_CONVEXITY` references added
in the previous commit rendered as plain <code> rather than as
hyperlinks: their docs target didn't exist yet, and the role was
being looked up in the wrong module.

- Add docstrings to PWL_METHOD, PWL_METHODS, PWL_CONVEXITY, and
  PWL_CONVEXITIES in linopy/constants.py.
- List all four in api.rst under the Piecewise subsection so
  autosummary generates dedicated pages.
- Switch the cross-references in PiecewiseFormulation.method /
  .convexity and in constants.py to the fully-qualified form
  (:data:`~linopy.constants.PWL_METHOD`) so they resolve from
  whichever module they're rendered in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: dissolve the api.rst Advanced umbrella; each item gets a natural home

The Advanced section was a subjective bucket. Each entry now sits
where it belongs:

- QuadraticExpression moves into Classes under the hood alongside
  LinearExpression — it's a class with its own surface, not a
  power-user appendix.
- CSRConstraint moves into Classes under the hood next to Constraint —
  alternative storage backend, but a regular documented class.
- Variables.fix / unfix / relax / unrelax return as a Bulk modify
  subgroup under the Variables container.
- Model.reformulate_sos_constraints joins Modifying a model — it
  transforms the model in place.
- Remote solving and Warnings become small top-level sections of
  their own at the end of the page rather than nested under Advanced.

Preamble updated to drop the now-stale "Advanced section at the
bottom" reference and to signal the actual top-level structure
(task-oriented top, supporting classes below).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: keep page TOC depth 2; expand right-side TOC to L2 site-wide

Inline ``.. contents::`` directive on api.rst stays at depth 2 — the
page-top TOC is the executive summary, not a complete map.

For navigation into a section, bump sphinx-book-theme's
``show_toc_level`` to 2 in ``html_theme_options``. The right-side
"On this page" panel now shows H3 entries by default rather than
only expanding the section the user has scrolled into. Applies
site-wide; the other pages have shallow H3 structures so this is
a usability win across the board.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: rename "Classes and types" → "Other classes and types"

Makes the structural role explicit: these are the classes and types
not already covered by the task-oriented top sections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: standardise H3 subgroup vocabulary across api.rst class sections

Align the subgroup names that appear in multiple class sections so a
reader who learns the scheme once can scan across classes. Six shared
labels emerge — Structure, Construction, Modification, Operations,
Conversion, Post-solve access — used wherever they fit.

- Variable: "Modifying state" → "Modification" (match Variables,
  Constraints).
- LinearExpression: "Building blocks" → "Structure" (match
  Constraint, QuadraticExpression below); "Manipulation" →
  "Operations" (match Variable).
- QuadraticExpression: replace the flat list with the same subgroup
  shape as LinearExpression — Structure, Conversion, Post-solve
  access.
- CSRConstraint: replace the flat list with the same subgroup shape
  as Constraint — Structure, Post-solve access, Conversion.

Also move the solver-status enums out of Post-solve access into
their own subsection (Solver status and result types) under Other
classes and types: SolverStatus / TerminationCondition / Status /
Solution / Result are types you compare Model.status against, not
accessors.

Model keeps its own task vocabulary (Building / Inspecting /
Modifying / Solving / Diagnostics / IO / etc.) because it's
structurally different from the data-type classes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: revert show_toc_level=2 in book-theme options

Roll back to the default (1). The right-side page TOC will only
expand the section the reader has scrolled into, not all H3 entries
across the page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: restructure api.rst as Model-first and tighten the curated surface

Two changes in one pass:

(1) Make api.rst per-class throughout. Drop the artificial split
between task-oriented top sections and "Other classes and types".
Model becomes the first H2 (with its task labels — Building /
Inspecting / Modifying / Solving / Post-solve / Diagnostics / IO —
as H3 subsections), and every supporting class becomes a sibling
H2 of Model. Preamble rewritten to frame the page: Model is the
entry point; supporting classes document the types reached via
``model.<attr>`` accessors.

(2) Tighten the surface further. Drop entries that are internal,
implicit-by-arithmetic, or trivial:

  - Variables.add / .remove, Constraints.add / .remove — internal
    mechanisms used by model.add_*; users never call them.
  - Variable.sanitize, Constraints.sanitize_missings — internal
    cleanup helpers in the solver pipeline.
  - LinearExpression.to_constraint, QuadraticExpression.to_constraint
    — almost always implicit via <=, >=, == on expressions.
  - LinearExpression.to_quadexpr — niche conversion only meaningful
    if you're already deep in quadratic forms.
  - Model.get_problem_file / .get_solution_file — debugging-only
    temp-file accessors.
  - solvers.Solver (abstract base, never instantiated) and
    solvers.quadratic_solvers (trivial list).

With Solver and quadratic_solvers removed, the "Solver interface"
section only contained available_solvers, so collapse it into a
single "Solvers" section with the implementation classes.

Also reordered the H3 subgroups inside each class so the most-used
entries appear first. Post-solve access leads on the expression /
constraint classes (the .solution and .dual readers are typically
what a reader is looking for); Aggregate access + Bulk modify lead
on the Variables container.

Top-level helpers moved to the bottom as "Utilities" (align,
options) — they're stragglers, not entry points.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: move piecewise construction helpers into the Piecewise section

The piecewise construction helpers (breakpoints, segments, Slopes)
were previously listed under Model > Building a model because they
are used alongside Model.add_piecewise_formulation. Move them into
the Piecewise section instead — they live in linopy.piecewise and a
reader looking for "how do I build a piecewise formulation" expects
everything piecewise in one place.

Split the Piecewise section into four small subsections (Construction
helpers / PiecewiseFormulation / Low-level helper / Type aliases) so
the helper functions, the return type, the standalone tangent_lines,
and the PWL_METHOD / PWL_CONVEXITY type aliases all sit under
clearly labelled groups instead of one mixed flat list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: harmonise Variables container labels with Variable; fix preamble cross-refs

- Rename Variables container subgroups so they match Variable's
  vocabulary where the role is the same:
    "Aggregate access" → "Attributes"
    "Bulk modify"      → "Modification"  (the "Bulk" qualifier is
                                          clear from context — the
                                          methods live on the
                                          container)
    "Inventory by type" → "Inventory"    (matches Constraints'
                                          equivalent subsection)
- Drop the redundant prose note "Container-wide analogues of
  Variable.fix, etc." — same reason; clear from context.

Also fix the preamble cross-references. The autosummary entries
register the documented entities under their full module paths
(linopy.model.Model, linopy.variables.Variable, …), so the bare
``:class:`Model``` refs were rendering as plain styled code rather
than hyperlinks. Use the qualified ``~linopy.<module>.<name>`` form
for class refs (display stays "Model" / "Variable" / "Constraint" /
"Objective") and the ``Target <full.path>`` form for methods and
attributes so the link text reads "Model.add_variables" etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: api reference link

* docs: add working-with-solvers notebook for the stateful Solver API

Covers `model.solver` inspection, `SolverReport`, the construct-then-solve
API (`Solver.from_name(...).solve()` + `model.assign_result`), solver
capabilities via `SolverFeature`, and `available_solvers` vs
`licensed_solvers` (see #682). Also extends `create-a-model.ipynb` with
a small tail demoing `model.solver`, the report, and `solver.close()`,
linking forward to the new page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(solvers): drop hardcoded SolverReport coverage list

The previous wording enumerated CBC/HiGHS/Gurobi/Knitro/cuPDLPx as the
solvers populating SolverReport, which would silently go stale as
backends fill in more fields. Replace with a general statement so the
notebook doesn't need to be touched every time a solver wires up a new
field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(solvers): strip stray notebook outputs

Re-run in PyCharm left outputs and ExecuteTime metadata in the file;
strip to match the repo's jupyter-notebook-cleanup convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(solvers): trim redundant cells, silence HiGHS logs

- Drop the `inspect-name`, `Highs.from_model` markdown aside, and
  `check_solver_licenses` cells (each duplicated something said better
  elsewhere).
- Pass `output_flag=False` to `m.solve(...)` and the direct-API solver
  options so the rendered notebook isn't dominated by HiGHS chatter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(solvers): surface the stateful Solver API in api.rst

Adds a structured Solvers section covering Solver.from_name /
from_model, the two-step build/solve flow, post-solve state
(status/solution/report/solver_model), capability probes, and the
licensed_solvers / SolverFeature / LicenseStatus helpers. Pulls
SolverReport into the result-types section now that it is exposed
on Solver.report and Result.report.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(solvers): rename notebook to using-solvers

Renames the notebook from working-with-solvers to using-solvers to
match the existing topic-page convention (creating-variables,
creating-constraints, manipulating-models, ...) and to read more
naturally inside the Solving toctree alongside solve-on-remote,
solve-on-oetc, and gpu-acceleration.

- Renames examples/working-with-solvers.ipynb -> examples/using-solvers.ipynb
- Renames doc/working-with-solvers.nblink -> doc/using-solvers.nblink
- Updates the toctree entry in doc/index.rst
- Updates the cross-link in examples/create-a-model.ipynb
- Updates the notebook's top-level heading

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(solvers): align notebook style with sibling notebooks

- Title-case all headings (H1 and H2) to match creating-variables,
  creating-constraints, manipulating-models, etc.
- Drop code-embedded and "Advanced:"/"?" stylings in section names
  ("The Recommended Path", "The Construct-Then-Solve API",
  "Querying Solver Capabilities", "Listing Installed Solvers").
- Replace HTML entities (&mdash;, &hellip;) with Unicode characters.
- Split the tuple display cell so the objective value and the solution
  table render on separate cells, matching how the other notebooks
  surface multiple results.
- Soften two reference-style sentences ("we'll use a tiny LP throughout",
  "Note that not every backend ...") to match the walk-along tone of
  the rest of the docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(solvers): restructure notebook around the common-case flow

Reorders the notebook so it follows the order a user actually
encounters: solve, inspect, then check which solvers are installed,
and only finally drop into the construct-then-solve API. Drops the
``SolverFeature`` section — capability gating is an advanced concern
already covered in the API reference under ``Solver.supports`` /
``solvers.SolverFeature``. Also drops the trailing ``Summary``
section, since none of the sibling notebooks use one.

New section order:

1. A Small Example Model
2. The Recommended Path (``model.solve(...)``)
3. Inspecting the Solver
4. Listing Installed Solvers
5. The Construct-Then-Solve API

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(solvers): document Model-side transformation lifecycle

Adds a "Model Transformations" section to the using-solvers notebook
covering SOS reformulation and constraint sanitization. Frames the
layering explicitly:

- Transformations live on the Model — not on the Solver.
- Model.solve(reformulate_sos=, sanitize_*) is a convenience that
  brackets these around the one-shot solve.
- The two-step Solver.from_name(...).solve() path does NOT auto-bracket;
  the caller invokes apply_sos_reformulation / sanitize_* themselves
  and undoes the SOS reformulation afterwards if they want the model
  back in its original form.
- The Solver only declares feature support and raises during _build()
  on incompatible state.

Includes a TODO note flagging that the same layering will apply when
further transformations (validation, etc.) move onto the Model.

Also:
- api.rst: surface Model.apply_sos_reformulation /
  Model.undo_sos_reformulation in the "Modifying a model" autosummary.
- sos-constraints.rst: switch the "manual reformulation" example from
  the lower-level reformulate_sos_constraints() call to the new
  stateful apply / undo pair, matching the documented lifecycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* docs(solvers): drop TODO note, use try/finally for SOS apply/undo

- Remove forward-looking note from the markdown cell (belongs in PR
  description, not the rendered notebook).
- Switch the two-step example to bracket apply/undo with try/finally so
  an exception from Solver.from_name / solver.solve doesn't leave the
  model stuck in reformulated form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(notebooks): strip ExecuteTime / execution metadata

Remove the PyCharm-flavored "ExecuteTime" cell metadata and Jupyter's
"execution" cell metadata from create-a-model.ipynb and
using-solvers.ipynb. These fields record per-cell start/end timestamps
that mutate on every notebook re-run and bloat the diff without adding
review value.

Drops ~180 noise lines from the PR diff vs master.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Fabian Hofmann <fab.hof@gmx.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Solver Refactor and Extension

3 participants