You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PyPSA's nodal balance constraint needs to know which slots of an expression have no live variable terms, and currently has to reach into linopy internals to find out (pypsa/optimization/constraints.py#L1166):
lhs=merge(exprs, join="outer").reindex(name=buses)
...
empty_nodal_balance= (lhs.vars==-1).all("_term")
ifempty_nodal_balance.any():
if (empty_nodal_balance& (rhs!=0)).any().item():
raiseValueError("Empty LHS with non-zero RHS in nodal balance constraint.")
mask=~empty_nodal_balanceelse:
mask=None
Buses with no attached components end up with all-dead terms (vars = -1) after the outer merge + reindex. PyPSA needs them to (a) raise on demand at a disconnected bus, and (b) mask those constraint rows out.
vars and "_term" are internal representation details. Downstream code depending on them couples PyPSA to linopy's storage layout, and shows a gap in the public API.
Why the existing API doesn't cover it
expr.empty is a single bool for the whole expression.
expr.isnull() is (vars == -1).all(helper_dims) & const.isnull() — absence (no terms and no constant). In the merge + reindex pattern above it happens to give the same answer (reindexed slots carry a NaN constant), but it tests a different concept: a slot whose constant has been filled — e.g. after fillna(0) — is no longer "null", yet still has no terms. The two concepts are also explicitly distinct in the v1 convention (feat: v1 semantic convention #717): dead terms at a present slot vs an absent slot.
So the check PyPSA actually means — "this constraint row has no variables" — has no public spelling.
Proposed API
@propertydefhas_terms(self) ->DataArray:
""" Boolean array, True at slots where the expression has at least one live term (vars != -1), reduced over the helper dims. """
on BaseExpression, so both LinearExpression and QuadraticExpression get it.
PyPSA's code then becomes internals-free:
has_terms=lhs.has_termsifnothas_terms.all():
if (~has_terms& (rhs!=0)).any().item():
raiseValueError("Empty LHS with non-zero RHS in nodal balance constraint.")
mask=has_termselse:
mask=None
Relation to isnull()
has_terms and isnull() answer different questions and both have a place:
slot state
isnull()
~has_terms
live terms
False
False
no terms, const = NaN (absent, e.g. after reindex)
True
True
no terms, const = 0 (present slot, e.g. after fillna(0))
Motivation
PyPSA's nodal balance constraint needs to know which slots of an expression have no live variable terms, and currently has to reach into linopy internals to find out (pypsa/optimization/constraints.py#L1166):
Buses with no attached components end up with all-dead terms (
vars = -1) after the outer merge + reindex. PyPSA needs them to (a) raise on demand at a disconnected bus, and (b) mask those constraint rows out.varsand"_term"are internal representation details. Downstream code depending on them couples PyPSA to linopy's storage layout, and shows a gap in the public API.Why the existing API doesn't cover it
expr.emptyis a single bool for the whole expression.expr.isnull()is(vars == -1).all(helper_dims) & const.isnull()— absence (no terms and no constant). In the merge + reindex pattern above it happens to give the same answer (reindexed slots carry a NaN constant), but it tests a different concept: a slot whose constant has been filled — e.g. afterfillna(0)— is no longer "null", yet still has no terms. The two concepts are also explicitly distinct in the v1 convention (feat: v1 semantic convention #717): dead terms at a present slot vs an absent slot.So the check PyPSA actually means — "this constraint row has no variables" — has no public spelling.
Proposed API
on
BaseExpression, so bothLinearExpressionandQuadraticExpressionget it.PyPSA's code then becomes internals-free:
Relation to
isnull()has_termsandisnull()answer different questions and both have a place:isnull()~has_termsconst = NaN(absent, e.g. after reindex)const = 0(present slot, e.g. afterfillna(0))I'll follow up with a PR implementing this.