Skip to content

feat(piecewise): partial active gate support via active_gate helper (#796)#797

Closed
FBumann wants to merge 2 commits into
masterfrom
fix/796-partial-active-gate
Closed

feat(piecewise): partial active gate support via active_gate helper (#796)#797
FBumann wants to merge 2 commits into
masterfrom
fix/796-partial-active-gate

Conversation

@FBumann

@FBumann FBumann commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

(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-commitment status), 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:

  1. Validate, don't auto-fix. add_piecewise_formulation now rejects an under-defined active (strict subset or masked) with an actionable error. A lower-dimensional gate still broadcasts and is accepted.
  2. 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 mirrors active.reindex(coords).fillna(fill_value) so migration is mechanical.

active_gate is a temporary legacy stopgap, isolated in linopy/_active_gate.py: under the planned v1 arithmetic semantics (#717) the bare reindex().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 bare Variable.fillna resolves a gap to always-off, fillna can't add absent labels (a strict subset needs reindex), and a masked entry has no NaN for fillna to fill (needs .where). There is no single correct one-liner across both shapes. active_gate encapsulates 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_gate helper.
  • linopy/piecewise.py_validate_active_coverage + wiring; active docstring.
  • linopy/__init__.py — export active_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.
  • Docs: 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

@codspeed-hq

codspeed-hq Bot commented Jun 29, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 138 untouched benchmarks
⏩ 138 skipped benchmarks1


Comparing fix/796-partial-active-gate (16fc4a2) with master (1dbde37)

Open in CodSpeed

Footnotes

  1. 138 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@FBumann FBumann force-pushed the fix/796-partial-active-gate branch from ba4430a to bf11d90 Compare June 29, 2026 12:51
…#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>
@FBumann FBumann force-pushed the fix/796-partial-active-gate branch from bf11d90 to c9c3298 Compare June 29, 2026 13:02
@FBumann FBumann requested a review from FabianHofmann June 29, 2026 13:35

@FabianHofmann FabianHofmann left a comment

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.

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?

Comment thread linopy/_active_gate.py Outdated
@FBumann

FBumann commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

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 mask parameter is indeed a good idea. But it should mask the whole constraint, not just active! So its not a fix to this use case.

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 add_piecewise_formulation clean. Else we need parameters purely scoped to active (which we could do...), and deprecate them gracefully. Maybe this is the right call from a user perspective though...

…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>
@FBumann

FBumann commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

I'll do another branch with inline parameters, so we can decide from seeing the api!

FabianHofmann pushed a commit that referenced this pull request Jun 30, 2026
…, #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>
@FabianHofmann

Copy link
Copy Markdown
Collaborator

closed via #798

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.

add_piecewise_formulation: support a partial active gate (treat missing/null entries as always-active)

2 participants