Skip to content

Commit 2bbbeef

Browse files
FabianHofmannpre-commit-ci[bot]FBumannclaude
authored
feat(persistent): in-place solver updates (Solver framework + HiGHS/Gurobi/Xpress/Mosek) (#718)
* feat(constraints): add _coef_dirty flag + rhs setter short-circuit Tracks per-Constraint coefficient mutation via a single boolean slot, flipped in coeffs/vars/lhs setters. Pure-constant rhs writes now short-circuit and leave coeffs/vars buffers untouched (by identity), so rhs-only updates don't trigger expensive coefficient recompare on the persistent-solver fast path. * feat(persistent): add ModelSnapshot, CoefPattern, StructuralKey Pure-Python snapshot primitives for the persistent-solver Phase 1. Deep-copies value-side fields (var_lb/ub, con_rhs/sign, obj_linear), holds vlabels/clabels by reference, stores canonical CSR (indptr, indices) per constraint container. No Solver import. * feat(persistent): add ModelDiff and compute_diff Pure-function diff for the persistent-solver Phase 1. Detects structural, coord, sparsity, quadratic-objective, value-only var/con, and objective-linear/sense changes. Supports same_model fast path via _coef_dirty and cross-model full re-scan. Includes a focused test suite covering capture, mutation paths, deep-copy invariant, and the same_model toggle. * feat(solvers): add persistent-update orchestration to Solver - supports_persistent_update class flag (default False) - snapshot/_rebuilds/_in_place_updates/_last_rebuild_reason fields - snapshot capture at end of direct _build, _clear_coef_dirty helper - apply_update stub raising UnsupportedUpdate - solve(model, assign) dispatcher with diff-or-rebuild path - update(model, apply=True) primitive returning ModelDiff - threading.Lock around diff+apply+resnapshot - __getstate__/__setstate__ drop native handle and snapshot * test(persistent): smoke test Solver orchestrator with fake backend * feat(solvers): short-circuit rebuild when backend lacks persistent-update support Skip diff computation entirely when supports_persistent_update is False on apply, per plan: 'dispatcher checks flag before calling — if False, skips diffing entirely and goes to rebuild.' * feat(solvers): Gurobi apply_update for persistent solves * feat(solvers): HiGHS apply_update for persistent solves * test(persistent): cross-model, pickle, threading, failure-path coverage * refactor(persistent): row-block numpy snapshot/diff Replace xarray-based snapshot and CSR pattern compare with per-row canonicalised numpy buffers; new ContainerVarUpdate / ContainerRowUpdate payloads. Gurobi/HiGHS apply_update rewritten around batched setAttr / changeColsBounds / changeColsCost / changeColsIntegrality; coefficient writes touch only changed cells. Cross-model diff now ~matches same-model cost for bound/rhs/coef-value sweeps. * feat(persistent): opt-in coord-equality via ignore_dims compute_diff/Solver.solve/Solver.update grow an ignore_dims kwarg. None (default) keeps the current no-coord-check behaviour; any iterable opts into per-container coord-equality on every dim not in the set, supporting rolling-horizon workflows where e.g. the snapshot dim is expected to drift. * feat(persistent): lazy-build Solver, ModelDiff constructors, disallow_rebuild - Solver.from_name now accepts model=None; the first solve(m, ...) builds. - compute_diff folded into ModelDiff.from_snapshot classmethod; new ModelDiff.from_models diffs two linopy models directly. - Solver.solve grows disallow_rebuild=True, which raises RebuildRequiredError instead of falling back to a rebuild. * feat(persistent): opt-in update tracking, snapshot-free ModelDiff.from_models - Add `track_updates` flag (default False) to Solver; skip ModelSnapshot capture when disabled. Raise UpdatesDisabledError on solve(model)/update() if a built solver was constructed without tracking. - Rewrite ModelDiff.from_models to build directly from two models without capturing snapshots; share helpers with from_snapshot. - Update persistent tests to opt into track_updates=True; add coverage for the disabled path. * feat(persistent): wire ModelDiff.from_models for track_updates=False Cross-instance resolves now diff via from_models against the previously built model, with no snapshot. Same-instance mutation still raises UpdatesDisabledError. Snapshot recapture is skipped in this mode. Add cross-instance solve/update tests for the no-snapshot path. * refactor(persistent): cleanups, VarKind enum, fold _clear_coef_dirty Collapse _diff_objective QUAD_OBJ branches; cache n_coef_updates; short-circuit _canonicalize_rows when rows already sorted; tighten buffer extraction. Introduce VarKind enum used across snapshot/diff and HiGHS/Gurobi apply_update; reuse linopy.constants sign tokens. Move _clear_coef_dirty into ModelSnapshot.capture. * refactor(persistent): CSR-backed ContainerConBuffers Source con buffers from Constraint.to_matrix_with_rhs, replacing the dense (n_rows, max_n_term) arrays with CSR (indptr, indices, data). Sign dtype adopts 'U1' across the persistent layer and apply_update in HiGHS/Gurobi consumes CSR-slice payloads instead of -1 masks. Deletes _canonicalize_rows and the _INT64_MAX sentinel. * refactor(persistent): flat-native ModelDiff storage Replace per-container ContainerVarUpdate/ContainerRowUpdate dicts with flat arrays (var_bounds_*, var_type_*, con_coef_* COO, con_rhs_*, con_sign_*) plus VarSlice/ConSlice per-container offsets for diagnostics. Add con_rhs_as_bounds() for ranged-row solvers. Backend apply_update bodies collapse to flat-array calls; remove duplicated label->position resolution. * feat(persistent): apply_update for Xpress and Mosek Implement in-place model updates for Xpress (chgbounds/chgrhs/chgmcoef/ chgrowtype/chgobj/chgobjsense/chgcoltype) and Mosek (chgvarbound/ chgconbound/putaijlist/putclist/putvartypelist/putobjsense). Mosek rejects constraint sign change to trigger rebuild. Consolidate gurobi/highs apply_update tests into a single parametrized file that also covers xpress and mosek. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(persistent): serialize concurrent solves; satisfy mypy * hold solver lock through _run_direct so two threads calling solve(model) on the same Solver no longer race on the native handle (HiGHS returned 0.0 from the second concurrent solve). * narrow Optional ndarrays in persistent.diff.push_var / push_con and in HiGHS/Gurobi/Xpress/Mosek apply_update objective paths. * widen Constraint.rhs setter to ExpressionLike | VariableLike | ConstantLike to match the as_expression call in the body. * widen Constraints.__getitem__(str) return type to Constraint (the dominant case) so tests can set .rhs/.coeffs/.sign without ignores. * add docs for in-place solver updates. * harden coords comparison * Variable.update() / Constraint.update() as canonical mutation API (#727) * feat: Variable.update() / Constraint.update() as canonical mutation API Introduces typed ``.update()`` methods on Variable and Constraint as the single, validated, multi-attribute mutation entry point. - ``Variable.update(lower=, upper=)``: validates non-constant inputs are rejected, new dims are rejected, and the resulting ``lower <= upper`` invariant holds across all coords. Returns ``self`` for chaining. - ``Constraint.update(rhs=, sign=)``: constant RHS only. The legacy ``c.rhs = variable`` rearrange-to-lhs path stays on the setter (different semantic, deserves its own explicit method). The existing ``.lower`` / ``.upper`` / ``.sign`` setters become thin shims that forward to ``.update()``, so single-attribute writes (``z.lower = 2``) stay ergonomic and the canonical validation runs in one place. The ``.rhs`` setter forwards constants through ``.update()`` and keeps the expression-rhs rearrange behaviour. This is the on-top experiment for the design discussion on #718. ``.lhs`` / ``.coeffs`` / ``.vars`` setters are untouched for now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(update): Constraint.update accepts Variable/Expression rhs Mirrors the existing ``c.rhs = expr`` setter and ``add_constraints`` which both accept mixed-side input and rearrange the residual onto lhs. ``c.update(rhs=x + 5)`` now subtracts ``x`` from lhs and stores ``5`` on rhs. ``.rhs`` setter collapses to a one-line shim. Variable bound rejection of Variable/Expression is kept (bounds are numeric, not symbolic); docstring clarified to spell out that pandas / xarray / numpy arrays are first-class (time-varying bounds). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(update): extend Constraint.update to lhs/coeffs/vars; shim all setters Adds lhs / coeffs / vars to the canonical mutation API. All .lhs / .coeffs / .vars setters now forward to .update() — every Constraint mutation goes through one method with one validation path, one place that flips _coef_dirty. Composition rules: - lhs= replaces the whole expression first; subsequent rhs= rearrangement (Variable/Expression in rhs) sees the new lhs. - lhs= and coeffs= / vars= are mutually exclusive (whole replacement vs partial array update). - sign= is applied last so it composes cleanly. Internal Constraint.sanitize_zeros migrated to update(vars=, coeffs=) — no more internal setter calls in linopy/. 389 tests pass across mutation + persistent-solver suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(update): rename Constraint.update kwarg vars= -> variables= Avoids shadowing Python's vars() builtin. The .vars attribute on Constraint stays (it parallels the .data.vars internal name); only the kwarg gets the unambiguous spelling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(update): accept positional ConstraintLike in Constraint.update Mirrors add_constraints' dispatch: c.update(x + 5 <= 3) is now shorthand for c.update(lhs=x, sign='<=', rhs=-2), extracted from the AnonymousConstraint / ConstraintBase the comparison produces. Mutually exclusive with the per-attribute kwargs; clear error when mixed. Also reverts the internal sanitize_zeros migration. The setters are pure shims forwarding to update(), so the migration didn't change behaviour or cost — just spelling. The original setter syntax reads more naturally there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(update): note kwarg form is the targeted, cheap path The positional ConstraintLike form (c.update(x + 5 <= 3)) always rewrites lhs / sign / rhs and flips _coef_dirty. For hot loops that only touch one part, kwarg form (c.update(rhs=...)) skips the unchanged attributes and is materially cheaper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(persistent): default ModelDiff.from_snapshot(same_model=False) Closes the A1 residual from the #718 review. The flag-trust path (`skip_coef_compare = same_model and not coef_dirty`) is correct through Constraint.update() (set in one place, shims forward), but `c.coeffs.values[...] = ...` still bypasses _coef_dirty. With same_model=True as the default, that bypass silently produces wrong diffs. Flip the default to False. Cross-model paths (the only production caller, Solver._update_locked, passes explicitly) are unaffected. Same-model warm-update paths now value-diff the CSR data — small perf hit (50-200ms at Mayk-scale per Mayk's bench), correct by default. Solver-aware callers who own the mutation contract can opt back into the optimization with `same_model=True`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: teach .update() in tutorials; mark setters as syntactic sugar - examples/manipulating-models.ipynb: rewrite mutation cells to use Variable.update / Constraint.update; setter form is mentioned in notes as syntactic sugar for the same call. - examples/creating-constraints.ipynb: reframe the CSRConstraint vs Constraint API table around .update() as the mutation API; setters are sugar. - Setter docstrings now say 'syntactic sugar for Constraint/Variable .update; do not add logic here so the contract stays single-sourced' — a directive to future contributors as much as to readers. No deprecation, no breaking change. .update() is the documented canonical mutation API; the seven setters continue to exist as one-line shims. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * deprecate(update): warn on mutation setters; promote .update() in docs Adds DeprecationWarning to all seven mutation setters (Variable.lower, Variable.upper, Constraint.coeffs, Constraint.vars, Constraint.sign, Constraint.rhs, Constraint.lhs). Each setter still forwards to .update() so existing code keeps working; the warning points at the canonical API. Internal sanitize_zeros migrated off setters (the last linopy/ caller). api.rst gains Modification sections listing .update() for both Variable and Constraint; tutorial notes rewritten to teach .update() and flag setters as deprecated. Release note added. dual.setter / solution.setter untouched — result assignment, not mutation, different deprecation track. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(update): edge-case coverage; document rhs-rearrangement invariant Constraint.update tests: lhs-only, coeffs-only (vars preserved), compound lhs+sign, mutually-exclusive lhs+coeffs and lhs+variables. Variable.update tests: upper-only, valid array bound. Migrate test_constraint_coef_dirty.py from the now-deprecated setters to .update(), exercising the canonical path; add positional-form and no-op cases. Net effect: same dirty-flag invariants, 7 fewer warnings per pytest run. Docs: Constraint.update rhs= gains a worked example showing the two forms (constant vs variable/expression). add_constraints rhs gets a matching note pointing at the linopy invariant so the rearrangement rule is documented at the creation site too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review(update): address inline feedback on #727 - Constraint._assign_lhs_expr → _assign_lhs (drop redundant suffix; the method already takes a LinearExpression, so the type was in the signature, not the name). - Add Constraint._assign_data(**fields) helper. Wraps the four ``self._data = assign_multiindex_safe(self.data, **kw)`` callsites inside update() (rhs / coeffs / vars / sign). Untouched: the same pattern in dual.setter, sanitize_missings, sanitize_infinities — those aren't update() and stay out of scope here. - Add types.CONSTANT_TYPES tuple, derived from ConstantLike via get_args so the two cannot drift. Variable.update bound validation flipped from negative (isinstance against a hand-rolled non_constant tuple) to positive (isinstance against CONSTANT_TYPES); drops a redundant in-function ``from linopy import expressions`` (the module-level import already covered it). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * deprecate(update): FutureWarning on DataArray as variables=; extract _validate_update Constraint - sanitize_zeros now writes _data via _assign_data directly (no longer round-trips through update(variables=DataArray), which would self-trigger the new deprecation warning). - Constraint.update(variables=...) emits FutureWarning when passed a raw DataArray of integer labels; Variable is the supported input. The path stays accepted for back-compat and will be removed alongside the v1 cleanup. Variable - Extract Variable._validate_update(*, lower, upper) — validates, broadcasts, and runs the cross-bound (lb<=ub) check, returning a dict ready for assignment. update() body shrinks to ~3 lines. Coord validation (parity with add_variables) deferred to #726 land. Tests - test_constraint_coef_dirty's variables= test now passes a Variable instead of a DataArray (matches the supported input). - test_constraint_vars_setter_with_array wrapped in pytest.warns(FutureWarning) — locks in the deprecation contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(constraints): _assign_data → _update_data; manage _coef_dirty inside The helper now writes fields AND flips _coef_dirty when the written set includes coeffs or vars. Callsites in update() (coeffs / vars branches) and sanitize_zeros drop their explicit `self._coef_dirty = True` lines — the rule lives in one place, can't be forgotten by future field additions. rhs / sign writes still don't dirty (correctly). _assign_lhs is untouched: it uses a different write mechanic (drop_vars + assign) and manages its own flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * revert(update): rename Constraint.update kwarg variables= back to vars= Restores symmetry with Constraint.vars (property) and data.vars (underlying xarray Dataset key) — the original rename traded one small asymmetry (read vs. mutate kwarg) for a worse one (Python property name vs. Dataset key name). The `vars()` builtin shadowing inside the kwarg position is benign: we never call `vars()` here, and dropping the rename also lets the top-level `linopy.variables` module be used directly inside the function body instead of importing `Variable as _Variable` to dodge the kwarg shadow. Closes #730. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * restore(update): keep Constraint.update kwarg as variables= Reverts cf845e1. Under the A/B framing for the .vars naming question, update(variables=...) is Category A — it accepts a linopy.Variable. The previous "restore symmetry with .vars / data.vars" argument conflated two layers: - Public Python API speaks about linopy variables → variables= - Internal xarray Dataset key stays as "vars" (xarray collision on Dataset.variables blocks renaming the key) The asymmetry between property/kwarg name and Dataset key name is principled (API layer vs. storage layer), not arbitrary — same pattern ORMs / serializers use. Keeping variables= here lines up with the broader .vars → .variables direction now being considered for properties. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: ensure dims in solution assignment * refactor(persistent): address review round ModelDiff.from_snapshot/from_models return RebuildReason on rebuild (NONE dropped); diff walkers moved onto _DiffBuilder with context in __init__, single _cat helper. Snapshot buffers share constraint arrays (identity fast path); CSRConstraint.sanitize_zeros copy-on-write. Use isinstance(val, ConstantLike) in Variable._validate_update. * fix: CI failures after master merge Variable.fix()/unfix() set both bounds atomically via update() instead of sequential deprecated setters (tripped new lower<=upper cross-validation). Fail fast on quadratic lhs in Constraint.update; type-narrowing fixes for mypy. * refactor(persistent): lazy COO expansion of coefficient diffs ModelDiff stores per-container _CoefDelta (changed rows referencing the CSR buffers); con_coef_rows/cols/vals materialize on first access via cached property. Expansion is now vectorized; backends guard on n_coef_updates. Follows coroa's suggestion on PR 718. * refactor(persistent): assemble snapshot from diff walk; pure capture (A2+A6) _DiffBuilder records target buffers/coords; ModelDiff.snapshot replaces the O(nnz) re-capture after in-place updates. ModelSnapshot.capture no longer mutates the model: the _coef_dirty clear moves to the solver, coupled to snapshot adoption (build + successful apply, never on apply=False). * refactor(persistent): template-method apply_update (A4, D2-D4) Base Solver orchestrates the diff sections and validates up front (sign support; Mosek semi-continuous now fails before any native mutation); backends implement _apply_* hooks. Binary [0,1] re-clamp lifted to base with Gurobi no-op (VType 'B' implies bounds natively). self.sense now set uniformly; HiGHS vtype map cached; Xpress/Mosek list-conversion helpers. * refactor(persistent): lock-free diff preview, document solve atomicity (A5) update(model, apply=False) computes the diff without the solver lock (immutable snapshot buffers, same_model=False since _coef_dirty cannot be trusted concurrently). solve keeps the coarse lock: apply->run must be atomic and native handles are not thread-safe. Tests pin the non-blocking preview and the preview/apply asymmetry for raw .values mutations. * refactor(persistent): split diff/apply, namedtuple ctx (coroa review) Replace _update_locked(apply=...) with _compute_diff + _apply_locked, dropping the dead apply=False branches. Read supports_* off the instance and give Gurobi's apply context a _GurobiApplyCtx namedtuple. * chore(codespell): whitelist 'coo' (sparse COO format) to fix false positive * refactor(persistent): delegate from_models to from_snapshot ModelDiff.from_models now captures a snapshot of model_a and defers to from_snapshot(same_model=False), removing the duplicated baseline-extraction and diff loops. * refactor(persistent): drop dead per-container structural guards The global _structural_reason precheck (via vlabels/clabels, the concatenation of per-container active_labels) already pins each container's active_labels and shape before diff_var/diff_con run, so the per-container shape/active_labels mismatch branches were unreachable duplication that silently degraded to a rebuild. Removed from diff_var, diff_con, and diff_objective. * test(persistent): cover diff rebuild reasons and snapshot var kinds Adds coverage for QUAD_OBJ, variable-type change, sign-only mutation, top-level STRUCTURAL_LABELS (vlabels/clabels), indices SPARSITY, binary/ integer/semi-continuous capture, and ModelDiff inspect/repr helpers. persistent/diff.py 81->96%, snapshot.py 87->99%. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Felix <117816358+FBumann@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8dfa6d4 commit 2bbbeef

21 files changed

Lines changed: 3569 additions & 103 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ repos:
3030
rev: v2.4.2
3131
hooks:
3232
- id: codespell
33+
args: ['--ignore-words-list=coo']
3334
types_or: [python, rst, markdown]
3435
files: ^(linopy|doc)/
3536
- repo: https://github.com/kynan/nbstripout

doc/api.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,14 @@ Attributes
136136
Modification
137137
------------
138138

139+
``Variable.update`` is the canonical mutation API. The legacy ``lower`` /
140+
``upper`` setters still forward to ``update`` but emit a
141+
``DeprecationWarning`` and will be removed in a future release.
142+
139143
.. autosummary::
140144
:toctree: generated/
141145

146+
variables.Variable.update
142147
variables.Variable.fix
143148
variables.Variable.unfix
144149
variables.Variable.relax
@@ -332,6 +337,19 @@ Structure
332337
constraints.Constraint.coeffs
333338
constraints.Constraint.vars
334339

340+
Modification
341+
------------
342+
343+
``Constraint.update`` is the canonical mutation API. The legacy ``lhs`` /
344+
``sign`` / ``rhs`` / ``coeffs`` / ``vars`` setters still forward to
345+
``update`` but emit a ``DeprecationWarning`` and will be removed in a
346+
future release.
347+
348+
.. autosummary::
349+
:toctree: generated/
350+
351+
constraints.Constraint.update
352+
335353
Post-solve access
336354
-----------------
337355

doc/release_notes.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,28 @@ Release Notes
44
Upcoming Version
55
----------------
66

7+
**Features**
8+
9+
10+
*In-place solver updates (persistent re-solve)*
11+
12+
* A built solver can now be re-solved against a mutated ``Model`` without a full rebuild. Construct with ``Solver.from_name(..., track_updates=True)`` and re-call ``solver.solve(model)`` after edits — the diff against the previous build is applied in place when the backend supports it, falling back to a rebuild otherwise. Supported on HiGHS, Gurobi, Xpress, and Mosek (``io_api="direct"``).
13+
* Pass ``disallow_rebuild=True`` to ``solve(model, ...)`` to guarantee an in-place update or raise ``RebuildRequiredError``. Inspect ``solver._last_rebuild_reason`` (a ``RebuildReason``, or ``None`` after an in-place update) to understand why a rebuild was triggered.
14+
* New ``linopy.persistent`` module exposes ``ModelSnapshot``, ``ModelDiff``, and ``RebuildReason`` for users who want to introspect or build the diff themselves. ``ModelDiff.from_snapshot`` / ``from_models`` return the ``RebuildReason`` directly when the change cannot be applied in place.
15+
16+
*Improved IO*
17+
718
* ``Model.to_netcdf`` now records the writing linopy version in the ``_linopy_version`` dataset attribute. Files written by older versions (without the attribute) continue to read unchanged.
819

9-
**Features**
20+
*Other*
1021

1122
* ``add_variables(binary=True, ...)`` now accepts ``lower``/``upper`` bounds, as long as they are 0 or 1. Previously binary bounds could only be set via the ``.lower``/``.upper`` setters after creation. (https://github.com/PyPSA/linopy/issues/776)
1223

24+
**Deprecations**
25+
26+
* Mutation via assignment to ``Variable.lower`` / ``Variable.upper`` / ``Constraint.coeffs`` / ``Constraint.vars`` / ``Constraint.lhs`` / ``Constraint.sign`` / ``Constraint.rhs`` is deprecated and emits a ``DeprecationWarning``. Use ``Variable.update(...)`` / ``Constraint.update(...)`` instead — the canonical mutation API with one validation path and one place that flips the persistent-solver dirty flag. Read access to these properties is unchanged. The setters will be removed in a future release.
27+
* Passing a raw ``DataArray`` of integer labels to ``Constraint.vars = ...`` setter is deprecated and emits a ``FutureWarning``. Pass a ``Variable`` to ``Constraint.update()`` instead — it is the supported input. The ``DataArray`` path will be removed in a future release.
28+
1329
**Bug fixes**
1430

1531
* LP file export now honors bounds tightened below ``[0, 1]`` on a binary variable via the ``.lower``/``.upper`` setters after creation (e.g. ``upper = 0``). Previously such bounds were written only by ``io_api="direct"`` and dropped by ``io_api="lp"``. (https://github.com/PyPSA/linopy/issues/776)

examples/creating-constraints.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,16 +348,16 @@
348348
"\n",
349349
"`CSRConstraint` deliberately exposes a narrower API than the xarray-backed `Constraint`:\n",
350350
"\n",
351-
"- **No in-place mutation.** Setters such as `con.coeffs = ...`, `con.vars = ...`, `con.sign = ...`, `con.rhs = ...`, and `con.lhs = ...` are only available on `Constraint`.\n",
351+
"- **No in-place mutation.** `Constraint.update(...)` is only available on `Constraint`. (The legacy setters — `con.coeffs = ...`, `con.vars = ...`, `con.sign = ...`, `con.rhs = ...`, `con.lhs = ...` — still forward to `update` on `Constraint` but emit a `DeprecationWarning` and will be removed in a future release.)\n",
352352
"- **No label-based indexing.** `con.loc[...]` is only available on `Constraint`.\n",
353353
"- **Accessing `.coeffs` / `.vars` triggers reconstruction.** On a `CSRConstraint` these properties rebuild the full xarray `Dataset` on demand and emit a `PerformanceWarning`. For solver-oriented workflows prefer `con.to_matrix()` or work with the CSR data directly.\n",
354354
"\n",
355355
"If you need any of the above, call `.mutable()` first to get a `Constraint`:\n",
356356
"\n",
357357
"```python\n",
358358
"con = m.constraints[\"my_constraint\"].mutable()\n",
359-
"con.loc[{\"time\": 0}] # label-based indexing now available\n",
360-
"con.rhs = 5 # mutation now available\n",
359+
"con.loc[{\"time\": 0}] # label-based indexing now available\n",
360+
"con.update(rhs=5) # mutation now available\n",
361361
"```"
362362
]
363363
},

examples/manipulating-models.ipynb

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"metadata": {},
7575
"outputs": [],
7676
"source": [
77-
"x.lower = 1"
77+
"x.update(lower=1)"
7878
]
7979
},
8080
{
@@ -83,7 +83,10 @@
8383
"metadata": {},
8484
"source": [
8585
".. note::\n",
86-
" The same could have been achieved by calling `m.variables.x.lower = 1`\n",
86+
" Assignment via the ``x.lower = 1`` setter still works but is\n",
87+
" deprecated and will be removed in a future release. Use\n",
88+
" ``Variable.update`` instead — it is the canonical mutation API\n",
89+
" with a single validation path.\n",
8790
"\n",
8891
"Let's solve it again!"
8992
]
@@ -127,7 +130,7 @@
127130
"metadata": {},
128131
"outputs": [],
129132
"source": [
130-
"x.lower = xr.DataArray(range(10, 0, -1), coords=(time,))"
133+
"x.update(lower=xr.DataArray(range(10, 0, -1), coords=(time,)))"
131134
]
132135
},
133136
{
@@ -157,9 +160,12 @@
157160
"source": [
158161
"## Varying Constraints\n",
159162
"\n",
160-
"A similar functionality is implemented for constraints. Here we can modify the left-hand-side, the sign and the right-hand-side.\n",
163+
"A similar functionality is implemented for constraints. We use\n",
164+
"``Constraint.update`` to change the left-hand-side, the sign,\n",
165+
"and the right-hand-side.\n",
161166
"\n",
162-
"Assume we want to relax the right-hand-side of the first constraint `con1` to `8 * factor`. This would translate to:"
167+
"Assume we want to relax the right-hand-side of the first constraint\n",
168+
"``con1`` to ``8 * factor``. This translates to:"
163169
]
164170
},
165171
{
@@ -169,7 +175,7 @@
169175
"metadata": {},
170176
"outputs": [],
171177
"source": [
172-
"con1.rhs = 8 * factor"
178+
"con1.update(rhs=8 * factor)"
173179
]
174180
},
175181
{
@@ -178,7 +184,10 @@
178184
"metadata": {},
179185
"source": [
180186
".. note::\n",
181-
" The same could have been achieved by calling `m.constraints.con1.rhs = 8 * factor`\n",
187+
" Assignment via the ``con1.rhs = 8 * factor`` setter still works\n",
188+
" but is deprecated and will be removed in a future release. Use\n",
189+
" ``Constraint.update`` instead — it is the canonical mutation API\n",
190+
" with a single validation path.\n",
182191
"\n",
183192
"Let's solve it again!"
184193
]
@@ -212,7 +221,7 @@
212221
"metadata": {},
213222
"outputs": [],
214223
"source": [
215-
"con1.lhs = 3 * x + 8 * y"
224+
"con1.update(lhs=3 * x + 8 * y)"
216225
]
217226
},
218227
{
@@ -221,9 +230,15 @@
221230
"metadata": {},
222231
"source": [
223232
"**Note:**\n",
224-
"The same could have been achieved by calling \n",
225-
"```python \n",
226-
"m.constraints['con1'].lhs = 3 * x + 8 * y\n",
233+
"Assignment via the ``con1.lhs = 3 * x + 8 * y`` setter still works\n",
234+
"but is deprecated and will be removed in a future release. Use\n",
235+
"``Constraint.update`` instead — it is the canonical mutation API\n",
236+
"with a single validation path.\n",
237+
"\n",
238+
"``Constraint.update`` also accepts a full constraint expression in one call:\n",
239+
"\n",
240+
"```python\n",
241+
"con1.update(3 * x + 8 * y <= 8 * factor) # replaces lhs / sign / rhs at once\n",
227242
"```"
228243
]
229244
},

0 commit comments

Comments
 (0)