Skip to content

refac: introduce consistent convention for linopy operations with subsets and supersets#572

Merged
FabianHofmann merged 27 commits intomasterfrom
harmonize-linopy-operations
Mar 10, 2026
Merged

refac: introduce consistent convention for linopy operations with subsets and supersets#572
FabianHofmann merged 27 commits intomasterfrom
harmonize-linopy-operations

Conversation

@FabianHofmann
Copy link
Collaborator

@FabianHofmann FabianHofmann commented Feb 9, 2026

Related to #550 #571

Changes proposed in this Pull Request

Establish a consistent coordinate alignment when linopy objects interact with DataArrays that have subset or superset coordinates

TODO:

  • extend test suit to linopy + linopy objects
  • manifest convention in the docs
  • clarify relation to linopy.align
  • really ensure convention holds true for all combinations of addition/multiplication/comparison + subset/superset` + order + object types
  • come up with a convention for equally shaped objects Fix coordinate misalignment in expression merge #550

Checklist

  • 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.

Add le(), ge(), eq() methods to LinearExpression and Variable classes,
mirroring the pattern of add/sub/mul/div methods. These methods support
the join parameter for flexible coordinate alignment when creating constraints.
Consolidate repetitive alignment handling in _add_constant and
_apply_constant_op into a single _align_constant method. This
eliminates code duplication and makes the alignment behavior
(handling join parameter, fill_value, size-aware defaults) testable
and maintainable in one place.
@FabianHofmann FabianHofmann marked this pull request as ready for review February 17, 2026 20:51
FabianHofmann and others added 5 commits February 18, 2026 09:53
numpy_to_dataarray no longer inflates ndim beyond arr.ndim, fixing
lower-dim numpy arrays as constraint RHS. Also reject higher-dim
constant arrays (numpy/pandas) consistently with DataArray behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@FabianHofmann
Copy link
Collaborator Author

@FBumann, if you have some time, could you run this branch on flexopt and check it does not change results?

@FBumann
Copy link
Collaborator

FBumann commented Feb 19, 2026

I'll do it this evening.

@FBumann
Copy link
Collaborator

FBumann commented Feb 19, 2026

@FabianHofmann I really like the effort to making the broadcasting in linopy more predictable.
I think is is great!

As far as i understand it, the convention is:

  • Same shape → "override" (fast positional alignment). More performant.
  • Different shape → "outer" for expression+expression, "left" for expression+constant

My thoughts on the current convention:

The default join=None unfortunately does some magic thats hard to grasp in my opinion.
I assume that this is partly to keep the current behaviour (as i found for flixopt --> all tests pass!)
So i fully understand if we rather go your proposed route.

That said, i see the following issues:

  1. think it would be great if we relied less on the heuristic of same shape --> align positionally. Id rather do an explicit a.add(b, join="override"). This also resolves the confusion with what are the final coords of the expression for me.
  2. left join for expression+constant means that expression + constant + expression might have different results than expression + expression + constant, which is unexpected.
m = linopy.Model()
time = pd.RangeIndex(5, name="time")
subset_time = pd.RangeIndex(3, name="time")
factor = xr.DataArray([0, 1, 2, 3, 4, 5], dims=["time"], coords={"time": [0, 1, 2, 3, 4, 5]})
x = m.add_variables(lower=0, coords=[time], name="x")
y = m.add_variables(lower=0, coords=[subset_time], name="y")

print(y + x + factor)
>>>LinearExpression [time: 5]:
---------------------------
[0]: +1 y[0] + 1 x[0]
[1]: +1 y[1] + 1 x[1] + 1
[2]: +1 y[2] + 1 x[2] + 2
[3]: +1 x[3] + 3
[4]: +1 x[4] + 4

print(y + factor + x )
>>>LinearExpression [time: 5]:
---------------------------
[0]: +1 y[0] + 1 x[0]
[1]: +1 y[1] + 1 x[1] + 1
[2]: +1 y[2] + 1 x[2] + 2
[3]: +1 x[3]
[4]: +1 x[4]

I'll try to come up with something that resolves this.

@FBumann
Copy link
Collaborator

FBumann commented Feb 20, 2026

@FabianHofmann After some thought i find the matter more complicated than expected.
xarray themselves use 'inner' for arithmetic operations. So we should probably go for that.
This fixes the ambiguity of arithmetics (x+a) + y == x + (y + a)
I need to think about the other implications

@FBumann
Copy link
Collaborator

FBumann commented Feb 20, 2026

@FabianHofmann I will not be able to finalize my review on this. Ill be gone for 2 weeks.

@FabianHofmann
Copy link
Collaborator Author

@FBumann wonderful comments. It is really good to have another person thinking about this. Have a good break and take your time afterwards. I might want to pull this in earlier but I am really happy to follow your thought on the new convention for the next major release!

@FBumann
Copy link
Collaborator

FBumann commented Feb 23, 2026

For what I've seen this PR is backwards compatibile, so I don't see a reason why not to merge this and think about another convention later

… as 'no constraint' in RHS

- Fill NaN with 0 (add/sub) or fill_value (mul/div) in _add_constant/_apply_constant_op
- Fill NaN coefficients with 0 in Variable.to_linexpr
- Restore NaN mask in to_constraint() so subset RHS still signals unconstrained positions
@FabianHofmann FabianHofmann merged commit ee62dcc into master Mar 10, 2026
20 checks passed
@FabianHofmann FabianHofmann deleted the harmonize-linopy-operations branch March 10, 2026 15:09
FabianHofmann added a commit to CharlieFModo/linopy that referenced this pull request Mar 12, 2026
…sets and supersets (PyPSA#572)

* refac: introduce consistent convention for linopy operations with subsets and supersets

* move scalar addition to add_constant

* add overwriting logic to add constant

* add join parameter to control alignment in operations

* Add le, ge, eq methods with join parameter for constraints

Add le(), ge(), eq() methods to LinearExpression and Variable classes,
mirroring the pattern of add/sub/mul/div methods. These methods support
the join parameter for flexible coordinate alignment when creating constraints.

* Extract constant alignment logic into _align_constant helper

Consolidate repetitive alignment handling in _add_constant and
_apply_constant_op into a single _align_constant method. This
eliminates code duplication and makes the alignment behavior
(handling join parameter, fill_value, size-aware defaults) testable
and maintainable in one place.

* update notebooks

* update release notes

* fix types

* add regression test

* fix numpy array dim mismatch in constraints and add RHS dim tests

numpy_to_dataarray no longer inflates ndim beyond arr.ndim, fixing
lower-dim numpy arrays as constraint RHS. Also reject higher-dim
constant arrays (numpy/pandas) consistently with DataArray behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* remove pandas reindexing warning

* Fix mypy errors: type ignores for xr.align/merge, match override signature, add test type hints

* remove outdated warning tests

* reintroduce expansions of extra rhs dims, fix multiindex alignment

* refactor test fixtures and use sign constants

* add tests for pandas series subset/superset

* test: add TestMissingValues for same-shape constants with NaN entries

* Fix broken test imports, stray docstring char, and incorrect test assertion from fixture refactor

* Fill NaN with neutral elements in expression arithmetic, preserve NaN as 'no constraint' in RHS

- Fill NaN with 0 (add/sub) or fill_value (mul/div) in _add_constant/_apply_constant_op
- Fill NaN coefficients with 0 in Variable.to_linexpr
- Restore NaN mask in to_constraint() so subset RHS still signals unconstrained positions

* Fix CI doctest collection by deferring linopy import in test/conftest.py

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
FBumann added a commit that referenced this pull request Mar 14, 2026
Documents 5 categories of legacy issues with paired legacy/v1 tests:

1. Positional alignment (#586, #550): same-shape operands with different
   labels silently paired by position, producing wrong results
2. Subset constant associativity (#572): left-join drops coordinates,
   making (a+c)+b != a+(c+b)
3. User NaN swallowed (#620): NaN in user data silently filled with
   inconsistent neutral elements (0 for add/mul, 1 for div)
4. Variable vs Expression inconsistency (#569, #571): x*c and (1*x)*c
   previously gave different results
5. Absent slot propagation (#620): legacy can't distinguish absent
   variables from zero, fillna() is a no-op

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

2 participants