feat(piecewise): partial active gate support via active_gate helper (#796)#797
feat(piecewise): partial active gate support via active_gate helper (#796)#797FBumann wants to merge 2 commits into
active gate support via active_gate helper (#796)#797Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
ba4430a to
bf11d90
Compare
…#796) `add_piecewise_formulation(active=...)` assumed the gate covered the whole indexed dimension. A gate defined over only a subset of coordinate labels (or with masked entries) silently forced the uncovered entries to zero — breaking mixed committable / non-committable formulations (PyPSA#1755). - Add `linopy.active_gate(active, coords, fill_value=1)`: pads a partial gate to full coverage, treating missing/masked entries as always-active (1) or always-off (0). Lives in its own module `linopy/_active_gate.py` as a temporary legacy stopgap; under v1 the bare `active.reindex(coords).fillna(fill_value)` idiom suffices and the helper is expected to be deprecated. - `add_piecewise_formulation` now rejects an under-defined `active` (strict subset or masked) with an actionable error instead of mis-solving. A lower-dimensional gate still broadcasts and is accepted. - Docs (api, piecewise guide, release notes) and tests in a dedicated `test/test_piecewise_active_gate.py` (parametrized over both partial shapes and incremental/sos2/disjunctive paths). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bf11d90 to
c9c3298
Compare
FabianHofmann
left a comment
There was a problem hiding this comment.
thanks @FBumann ; the exposure of the new active_gate function is not ideal as this will be a throw-away in v1. but I definitely see the point. a potentiially cleaner solution would be adding a explicit boolean mask that has prio and active can be a subset. then we have a clear split on masking and gating and align the signature with the other add_* functions. what do you think?
A And yes, the helper isnt ideal, but as you can see from the tests and the AI comment, under legacy, there are many edge cases, and scoping them in a helper keeps |
…ction Apply review suggestion (#797): replace the hand-rolled vars/term-dim reduction with the public `has_terms` property in `active_gate` and the coverage validation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
I'll do another branch with inline parameters, so we can decide from seeing the api! |
…, #796) (#798) * feat(piecewise): partial `active` gate support via active_gate helper (#796) `add_piecewise_formulation(active=...)` assumed the gate covered the whole indexed dimension. A gate defined over only a subset of coordinate labels (or with masked entries) silently forced the uncovered entries to zero — breaking mixed committable / non-committable formulations (PyPSA#1755). - Add `linopy.active_gate(active, coords, fill_value=1)`: pads a partial gate to full coverage, treating missing/masked entries as always-active (1) or always-off (0). Lives in its own module `linopy/_active_gate.py` as a temporary legacy stopgap; under v1 the bare `active.reindex(coords).fillna(fill_value)` idiom suffices and the helper is expected to be deprecated. - `add_piecewise_formulation` now rejects an under-defined `active` (strict subset or masked) with an actionable error instead of mis-solving. A lower-dimensional gate still broadcasts and is accepted. - Docs (api, piecewise guide, release notes) and tests in a dedicated `test/test_piecewise_active_gate.py` (parametrized over both partial shapes and incremental/sos2/disjunctive paths). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(piecewise): use LinearExpression.has_terms for gate gap detection Apply review suggestion (#797): replace the hand-rolled vars/term-dim reduction with the public `has_terms` property in `active_gate` and the coverage validation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(piecewise): replace active_gate helper with active_fill parameter Per PR #797 review: instead of a standalone `active_gate` helper, gate a partial `active` via an `active_fill` parameter on `add_piecewise_formulation`. The function derives its own coordinate, so the caller supplies nothing extra; `active_fill=None` (default) keeps the function strict (partial `active` raises), and `0`/`1` opt into always-off / always-on. - Drop `linopy/_active_gate.py` and the `active_gate` export. - Add `active_fill: int | None` to `add_piecewise_formulation`; fold the guard + padding into `_resolve_active`. - `active_fill` is transitional (removed once v1 makes `active.reindex(coords).fillna(value)` sufficient) — documented as such. - Tests renamed to test_piecewise_active_fill.py; docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(piecewise): state explicitly that a partial active = subset labels or masked Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(piecewise): type active_fill as int to satisfy mypy Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
closed via #798 |
(placeholder — Felix to rewrite)
Closes #796. See the design discussion in the issue.
Note
The following content was generated by AI.
Problem
add_piecewise_formulation(active=...)assumed the gate spanned the formulation's full indexed dimension. A gate defined over only a subset of coordinate labels — or with masked entries — silently forced the uncovered entries to zero, instead of leaving them ungated. This broke formulations that mix gated and ungated entities (e.g. committable + non-committable units sharing a unit-commitmentstatus), surfaced in PyPSA/PyPSA#1755.Approach
Per the issue discussion, this does not make the function silently accept misaligned coords (that would mask genuine coverage bugs and clashes with v1 alignment semantics). Instead:
add_piecewise_formulationnow rejects an under-definedactive(strict subset or masked) with an actionable error. A lower-dimensional gate still broadcasts and is accepted.linopy.active_gate(active, coords, fill_value=1)pads a partial gate to full coverage — missing/masked entries become always-active (1) or always-off (0). The signature mirrorsactive.reindex(coords).fillna(fill_value)so migration is mechanical.active_gateis a temporary legacy stopgap, isolated inlinopy/_active_gate.py: under the planned v1 arithmetic semantics (#717) the barereindex().fillna()idiom is correct on its own, so the helper is expected to be deprecated and that file removed. The validation guard stays.Why a helper is needed today (legacy footgun)
The "always-on" pad is conceptually just
reindex(coords).fillna(1). Today that idiom is shape- and cast-dependent: a bareVariable.fillnaresolves a gap to always-off,fillnacan't add absent labels (a strict subset needsreindex), and a masked entry has no NaN forfillnato fill (needs.where). There is no single correct one-liner across both shapes.active_gateencapsulates a.where-on-term-labels recipe that is correct for both shapes; under v1 it collapses to the bare idiom. Full per-shape/per-regime table in the issue comment.Changes
linopy/_active_gate.py(new) —active_gatehelper.linopy/piecewise.py—_validate_active_coverage+ wiring;activedocstring.linopy/__init__.py— exportactive_gate.test/test_piecewise_active_gate.py(new) — parametrized over both partial shapes × helper/validation/solver (incremental, sos2, disjunctive), plus broadcast and full/scalar acceptance.api.rst, piecewise guide (partial-gates section), release notes.Tests
test_piecewise_active_gate.py+test_piecewise_constraints.py: 247 passed. mypy and ruff clean.🤖 Generated with Claude Code