Note
You can download this example as a Jupyter notebook or start it in interactive mode.
Creating Variables#
Variables are created and assigned to the model using the function
model.add_variables
where model
is a linopy.Model
instance. In the following we show how this function works and what the resulting variables look like. So, let’s create a model and go through it!
[1]:
import numpy as np
import pandas as pd
import xarray as xr
from linopy import Model
m = Model()
First of all it is crucial to know, that the return value of the .add_variables
function is a linopy.Variable
which itself contains all important information and provides helpful functions. It can have an arbitrary number of labeled dimensions. For each combination of coordinates, exactly one representative scalar variable is defined and, in the end, passed to the solver.
The first three arguments of the .add_variables
function are 1. lower
denoting the lower bound of the variables (default -inf
) 2. upper
denoting the upper bound (default +inf
) 3. coords
(default None).
These argument determine the shape of the added variable.
Generally, the function is strongly aligned to the initialization of an xarray.DataArray
. Therefore lower
and upper
can be
scalar values (int/float)
numpy ndarray’s
pandas Series
pandas DataFrame’s
xarray DataArray’s
Note that scalars, numpy objects and pandas objects do not have or do not require dimension names. Thus, the naming of the dimensions is done by xarray
. Therefore you can pass the coords
argument, or alternatively, a dims
argument in order to name your dimensions.
Hint
It is best practice to always define variables with explicit name
and dimension names. This eases the inspection and avoids confusion from the automatically derived names.
Let’s start by creating a simple variable:
If we just keep the default, which is -inf
and +inf
for lower
and upper
, the code returns
[2]:
x = m.add_variables(name="x")
x
[2]:
Variable
--------
x ∈ [-inf, inf]
which is a variable without any coordinates and with just one optimization variable. The variable name is set by name = 'x'
.
Like this the variable appears with its name when defining expression with it:
[3]:
x + 5
[3]:
LinearExpression
----------------
+1 x + 5
We can alter the lower and upper bounds of the variable by assigning scalar values to them.
[4]:
y = m.add_variables(lower=0, upper=4, name="y")
Variable Types#
Per default the variable type is continuous, that the variables can take any real value in between and including the lower and upper bound. In order to alter the type, you have the option to set integer
or binary
to True
.
[5]:
m.add_variables(lower=0, upper=10, integer=True)
[5]:
Variable
--------
var0 ∈ Z ⋂ [0,...,10]
Note
Since we did not set the name argument the variable name is automatically determined and set to var0
.
This variable var0
can take all integer number between 0 and 10 inclusively. On the other hand, when defining a binary variable, we do not specify the lower and upper bounds and set binary
to true.
[6]:
m.add_variables(binary=True)
[6]:
Variable
--------
var1 ∈ {0, 1}
Working with dimensions#
When initializing dimensional variables, it is most straight-forward and recommended to create variables with DataArray
’s which are passed to the as lower
and/or upper
.
[7]:
lower = xr.DataArray([1, 2, 3])
v = m.add_variables(lower, name="v")
v
[7]:
Variable (dim_0: 3)
-------------------
[0]: v[0] ∈ [1, inf]
[1]: v[1] ∈ [2, inf]
[2]: v[2] ∈ [3, inf]
The returned Variable
now has the same shape as the lower
bound that we passed to the initialization. Since we did not specify any dimension name, it defaults to dim_0
. In order to give the dimension a proper name we can use the dims
argument.
[8]:
lower = xr.DataArray([1, 2, 3], dims=["my-dim"])
m.add_variables(lower)
[8]:
Variable (my-dim: 3)
--------------------
[0]: var2[0] ∈ [1, inf]
[1]: var2[1] ∈ [2, inf]
[2]: var2[2] ∈ [3, inf]
You can arbitrarily broadcast dimensions when passing DataArray’s with different set of dimensions. Let’s do it and give lower
another dimension than upper
:
[9]:
lower = xr.DataArray([1, 2, 3], dims=["my-dim"])
upper = xr.DataArray([10, 11, 12, 13], dims=["my-dim-2"])
m.add_variables(lower, upper)
[9]:
Variable (my-dim: 3, my-dim-2: 4)
---------------------------------
[0, 0]: var3[0, 0] ∈ [1, 10]
[0, 1]: var3[0, 1] ∈ [1, 11]
[0, 2]: var3[0, 2] ∈ [1, 12]
[0, 3]: var3[0, 3] ∈ [1, 13]
[1, 0]: var3[1, 0] ∈ [2, 10]
[1, 1]: var3[1, 1] ∈ [2, 11]
[1, 2]: var3[1, 2] ∈ [2, 12]
[1, 3]: var3[1, 3] ∈ [2, 13]
[2, 0]: var3[2, 0] ∈ [3, 10]
[2, 1]: var3[2, 1] ∈ [3, 11]
[2, 2]: var3[2, 2] ∈ [3, 12]
[2, 3]: var3[2, 3] ∈ [3, 13]
Now instead of a single dimension, we end up with two dimensions my-dim
and my-dim-2
in the variable. This kind of broadcasting is a deeply incorporated in the functionality of linopy.
We recall that, in order to improve the inspection, it is encouraged to define a name
when creating a variable. So in your model you would rather write something like:
[10]:
lower = xr.DataArray([1, 2, 3], dims=["time"])
upper = xr.DataArray([10, 11, 12, 13], dims=["station"])
m.add_variables(lower, upper, name="supply")
[10]:
Variable (time: 3, station: 4)
------------------------------
[0, 0]: supply[0, 0] ∈ [1, 10]
[0, 1]: supply[0, 1] ∈ [1, 11]
[0, 2]: supply[0, 2] ∈ [1, 12]
[0, 3]: supply[0, 3] ∈ [1, 13]
[1, 0]: supply[1, 0] ∈ [2, 10]
[1, 1]: supply[1, 1] ∈ [2, 11]
[1, 2]: supply[1, 2] ∈ [2, 12]
[1, 3]: supply[1, 3] ∈ [2, 13]
[2, 0]: supply[2, 0] ∈ [3, 10]
[2, 1]: supply[2, 1] ∈ [3, 11]
[2, 2]: supply[2, 2] ∈ [3, 12]
[2, 3]: supply[2, 3] ∈ [3, 13]
Initializing variables with numpy arrays#
If lower
and upper
are numpy arrays, linopy
it is recommended to pass a dims
or a coords
argument.
[11]:
lower = np.array([1, 2])
upper = np.array([10, 10])
m.add_variables(lower, upper, dims=["my-dim"])
[11]:
Variable (my-dim: 2)
--------------------
[0]: var4[0] ∈ [1, 10]
[1]: var4[1] ∈ [2, 10]
This is equivalent to the following
[12]:
my_dim = pd.RangeIndex(2, name="my-dim")
lower = np.array([1, 2])
upper = np.array([10, 10])
m.add_variables(lower, upper, coords=[my_dim])
[12]:
Variable (my-dim: 2)
--------------------
[0]: var5[0] ∈ [1, 10]
[1]: var5[1] ∈ [2, 10]
Note that
dims
is a list of string defining the dimension names.coords
is an tuple of indexes as expected byxarray.DataArray
.The shape of
lower
andupper
is aligned withcoords
.When defining the index for the coords, a name was set in the index creation. This is helpful as we can ensure which dimension the variable is defined on.
Let’s make the same example without setting an explicit dimension name:
[13]:
coords = (pd.RangeIndex(2),)
m.add_variables(lower=lower, coords=coords)
[13]:
Variable (dim_0: 2)
-------------------
[0]: var6[0] ∈ [1, inf]
[1]: var6[1] ∈ [2, inf]
The dimension is now called dim_0
, any new assignment of variable without dimension names, will also use that dimension name. When combining the variables to expressions it is important that you make sure that dimension names represent what they should.
Hint
If you want to make sure, you are not messing up with dimensions, create the model with the flag force_dim_names = True
, i.e.
[14]:
other = Model(force_dim_names=True)
try:
other.add_variables(lower=lower, coords=coords)
except ValueError as e:
print("This raised an error:", e)
This raised an error: Added data contains non-customized dimension names. This is not allowed when setting `force_dim_names` to True.
Initializing variables with Pandas objects#
Pandas objects always have indexes but do not require dimension names. It is again helpful to ensure that the variable have explicit dimension names, when passing lower
and upper
without coords
. This can be done by either passing the dims
argument to the .add_variables
function, i.e.
[15]:
lower = pd.Series([1, 1])
upper = pd.Series([10, 12])
m.add_variables(lower, upper, dims=["my-dim"])
[15]:
Variable (my-dim: 2)
--------------------
[0]: var7[0] ∈ [1, 10]
[1]: var7[1] ∈ [1, 12]
or naming the indexes and columns of the pandas objects directly, e.g.
[16]:
lower = pd.Series([1, 1]).rename_axis("my-dim")
upper = pd.Series([10, 12]).rename_axis("my-dim")
m.add_variables(lower, upper)
[16]:
Variable (my-dim: 2)
--------------------
[0]: var8[0] ∈ [1, 10]
[1]: var8[1] ∈ [1, 12]
Note
Again, if lower
and upper
do not have the same dimension names, the arrays are broadcasted, meaning the dimensions are spanned:
[17]:
lower = pd.Series([1, 1]).rename_axis("my-dim")
upper = pd.Series([10, 12]).rename_axis("my-other-dim")
m.add_variables(lower, upper)
[17]:
Variable (my-dim: 2, my-other-dim: 2)
-------------------------------------
[0, 0]: var9[0, 0] ∈ [1, 10]
[0, 1]: var9[0, 1] ∈ [1, 12]
[1, 0]: var9[1, 0] ∈ [1, 10]
[1, 1]: var9[1, 1] ∈ [1, 12]
Now instead of 2 variables, 4 variables were defined.
The similar bahvior accounts for the case when passing a DataFrame and a Series without dimension names. The index axis is the first axis of both objects, thus these are expected to be the same (Note that pandas convention, is that Series are aligned and broadcasted along the column dimension of DataFrames):
[18]:
lower = pd.DataFrame([[1, 1, 2], [1, 2, 2]])
upper = pd.Series([10, 12])
m.add_variables(lower, upper)
[18]:
Variable (dim_0: 2, dim_1: 3)
-----------------------------
[0, 0]: var10[0, 0] ∈ [1, 10]
[0, 1]: var10[0, 1] ∈ [1, 10]
[0, 2]: var10[0, 2] ∈ [2, 10]
[1, 0]: var10[1, 0] ∈ [1, 12]
[1, 1]: var10[1, 1] ∈ [2, 12]
[1, 2]: var10[1, 2] ∈ [2, 12]
Again, one is always safer when explicitly naming the dimensions:
[19]:
lower = lower.rename_axis(index="my-dim", columns="my-other-dim")
upper = upper.rename_axis("my-dim")
m.add_variables(lower, upper)
[19]:
Variable (my-dim: 2, my-other-dim: 3)
-------------------------------------
[0, 0]: var11[0, 0] ∈ [1, 10]
[0, 1]: var11[0, 1] ∈ [1, 10]
[0, 2]: var11[0, 2] ∈ [2, 10]
[1, 0]: var11[1, 0] ∈ [1, 12]
[1, 1]: var11[1, 1] ∈ [2, 12]
[1, 2]: var11[1, 2] ∈ [2, 12]
**New in version 0.3.6**
As pandas objects always have indexes, the `coords` argument is not required and is ignored is passed. Before, it was used to overwrite the indexes of the pandas objects. A warning is raised if `coords` is passed and if these are not aligned with the pandas object.
[20]:
unaligned_coords = pd.Index([1, 2]), pd.Index([2, 3, 4])
m.add_variables(lower, upper, coords=unaligned_coords)
/home/docs/checkouts/readthedocs.org/user_builds/linopy/envs/latest/lib/python3.12/site-packages/linopy/common.py:154: UserWarning: coords for dimension(s) ['my-dim', 'my-other-dim'] is not aligned with the pandas object. Previously, the indexes of the pandas were ignored and overwritten in these cases. Now, the pandas object's coordinates are taken considered for alignment.
warn(
/home/docs/checkouts/readthedocs.org/user_builds/linopy/envs/latest/lib/python3.12/site-packages/linopy/common.py:154: UserWarning: coords for dimension(s) ['my-dim'] is not aligned with the pandas object. Previously, the indexes of the pandas were ignored and overwritten in these cases. Now, the pandas object's coordinates are taken considered for alignment.
warn(
[20]:
Variable (my-dim: 2, my-other-dim: 3)
-------------------------------------
[0, 0]: var12[0, 0] ∈ [1, 10]
[0, 1]: var12[0, 1] ∈ [1, 10]
[0, 2]: var12[0, 2] ∈ [2, 10]
[1, 0]: var12[1, 0] ∈ [1, 12]
[1, 1]: var12[1, 1] ∈ [2, 12]
[1, 2]: var12[1, 2] ∈ [2, 12]
Masking Arrays#
In some cases, you want to create a variable with given dimensions, but not all parts should be active.
For example, think about an set of ports between which goods can be transported. However, a port cannot transport goods to itself. For such a case, you would create an variable transport
which has the dimension (from
, to
) with values on the diagonal disabled.
Therefore, you can pass a mask
argument which has False
values on the diagonal and True
elsewhere.
[21]:
ports = list("abcdef")
port_from = pd.Index(ports, name="from")
port_to = pd.Index(ports, name="to")
mask = np.ones((len(ports), len(ports)), dtype=bool)
np.fill_diagonal(mask, False)
mask
[21]:
array([[False, True, True, True, True, True],
[ True, False, True, True, True, True],
[ True, True, False, True, True, True],
[ True, True, True, False, True, True],
[ True, True, True, True, False, True],
[ True, True, True, True, True, False]])
[22]:
transport = m.add_variables(
lower=0, coords=[port_from, port_to], name="transport", mask=mask
)
transport
[22]:
Variable (from: 6, to: 6) - 6 masked entries
--------------------------------------------
[a, a]: None
[a, b]: transport[a, b] ∈ [0, inf]
[a, c]: transport[a, c] ∈ [0, inf]
[a, d]: transport[a, d] ∈ [0, inf]
[a, e]: transport[a, e] ∈ [0, inf]
[a, f]: transport[a, f] ∈ [0, inf]
[b, a]: transport[b, a] ∈ [0, inf]
...
[e, f]: transport[e, f] ∈ [0, inf]
[f, a]: transport[f, a] ∈ [0, inf]
[f, b]: transport[f, b] ∈ [0, inf]
[f, c]: transport[f, c] ∈ [0, inf]
[f, d]: transport[f, d] ∈ [0, inf]
[f, e]: transport[f, e] ∈ [0, inf]
[f, f]: None
Now the diagonal values, for example at the variable at [a,a], are None
.
Accessing assigned variables#
All variables added to the model are stored in the .variables
container.
[23]:
m.variables
[23]:
linopy.model.Variables
----------------------
* x
* y
* var0
* var1
* v (dim_0)
* var2 (my-dim)
* var3 (my-dim, my-dim-2)
* supply (time, station)
* var4 (my-dim)
* var5 (my-dim)
* var6 (dim_0)
* var7 (my-dim)
* var8 (my-dim)
* var9 (my-dim, my-other-dim)
* var10 (dim_0, dim_1)
* var11 (my-dim, my-other-dim)
* var12 (my-dim, my-other-dim)
* transport (from, to)
You can always access the variables from the .variables
container either by get-item, i.e.
[24]:
m.variables["x"]
[24]:
Variable
--------
x ∈ [-inf, inf]
or by attribute accessing
[25]:
m.variables.x
[25]:
Variable
--------
x ∈ [-inf, inf]