Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions linopy/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1735,6 +1735,16 @@ def fillna(
-------
linopy.LinearExpression or linopy.QuadraticExpression
A new object with missing values filled with the given value.

Notes
-----
This fills ``NaN`` entries only. Under legacy semantics an absent slot
of a variable is already materialised as ``const = 0`` by the time it
reaches an expression, so there is no ``NaN`` here to fill and the value
is a no-op (under v1 absence is carried as ``NaN`` and is filled). To
resolve a variable's absent slots to a constant on both conventions, use
:meth:`Variable.fillna` (or resolve on the variable before
``to_linexpr``), which still holds the absence labels.
"""
value = _expr_unwrap(value)
if isinstance(value, DataArray | np.floating | np.integer | int | float):
Expand Down
13 changes: 11 additions & 2 deletions linopy/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ def to_pandas(self) -> pd.Series:
def to_linexpr(
self,
coefficient: ConstantLike = 1,
*,
_warn_absence: bool = True, # LEGACY: remove at 1.0 — gates the legacy absence warning
) -> expressions.LinearExpression:
"""
Create a linear expression from the variables.
Expand Down Expand Up @@ -380,7 +382,7 @@ def to_linexpr(
# the origin of the most common legacy↔v1 divergence (masked
# variables in arithmetic) that no other warn-site catches.
has_absence = bool((self.labels == -1).any())
if has_absence:
if has_absence and _warn_absence:
warn_legacy(_legacy_masked_variable_message(self.name))
coefficient = reindex_like_if_needed(coefficient, self.labels, 0)
coefficient = coefficient.fillna(0)
Expand Down Expand Up @@ -1319,7 +1321,14 @@ def fillna(
Value to fill the absent slots with.
"""
if isinstance(fill_value, int | float | np.integer | np.floating):
return self.to_linexpr().fillna(fill_value)
if is_v1():
return self.to_linexpr().fillna(fill_value)
# LEGACY: remove at 1.0 — legacy to_linexpr marks absent const as 0
# (not NaN), so LinearExpression.fillna can't fill it and the fill is
# silently dropped. Place fill_value directly to match v1, and skip
# the absence warning fillna is itself the documented resolution for.
lin = self.to_linexpr(_warn_absence=False)
return lin.assign(const=lin.const.where(self.labels != -1, fill_value))
return self.where(~self.isnull(), fill_value)

def ffill(self, dim: str, limit: None = None) -> Variable:
Expand Down
20 changes: 17 additions & 3 deletions test/test_legacy_violations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1359,13 +1359,27 @@ def test_legacy_expr_fillna_is_noop(self, xs: Variable) -> None:
assert not bool(result.isnull().values.any())

@pytest.mark.legacy
def test_legacy_variable_fillna_numeric_is_noop(self, xs: Variable) -> None:
"""Same no-op on the Variable path: the fill value is ignored."""
def test_legacy_variable_fillna_numeric_fills_like_v1(self, xs: Variable) -> None:
"""
#848 — unlike the raw ``to_linexpr().fillna`` two-step above,
``Variable.fillna`` still holds the absent (-1) labels, so it places
the fill value directly and honours it under legacy too, matching v1.
This is the single cross-convention form the migration relies on.
"""
from linopy import LinearExpression

result = xs.fillna(42)
assert isinstance(result, LinearExpression)
assert result.const.values[0] == 0.0
assert result.const.values[0] == 42.0

@pytest.mark.legacy
def test_legacy_variable_fillna_does_not_warn(
self, xs: Variable, unsilenced: None
) -> None:
"""#847 — the documented absence resolution must not itself warn."""
with warnings.catch_warnings():
warnings.simplefilter("error", LinopySemanticsWarning)
xs.fillna(0)

@pytest.mark.legacy
def test_legacy_outer_fillna_then_add_double_counts(
Expand Down