Skip to content

fix(v1): make Variable.fillna(scalar) resolve absence under both conventions (#847, #848)#851

Merged
FBumann merged 2 commits into
feat/arithmetic-conventionfrom
fix/847-848-fillna-cross-convention
Jul 20, 2026
Merged

fix(v1): make Variable.fillna(scalar) resolve absence under both conventions (#847, #848)#851
FBumann merged 2 commits into
feat/arithmetic-conventionfrom
fix/847-848-fillna-cross-convention

Conversation

@FBumann

@FBumann FBumann commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Closes #847, closes #848. Stacked on feat/arithmetic-convention (#717).

Note

The following content was generated by AI.

Problem

Variable.fillna(<scalar>) is the documented resolution for absent slots (from shift/where/reindex/mask), but under legacy it misbehaved two ways, so there was no single form valid under both conventions:

Fix

The v1 path already behaved correctly, so this is a pure legacy workaround (marked LEGACY: remove at 1.0):

  • v1 stays the clean one-liner self.to_linexpr().fillna(fill_value).
  • under legacy, place the value at the -1 labels directly and skip the absence warning (via a private _warn_absence flag on to_linexpr).

var.shift(1).fillna(v) is now a single form, identical under both conventions — same vars/const; only the immaterial phantom coefficient at vars == -1 slots differs (dropped at solve time).

Note: the raw two-step var.to_linexpr().fillna(v) remains a legacy no-op — that low-level path has already overwritten absence with 0. Variable.fillna is correct because it still holds the -1 labels.

Cross-release (0.8.0) is intentionally out of scope: downstream requires the v1 release rather than a 0.8.x backport (a naive backport would be silently wrong for non-zero fills).

Tests

  • test_legacy_variable_fillna_numeric_fills_like_v1 — legacy fill now lands (was pinned as a no-op).
  • test_legacy_variable_fillna_does_not_warn — the documented resolution no longer warns.
  • v1 counterparts unchanged.

FBumann and others added 2 commits July 20, 2026 09:01
…entions (#847, #848)

Variable.fillna(<scalar>) is the documented resolution for absent slots
(from shift/where/reindex/mask), but under legacy it misbehaved two ways:

- #847: it warned. fillna internally calls to_linexpr(), whose legacy path
  emits the masked-variable LinopySemanticsWarning — even though fillna is
  itself the resolution that warning points to. So the documented fix
  couldn't be written warning-free on both conventions.
- #848: it silently dropped the fill value. Legacy to_linexpr marks absent
  const as 0 (not NaN), so the subsequent LinearExpression.fillna had
  nothing to fill; fillna(5) left 0 at absent slots while v1 put 5.

The v1 path already behaved correctly, so this is a pure legacy workaround
(marked LEGACY: remove at 1.0): keep the clean one-liner under v1, and under
legacy place the value at the -1 labels directly and skip the absence warning.
Result: `var.shift(1).fillna(v)` is now a single form, identical under both
conventions (same vars/const; only the immaterial phantom coeff differs), so
downstream migrating to v1 no longer has to version-gate the expression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document why var.to_linexpr().fillna(v) is a no-op under legacy (absence is
already materialised as const=0, so there is no NaN to fill) and point at
Variable.fillna as the cross-convention resolution. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FBumann
FBumann marked this pull request as ready for review July 20, 2026 07:44
@FBumann
FBumann merged commit 3700657 into feat/arithmetic-convention Jul 20, 2026
2 checks passed
@FBumann
FBumann deleted the fix/847-848-fillna-cross-convention branch July 20, 2026 07:44
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.

1 participant