Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor LinearExpression and Constraint using composition #55

Merged
merged 26 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e882781
Refactor LinearExpression and Constraint using composition
coroa Nov 3, 2022
5fda490
Make linter happy
coroa Nov 3, 2022
ef9d2ee
Small fixes introduced during clean-up
coroa Nov 3, 2022
f903d46
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 3, 2022
21ed04e
Fix imports
coroa Nov 3, 2022
d94214d
Use deprecation instead of deprecated package
coroa Nov 3, 2022
f4504bd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 3, 2022
e10c096
variables: Convert into composition pattern (WIP)
coroa Nov 7, 2022
e87e353
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 7, 2022
95fc5c4
Add monkey_patch work-around for left multiplication
coroa Nov 7, 2022
060fe25
Fix tests
coroa Nov 7, 2022
c03acd3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 7, 2022
cc58dd1
test_model_creation: Fix final test
coroa Nov 7, 2022
d164454
Move monkey patching into dedicated module
coroa Nov 8, 2022
44fafc8
support python3.11
FabianHofmann Nov 8, 2022
f7e134b
variables: add locindexer
FabianHofmann Nov 9, 2022
7f6ffa0
add LinearExpressionGroupby class
FabianHofmann Nov 10, 2022
fd27342
add LinearExpressionRolling class
FabianHofmann Nov 10, 2022
705c970
style: make flake8 a bit more happy
FabianHofmann Nov 14, 2022
c97942a
Merge branch 'composition-design' of github.com:PyPSA/linopy into com…
coroa Nov 21, 2022
b63cf02
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 21, 2022
8529999
Merge branch 'master' into composition-design
FabianHofmann Dec 9, 2022
0330959
fix circular imports
FabianHofmann Dec 9, 2022
d12a5bb
fix imports for xarray v2022.12.
FabianHofmann Dec 9, 2022
de43928
update release notes
FabianHofmann Dec 9, 2022
749b673
test: increase coverage
FabianHofmann Dec 9, 2022
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
7 changes: 5 additions & 2 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
Release Notes
=============

.. Upcoming Release
.. ----------------
Upcoming Release
----------------

* The internal data structure of linopy classes were updated to a safer design. Instead of being defined as inherited xarray classes, the class `Variable`, `LinearExpression` and `Constraint` are now dataclasses with containing the xarray objects in the data field. This allows the package to have more flexible function design and a reduced set of wrapped functions that are sensible to use in the optimization context.
* The class `Variable` and `LinearExpression` have new functions `groupby` and `rolling` imitating the corresponding xarray functions but with safe type inheritance and application of appended operations.


Version 0.0.15
Expand Down
3 changes: 3 additions & 0 deletions linopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
@author: fabulous
"""

# Note: For intercepting multiplications between xarray dataarrays, Variables and Expressions
# we need to extend their __mul__ functions with a quick special case
import linopy.monkey_patch_xarray
from linopy import model, remote
from linopy.expressions import merge
from linopy.io import read_netcdf
Expand Down
19 changes: 18 additions & 1 deletion linopy/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
This module contains commonly used functions.
"""

from functools import wraps
from functools import partialmethod, update_wrapper, wraps

import numpy as np
from xarray import DataArray, apply_ufunc, merge
Expand Down Expand Up @@ -98,3 +98,20 @@ def wrapper(self, arg):
return func(self, arg)

return wrapper


def forward_as_properties(**routes):
def add_accessor(cls, item, attr):
@property
def get(self):
return getattr(getattr(self, item), attr)

setattr(cls, attr, get)

def deco(cls):
for item, attrs in routes.items():
for attr in attrs:
add_accessor(cls, item, attr)
return cls

return deco
73 changes: 28 additions & 45 deletions linopy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,58 +14,37 @@
import numpy as np
import pandas as pd
import xarray as xr
from deprecation import deprecated
from numpy import arange, array
from scipy.sparse import coo_matrix
from xarray import DataArray, Dataset

from linopy import expressions, variables
from linopy.common import (
_merge_inplace,
has_assigned_model,
has_optimized_model,
is_constant,
replace_by_map,
)


class Constraint(DataArray):
@dataclass(repr=False)
class Constraint:
"""
Constraint container for storing constraint labels.
Projection to a single constraint in a model.

The Constraint class is a subclass of xr.DataArray hence most xarray
functions can be applied to it.
"""

__slots__ = ("_cache", "_coords", "_indexes", "_name", "_variable", "model")

def __init__(self, *args, **kwargs):

# workaround until https://github.com/pydata/xarray/pull/5984 is merged
if isinstance(args[0], DataArray):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to suggest that the class supports initialization from a DataArray object passed as first positional parameter args[0]. The new interface is not backwards compatible in this way. But I found only one place where the constructor Constraint() is used directly, so I guess this should be fine. I don't really understand the comment and how it is related to the linked pull request.

Copy link
Member Author

@coroa coroa Nov 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR 5984 was merged for xarray 0.20.2 , ie below what we currently support as oldest possible version so not relevant anymore.

Indeed, I changed the Constraint semantics to only ever possibly refer to a full named Constraint that is already represented within a Constraints container (i considered renaming it to BoundConstraint to make this more obvious).

The AnonymousConstraint seemed to fulfill the need for anything that is not attached to a model yet.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note also that this means that you cannot use Constraint to represent a slice of a constraint, ie. there is no way to sel into a constraint and still use a Constraint object to hold it. (i only understood that feature through converting Variable where this is needed).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm the workaround for PR 5984 is obsolete, but I assume there was a reason why linopy.Constraint had to support initialization from a DataArray object, right? I can't see such a place changed in this PR here nor could I find such a place in the linopy repository, so I'd guess: this is either outdated or used in some downstream repository (PyPSA?).

da = args[0]
args = (da.data, da.coords)
kwargs.update({"attrs": da.attrs, "name": da.name})

self.model = kwargs.pop("model", None)
super().__init__(*args, **kwargs)
assert self.name is not None, "Constraint data does not have a name."

# We have to set the _reduce_method to None, in order to overwrite basic
# reduction functions as `sum`. There might be a better solution (?).
_reduce_method = None

# Disable array function, only function defined below are supported
# and set priority higher than pandas/xarray/numpy
__array_ufunc__ = None
__array_priority__ = 10000
name: str
model: Any

def __repr__(self):
"""
Get the string representation of the constraints.
"""
data_string = (
"Constraint labels:\n" + self.to_array().__repr__().split("\n", 1)[1]
)
data_string = "Constraint labels:\n" + self.labels.__repr__().split("\n", 1)[1]
extend_line = "-" * len(self.name)
return (
f"Constraint '{self.name}':\n"
Expand All @@ -78,18 +57,26 @@ def _repr_html_(self):
Get the html representation of the variables.
"""
# return self.__repr__()
data_string = self.to_array()._repr_html_()
data_string = self.labels._repr_html_()
data_string = data_string.replace("xarray.DataArray", "linopy.Constraint")
return data_string

@deprecated(details="Use the `labels` property instead of `to_array`")
def to_array(self):
"""
Convert the variable array to a xarray.DataArray.
"""
return DataArray(self)
return self.labels

@property
def labels(self):
return self.model.constraints.labels[self.name]

@labels.setter
def labels(self, value):
raise RuntimeError("Labels are read-only")

@property
@has_assigned_model
def coeffs(self):
"""
Get the left-hand-side coefficients of the constraint.
Expand All @@ -100,7 +87,6 @@ def coeffs(self):
return self.model.constraints.coeffs[self.name]

@coeffs.setter
@has_assigned_model
def coeffs(self, value):
term_dim = self.name + "_term"
value = DataArray(value).broadcast_like(self.vars, exclude=[term_dim])
Expand All @@ -115,7 +101,6 @@ def coeffs(self, value):
self.model.constraints.coeffs = coeffs

@property
@has_assigned_model
def vars(self):
"""
Get the left-hand-side variables of the constraint.
Expand All @@ -126,7 +111,6 @@ def vars(self):
return self.model.constraints.vars[self.name]

@vars.setter
@has_assigned_model
def vars(self, value):
term_dim = self.name + "_term"
value = DataArray(value).broadcast_like(self.coeffs, exclude=[term_dim])
Expand All @@ -141,7 +125,6 @@ def vars(self, value):
self.model.constraints.vars = vars

@property
@has_assigned_model
def lhs(self):
"""
Get the left-hand-side linear expression of the constraint.
Expand All @@ -155,7 +138,6 @@ def lhs(self):
return expressions.LinearExpression(Dataset({"coeffs": coeffs, "vars": vars}))

@lhs.setter
@has_assigned_model
def lhs(self, value):
if not isinstance(value, expressions.LinearExpression):
raise TypeError("Assigned lhs must be a LinearExpression.")
Expand All @@ -165,7 +147,6 @@ def lhs(self, value):
self.vars = value.vars

@property
@has_assigned_model
def sign(self):
"""
Get the signs of the constraint.
Expand All @@ -176,16 +157,14 @@ def sign(self):
return self.model.constraints.sign[self.name]

@sign.setter
@has_assigned_model
@is_constant
def sign(self, value):
value = DataArray(value).broadcast_like(self)
value = DataArray(value).broadcast_like(self.sign)
if (value == "==").any():
raise ValueError('Sign "==" not supported, use "=" instead.')
self.model.constraints.sign[self.name] = value

@property
@has_assigned_model
def rhs(self):
"""
Get the right hand side constants of the constraint.
Expand All @@ -196,12 +175,11 @@ def rhs(self):
return self.model.constraints.rhs[self.name]

@rhs.setter
@has_assigned_model
@is_constant
def rhs(self, value):
if isinstance(value, (variables.Variable, expressions.LinearExpression)):
raise TypeError(f"Assigned rhs must be a constant, got {type(value)}).")
value = DataArray(value).broadcast_like(self)
value = DataArray(value).broadcast_like(self.rhs)
self.model.constraints.rhs[self.name] = value

@property
Expand All @@ -219,6 +197,10 @@ def dual(self):
)
return self.model.dual[self.name]

@property
def shape(self):
return self.labels.shape


@dataclass(repr=False)
class Constraints:
Expand Down Expand Up @@ -268,7 +250,7 @@ def __getitem__(
self, names: Union[str, Sequence[str]]
) -> Union[Constraint, "Constraints"]:
if isinstance(names, str):
return Constraint(self.labels[names], model=self.model)
return Constraint(names, model=self.model)

return self.__class__(
self.labels[names],
Expand Down Expand Up @@ -581,7 +563,8 @@ def __init__(self, lhs, sign, rhs):
"""
if isinstance(rhs, (variables.Variable, expressions.LinearExpression)):
raise TypeError(f"Assigned rhs must be a constant, got {type(rhs)}).")
self._lhs, self._rhs = xr.align(lhs, DataArray(rhs))
lhs, self._rhs = xr.align(lhs.data, DataArray(rhs))
self._lhs = expressions.LinearExpression(lhs)
self._sign = DataArray(sign)

@property
Expand Down Expand Up @@ -696,7 +679,7 @@ class AnonymousScalarConstraint:
(rhs) for exactly one constraint.
"""

lhs: expressions.ScalarLinearExpression
lhs: "expressions.ScalarLinearExpression"
sign: str
rhs: float

Expand Down
Loading