Skip to content

fix(v1): fill join-created positions per side; add fill_value= to add/sub/mul/div - #887

Merged
FabianHofmann merged 3 commits into
feat/arithmetic-conventionfrom
fix/join-created-fill
Aug 13, 2026
Merged

fix(v1): fill join-created positions per side; add fill_value= to add/sub/mul/div#887
FabianHofmann merged 3 commits into
feat/arithmetic-conventionfrom
fix/join-created-fill

Conversation

@FabianHofmann

Copy link
Copy Markdown
Collaborator

Closes # (if applicable).

Note

The following content was generated by AI.

Changes proposed in this Pull Request

Under v1, a reindexing join= filled both operands from a single value.
That value is the constant operand's identity (0 for *, 1 for /), so it
also landed on the expression's own const: dividing an expression that was
missing at a label produced 1 / divisor there. The additive family was
unaffected only because 0 is both its identity and its "contributes nothing"
value.

Each side is now filled separately:

  • the linopy operand contributes the zero expression at a created label
    (no terms, const 0), whatever the operator;
  • the constant operand contributes fill_value=, new on .add / .sub /
    .mul / .div (and their Variable counterparts). It defaults to "this
    operand does not apply here" — 0 for +, - and *, and the same zero row
    for /.

The join keeps deciding the coordinates: outer still returns the union, and
every created label stays in the result. What changed is only what those rows
are worth.

fill_value= requires an explicit join= (without one there are no created
positions) and applies to constant operands only — an expression missing at a
label always contributes the zero expression. Both cases raise rather than
being ignored. expr.div(cost, join="outer", fill_value=1) restores the
unscaled term; expr.add(price, join="outer", fill_value=10) treats an
unpriced label as 10.

Absence an operand carries in — from mask=, .where(), .shift(),
.reindex() — is untouched and still propagates under §6. Only the positions
the join creates are filled.

Behaviour change: the two division cells only. A missing divisor zeroes its
row instead of leaving the term unscaled, and a missing numerator no longer
comes out as 1 / divisor. Addition, subtraction and multiplication are
unchanged in both directions, and legacy semantics are untouched throughout.

Outer-join value table (v1), pinned as a parametrised test

Expression 1 * x + 2, constant 2, one label missing on one side,
join="outer"; values shown at the created label.

operation side missing the label result there
expr + expr either survivor kept, missing side contributes 0
expr ± const const expression unchanged
expr ± const expr const = ±2
expr * const factor coeff 0, const 0
expr * const expr no terms, const 0
expr / const divisor coeff 0, const 0 — was coeff 1, const 2
expr / const numerator const 0 — was const 0.5

Verified by reverting expressions.py alone: exactly the two division cells
fail, the other ten pass.

Also: check_join_fill_value in semantics.py next to the other convention
checks; internally the previously shared fill parameter is split into
nan_fill (legacy's silent NaN fill) and join_fill (what the join creates).
§7 and §10 of the convention, the migration guide and the release notes are
updated.

Full suite green under both semantics (6347 passed), ruff clean.

Checklist

  • AI-generated content is marked (see AGENTS.md).
  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in doc.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes doc/release_notes.rst of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

…/sub/mul/div

A reindexing join filled both operands from one value, so div's divisor
identity (1) also filled the expression's own const — a missing numerator
came out as 1/divisor. Fill each side separately: the expression contributes
the zero expression, the constant operand fill_value=, defaulting to
'does not apply here'. Only the two division cells change; legacy is
untouched. Pins the full outer-join value table as a parametrised test.
The dataset dim order may deviate from the coord dim order, which made
coord_sizes disagree with coord_dims and coord_names. Key coord_sizes off
coord_dims so all three stay consistent.
@FabianHofmann
FabianHofmann merged commit 2db13aa into feat/arithmetic-convention Aug 13, 2026
1 check passed
@FabianHofmann
FabianHofmann deleted the fix/join-created-fill branch August 13, 2026 11:28
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