Skip to content

Commit 5d3e90c

Browse files
Merge pull request #88 from PyPSA/improve-repr-follow-up
Improve repr follow up
2 parents 77f8f66 + f727c8e commit 5d3e90c

14 files changed

+323
-129
lines changed

.github/workflows/CI.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ jobs:
1616

1717
steps:
1818
- uses: actions/checkout@v2
19-
- name: Set up Python 3.8
19+
- name: Set up Python 3.9
2020
uses: actions/setup-python@v1
2121
with:
22-
python-version: 3.8
22+
python-version: 3.9
2323

2424
- name: Install ubuntu dependencies
2525
if: matrix.os == 'ubuntu-latest'

doc/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Upcoming Release
1616
* The classes Variable, LinearExpression, Constraint, ScalarVariable, ScalarLinearExpression and ScalarConstraint now require the model in the initialization (mostly internal code is affected).
1717
* The `eval` module was removed in favor of arithmetic operations on the classes `Variable`, `LinearExpression` and `Constraint`.
1818
* Solver options are now printed out in the console when solving a model.
19+
* If a variable with indexes differing from the model internal indexes are assigned, linopy will raise a warning and align the variable to the model indexes.
1920

2021
Version 0.0.15
2122
--------------

linopy/common.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ def best_int(max_value):
7575
return t
7676

7777

78+
def dictsel(d, keys):
79+
"Reduce dictionary to keys that appear in selection."
80+
return {k: v for k, v in d.items() if k in keys}
81+
82+
7883
def head_tail_range(stop, max_number_of_values=14):
7984
split_at = max_number_of_values // 2
8085
if stop > max_number_of_values:
@@ -84,17 +89,23 @@ def head_tail_range(stop, max_number_of_values=14):
8489

8590

8691
def print_coord(coord):
92+
if isinstance(coord, dict):
93+
coord = coord.values()
8794
if len(coord):
8895
return "[" + ", ".join([str(c) for c in coord]) + "]"
8996
else:
9097
return ""
9198

9299

93-
def print_single_variable(lower, upper, var, vartype):
94-
if vartype == "Binary Variable":
95-
return f"\n {var}"
100+
def print_single_variable(variable, name, coord, lower, upper):
101+
if name in variable.model.variables._integer_variables:
102+
bounds = "Z ⋂ " + f"[{lower},...,{upper}]"
103+
elif name in variable.model.variables._binary_variables:
104+
bounds = "{0, 1}"
96105
else:
97-
return f"\n{lower}{var}{upper}"
106+
bounds = f"[{lower}, {upper}]"
107+
108+
return f"{name}{print_coord(coord)}{bounds}"
98109

99110

100111
def print_single_expression(c, v, model):

linopy/constraints.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from linopy import expressions, variables
2323
from linopy.common import (
2424
_merge_inplace,
25+
dictsel,
2526
forward_as_properties,
2627
has_optimized_model,
2728
is_constant,
@@ -118,29 +119,20 @@ def __repr__(self):
118119
# create string, we use numpy to get the indexes
119120
data_string = ""
120121
idx = np.unravel_index(to_print, self.shape)
121-
indexes = np.stack(idx)
122-
coords = [self.indexes[self.dims[i]][idx[i]] for i in range(len(self.dims))]
122+
coords = [self.indexes[dim][idx[i]] for i, dim in enumerate(self.dims)]
123123

124124
# loop over all values to LinearExpression(print
125125
data_string = ""
126126
for i in range(len(to_print)):
127-
# this is the index for the labels array
128-
ix = tuple(indexes[..., i])
129-
# sign and rhs might only be defined for some dimensions
130-
six = tuple(
131-
ix[i] for i in range(self.ndim) if self.dims[i] in self.sign.dims
132-
)
133-
rix = tuple(
134-
ix[i] for i in range(self.ndim) if self.dims[i] in self.rhs.dims
135-
)
136-
137-
coord = [c[i] for c in coords]
127+
coord = {dim: c[i] for dim, c in zip(self.dims, coords)}
138128
coord_string = print_coord(coord)
139129
expr_string = print_single_expression(
140-
self.coeffs.values[ix], self.vars.values[ix], self.lhs.model
130+
self.coeffs.sel(coord).values,
131+
self.vars.sel(coord).values,
132+
self.lhs.model,
141133
)
142-
sign_string = f"{self.sign.values[six]}"
143-
rhs_string = f"{self.rhs.values[rix]}"
134+
sign_string = f"{self.sign.sel(**dictsel(coord, self.sign.dims)).item()}"
135+
rhs_string = f"{self.rhs.sel(**dictsel(coord, self.sign.dims)).item()}"
144136

145137
data_string += f"\n{self.name}{coord_string}: {expr_string} {sign_string} {rhs_string}"
146138

linopy/expressions.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,24 @@ def __init__(self, data, model):
166166
if not set(data).issuperset({"coeffs", "vars"}):
167167
raise ValueError("data must contain the variables 'coeffs' and 'vars'")
168168

169+
term_dims = [d for d in data.dims if d.endswith("_term")]
170+
if not term_dims:
171+
raise ValueError("data must contain one dimension ending with '_term'")
172+
term_dim = term_dims[0]
173+
169174
# make sure that all non-helper dims have coordinates
170-
if not all(dim in data.coords for dim in data.dims if not dim.startswith("_")):
171-
raise ValueError("data must have coordinates for all non-helper dimensions")
175+
missing_coords = set(data.dims) - set(data.coords) - {term_dim}
176+
if missing_coords:
177+
raise ValueError(
178+
f"Dimensions {missing_coords} have no coordinates. For "
179+
"consistency all dimensions must have coordinates."
180+
)
172181

173182
if np.issubdtype(data.vars, np.floating):
174183
data["vars"] = data.vars.fillna(-1).astype(int)
175184

176185
(data,) = xr.broadcast(data)
177-
data = data.transpose(..., "_term")
186+
data = data.transpose(..., term_dim)
178187

179188
if not isinstance(model, Model):
180189
raise ValueError("model must be an instance of linopy.Model")
@@ -205,9 +214,9 @@ def __repr__(self):
205214
return f"LinearExpression:\n-----------------\n{expr_string}"
206215

207216
# print only a few values
208-
max_prints = 14
209-
split_at = max_prints // 2
210-
to_print = head_tail_range(nexprs, max_prints)
217+
max_print = 14
218+
split_at = max_print // 2
219+
to_print = head_tail_range(nexprs, max_print)
211220
coords = self.unravel_coords(to_print)
212221

213222
# loop over all values to print
@@ -221,7 +230,7 @@ def __repr__(self):
221230

222231
data_string += f"\n{coord_string}: {expr_string}"
223232

224-
if i == split_at - 1 and nexprs > max_prints:
233+
if i == split_at - 1 and nexprs > max_print:
225234
data_string += "\n\t\t..."
226235

227236
# create shape string
@@ -452,6 +461,11 @@ def from_tuples(cls, *tuples, chunk=None):
452461
c = DataArray(c, v.coords)
453462
else:
454463
c = as_dataarray(c)
464+
# if a dimension is not in the coords, add it as a range index
465+
for i, dim in enumerate(c.dims):
466+
if dim not in c.coords:
467+
c = c.assign_coords(**{dim: pd.RangeIndex(c.shape[i])})
468+
455469
ds = Dataset({"coeffs": c, "vars": v}).expand_dims("_term")
456470
expr = cls(ds, model)
457471
exprs.append(expr)
@@ -803,10 +817,6 @@ def sanitize(self):
803817
def equals(self, other: "LinearExpression"):
804818
return self.data.equals(_expr_unwrap(other))
805819

806-
# TODO: make this return a LinearExpression (needs refactoring of __init__)
807-
def rename(self, name_dict=None, **names) -> Dataset:
808-
return self.data.rename(name_dict, **names)
809-
810820
def __iter__(self):
811821
return self.data.__iter__()
812822

@@ -843,6 +853,8 @@ def __iter__(self):
843853

844854
reindex = exprwrap(Dataset.reindex, fill_value=_fill_value)
845855

856+
rename = exprwrap(Dataset.rename)
857+
846858
rename_dims = exprwrap(Dataset.rename_dims)
847859

848860
roll = exprwrap(Dataset.roll)
@@ -894,8 +906,11 @@ def merge(*exprs, dim="_term", cls=LinearExpression):
894906
model = exprs[0].model
895907
exprs = [e.data if isinstance(e, cls) else e for e in exprs]
896908

897-
if not all(len(expr._term) == len(exprs[0]._term) for expr in exprs[1:]):
898-
exprs = [expr.assign_coords(_term=np.arange(len(expr._term))) for expr in exprs]
909+
if cls == LinearExpression:
910+
if not all(len(expr._term) == len(exprs[0]._term) for expr in exprs[1:]):
911+
exprs = [
912+
expr.assign_coords(_term=np.arange(len(expr._term))) for expr in exprs
913+
]
899914

900915
kwargs = dict(fill_value=cls._fill_value, coords="minimal", compat="override")
901916
ds = xr.concat(exprs, dim, **kwargs)

linopy/io.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,9 +461,12 @@ def to_netcdf(m, *args, **kwargs):
461461
**kwargs : TYPE
462462
Keyword arguments passed to ``xarray.Dataset.to_netcdf``.
463463
"""
464+
from linopy.expressions import LinearExpression
464465

465466
def get_and_rename(m, attr, prefix=""):
466467
ds = getattr(m, attr)
468+
if isinstance(ds, LinearExpression):
469+
ds = ds.data
467470
return ds.rename({v: prefix + attr + "-" + v for v in ds})
468471

469472
vars = [

linopy/model.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging
99
import os
1010
import re
11+
import warnings
1112
from pathlib import Path
1213
from tempfile import NamedTemporaryFile, gettempdir
1314

@@ -406,18 +407,18 @@ def add_variables(
406407
>>> m = Model()
407408
>>> time = pd.RangeIndex(10, name="Time")
408409
>>> m.add_variables(lower=0, coords=[time], name="x")
409-
Continuous Variable (Time: 10)
410-
------------------------------
411-
0 ≤ x[0] inf
412-
0 ≤ x[1] inf
413-
0 ≤ x[2] inf
414-
0 ≤ x[3] inf
415-
0 ≤ x[4] inf
416-
0 ≤ x[5] inf
417-
0 ≤ x[6] inf
418-
0 ≤ x[7] inf
419-
0 ≤ x[8] inf
420-
0 ≤ x[9] inf
410+
Variable (Time: 10)
411+
-------------------
412+
[0]: x[0] ∈ [0, inf]
413+
[1]: x[1] ∈ [0, inf]
414+
[2]: x[2] ∈ [0, inf]
415+
[3]: x[3] ∈ [0, inf]
416+
[4]: x[4] ∈ [0, inf]
417+
[5]: x[5] ∈ [0, inf]
418+
[6]: x[6] ∈ [0, inf]
419+
[7]: x[7] ∈ [0, inf]
420+
[8]: x[8] ∈ [0, inf]
421+
[9]: x[9] ∈ [0, inf]
421422
"""
422423
if name is None:
423424
name = "var" + str(self._varnameCounter)
@@ -451,11 +452,37 @@ def add_variables(
451452
lower = DataArray(-inf, coords=coords, **kwargs)
452453
upper = DataArray(inf, coords=coords, **kwargs)
453454

454-
labels = DataArray(coords=coords).assign_attrs(binary=binary, integer=integer)
455+
labels = DataArray(-2, coords=coords).assign_attrs(
456+
binary=binary, integer=integer
457+
)
458+
455459
# ensure order of dims is the same
456460
lower = lower.transpose(*[d for d in labels.dims if d in lower.dims])
457461
upper = upper.transpose(*[d for d in labels.dims if d in upper.dims])
458462

463+
if mask is not None:
464+
mask = DataArray(mask).astype(bool)
465+
mask, _ = xr.align(mask, labels, join="right")
466+
assert set(mask.dims).issubset(
467+
labels.dims
468+
), "Dimensions of mask not a subset of resulting labels dimensions."
469+
470+
# It is important to end up with monotonically increasing labels in the
471+
# model's variables container as we use it for indirect indexing.
472+
labels_reindexed = labels.reindex_like(self.variables.labels, fill_value=-1)
473+
if not labels.equals(labels_reindexed):
474+
warnings.warn(
475+
f"Reindexing variable {name} to match existing coordinates.",
476+
UserWarning,
477+
)
478+
labels = labels_reindexed
479+
lower = lower.reindex_like(labels)
480+
upper = upper.reindex_like(labels)
481+
if mask is None:
482+
mask = labels != -1
483+
else:
484+
mask = mask.reindex_like(labels_reindexed, fill_value=False)
485+
459486
self.check_force_dim_names(labels)
460487

461488
start = self._xCounter
@@ -464,10 +491,6 @@ def add_variables(
464491
self._xCounter += labels.size
465492

466493
if mask is not None:
467-
mask = DataArray(mask)
468-
assert set(mask.dims).issubset(
469-
labels.dims
470-
), "Dimensions of mask not a subset of resulting labels dimensions."
471494
labels = labels.where(mask, -1)
472495

473496
if self.chunk:

0 commit comments

Comments
 (0)