Skip to content
Open
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
1 change: 1 addition & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Upcoming Version
* LP file export now honors bounds tightened below ``[0, 1]`` on a binary variable via the ``.lower``/``.upper`` setters after creation (e.g. ``upper = 0``). Previously such bounds were written only by ``io_api="direct"`` and dropped by ``io_api="lp"``. (https://github.com/PyPSA/linopy/issues/776)
* Freezing an empty constraint group (e.g. an empty ``isel`` slice) no longer raises ``ValueError: cannot reshape array of size 0``. ``Model(freeze_constraints=True)`` and ``Constraint.freeze()`` now round-trip zero-row constraints losslessly.
* ``Variable.where`` no longer raises ``ValueError: exact match required for all data variable names`` once a solution is attached (after ``Model.solve``) or the variable is fixed. The fill value now covers auxiliary data variables (``solution``, stashed bounds) instead of only ``labels``/``lower``/``upper``.
* ``linopy.testing.assert_linequal`` now aligns dimension order before comparing, so mathematically identical expressions built in different orders (e.g. ``x + y`` versus ``y + x``, which inherit different dimension orders from xarray broadcasting) are correctly treated as equal. Genuinely different expressions still fail.

Version 0.8.0
-------------
Expand Down
29 changes: 23 additions & 6 deletions linopy/testing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import numpy as np
import xarray as xr
from xarray.testing import assert_equal

from linopy.constants import TERM_DIM
Expand All @@ -10,6 +11,22 @@
from linopy.variables import Variable, _var_unwrap


def _align_dim_order(target: xr.Dataset, other: xr.Dataset) -> xr.Dataset:
"""
Reorder ``other``'s dimensions to match ``target`` when they share a dim set.

linopy expressions inherit xarray's broadcasting, whose dimension order
follows operand order (``x + y`` yields ``(x_dim, y_dim)`` while ``y + x``
yields ``(y_dim, x_dim)``). That difference is not semantically meaningful,
so it should not make two expressions compare unequal. If the dimension
sets differ the inputs are genuinely unequal; leave ``other`` untouched so
:func:`assert_equal` reports the real difference.
"""
if set(target.dims) == set(other.dims):
return other.transpose(*target.dims)
return other


def _sort_by_vars_along_term(expr: LinearExpression) -> LinearExpression:
"""Sort a linear expression's terms by variable labels along _term."""
ds = expr.data
Expand All @@ -35,15 +52,15 @@ def assert_linequal(
"""
Assert that two linear expressions are semantically equal.

Terms are sorted by variable labels along _term before comparing,
so expressions with different term orderings but identical mathematical
meaning are considered equal.
Terms are sorted by variable labels along _term and dimension order is
aligned before comparing, so expressions with different term orderings or
dimension orderings but identical mathematical meaning are considered equal.
"""
assert isinstance(a, LinearExpression)
assert isinstance(b, LinearExpression)
a_sorted = _sort_by_vars_along_term(a)
b_sorted = _sort_by_vars_along_term(b)
return assert_equal(_expr_unwrap(a_sorted), _expr_unwrap(b_sorted))
a_ds = _expr_unwrap(_sort_by_vars_along_term(a))
b_ds = _expr_unwrap(_sort_by_vars_along_term(b))
return assert_equal(a_ds, _align_dim_order(a_ds, b_ds))


def assert_quadequal(
Expand Down
36 changes: 36 additions & 0 deletions test/test_testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pandas as pd
import pytest

from linopy import Model
from linopy.testing import assert_linequal


@pytest.fixture
def model() -> Model:
return Model()


def test_assert_linequal_ignores_dimension_order(model: Model) -> None:
"""
Commutative arithmetic yields different dimension orders (``x + y`` gives
``(i, j)`` while ``y + x`` gives ``(j, i)``), inherited from xarray
broadcasting. That is not semantically meaningful, so ``assert_linequal``
must treat the two as equal.
"""
a = model.add_variables(coords=[pd.Index([0, 1], name="i")], name="a")
b = model.add_variables(coords=[pd.Index([0, 1, 2], name="j")], name="b")

assert (a + b).data.coeffs.dims != (b + a).data.coeffs.dims
assert_linequal(a + b, b + a)
assert_linequal(2 * a + 3 * b, 3 * b + 2 * a)


def test_assert_linequal_still_detects_real_differences(model: Model) -> None:
"""Aligning dimension order must not mask genuinely unequal expressions."""
a = model.add_variables(coords=[pd.Index([0, 1], name="i")], name="a")
c = model.add_variables(coords=[pd.Index([0, 1], name="k")], name="c")

with pytest.raises(AssertionError):
assert_linequal(1 * a, 1 * c) # different dimension sets
with pytest.raises(AssertionError):
assert_linequal(1 * a, 2 * a) # different coefficients
Loading