Note
You can download this example as a Jupyter notebook or start it in interactive mode.
Creating constraints
Constraints are created and at the same time assigned to the model using the function
model.add_constraints
where model
is a linopy.Model
instance. Again, we want to understand this function and its argument. So, let’s create a model first.
[1]:
from linopy import Model
import numpy as np
import pandas as pd
import xarray as xr
m = Model()
linopy
follows the convention that all variables stand on the left-hand-side (lhs) of a constraint. In contrast, constant values are on the right-hand-side (rhs). Given a variable x
which has to by lower than 10/3, the constraint would be formulated as
or
and not as
Using arithmetic operations
Typically the lhs is given as a linear expression built by an arithmetic linear combination of variables, e.g.
[2]:
x = m.add_variables()
[3]:
lhs = 3 * x
lhs
[3]:
LinearExpression:
-----------------
3.0 var0
When applying one of the operators <=
, >=
, ==
to the expression, an anomymous constraint is built:
[4]:
con = lhs <= 10
con
[4]:
AnomymousConstraint
-------------------
3.0 var0 <= 10
Why is it anonymous? Because it is not yet added to the model. We can inspect the elements of the anonymous constraint:
[5]:
con.lhs
[5]:
LinearExpression:
-----------------
3.0 var0
[6]:
con.rhs
[6]:
<xarray.DataArray ()> array(10)
The attributes of the AnonymousConstraint
are immutable, thus con.rhs = 20
would raise an error.
We can now add the constraint to the model by passing the AnonymousConstraint
to the .add_constraint
function.
[7]:
c = m.add_constraints(con, name='my-constraint')
c
[7]:
Constraint `my-constraint`
--------------------------
3.0 var0 <= 10
Note the same output would be generated if passing lhs, sign and rhs as separate arguments to the function:
[8]:
m.add_constraints(lhs, "<=", 10, name='the-same-constraint')
[8]:
Constraint `the-same-constraint`
--------------------------------
3.0 var0 <= 10
Note that the return value of the operation is a Constraint
which contains the reference labels to the constraints in the optimization model. Also is redirects to its lhs, sign and rhs, for example we can call
[9]:
c.lhs
[9]:
LinearExpression:
-----------------
3.0 var0
to inspect the lhs of a defined constraint.
Multiplication with arrays
When multiplying variables with coefficients, the dimension handling follows the convention of xarray
. That is, non-overlapping dimensions are spanned and broadcasted. For example, let’s multiply x
with an array going from 0 to 5:
[10]:
coeff = xr.DataArray(np.arange(5), coords={'my-dim': pd.RangeIndex(5)})
coeff * x
[10]:
LinearExpression (my-dim: 5):
-----------------------------
[0]: 0.0 var0
[1]: 1.0 var0
[2]: 2.0 var0
[3]: 3.0 var0
[4]: 4.0 var0
Now, an expression of shape 5 with one term is created.
Note: It is strongly recommended to use xarray.DataArray
’s for multiplying coefficients with Variable
’s. It is also possible to use numpy arrays, however these are less secure considering the dimension handling. It is not recommended to use pandas
objects, as these do not preserve the linopy
types.
Using tuples
For long expression, it can be more performant to create linear expressions with tuples instead of arithmetic operations, as the latter are calculated iteratively. Therefore, the model’s .linexpr
combines the expression parallelly and also ensures the correct conversion of data types. Let’s create two other variables first
[11]:
y = m.add_variables()
z = m.add_variables()
and a expression using the .linexpr
function. Here, the convention is to pass pair of coefficients and variables for each term:
[12]:
tuples = (3, x), (-2, y), (6, z)
expr = m.linexpr(*tuples)
expr
[12]:
LinearExpression:
-----------------
3.0 var0 - 2.0 var1 + 6.0 var2
We can now use this expression in the add_constraints
function.
[13]:
con = m.add_constraints(expr >= 30)
Again, combining variables with arrays of coefficients is possible and more secure with the usage of tuples.
[14]:
coeff = xr.DataArray(range(3), {'additional-dim': pd.RangeIndex(3)})
tuples = (coeff, x), (-2, y), (6, z)
expr = m.linexpr(*tuples)
expr
[14]:
LinearExpression (additional-dim: 3):
-------------------------------------
[0]: 0.0 var0 - 2.0 var1 + 6.0 var2
[1]: 1.0 var0 - 2.0 var1 + 6.0 var2
[2]: 2.0 var0 - 2.0 var1 + 6.0 var2
Moreover, the usage of pandas objects as coefficients is possible. However in most cases, these have to have explicit dimension names, otherwise it will raise an error.
[15]:
coeff = pd.Series(range(3))
tuples = (coeff, x), (-2, y), (6, z)
try:
expr = m.linexpr(*tuples)
except ValueError as e:
print("This raises an error:", e)
This raises an error: different number of dimensions on data and dims: 1 vs 0
Correct would be:
[16]:
coeff = coeff.rename_axis('additional-dim')
tuples = (coeff, x), (-2, y), (6, z)
m.linexpr(*tuples)
[16]:
LinearExpression (additional-dim: 3):
-------------------------------------
[0]: 0.0 var0 - 2.0 var1 + 6.0 var2
[1]: 1.0 var0 - 2.0 var1 + 6.0 var2
[2]: 2.0 var0 - 2.0 var1 + 6.0 var2
Using rules
Similar to the implementation in Pyomo, expressions and constraints can be created using a combination of a function and a set of coordinates to iterate over. For creating expressions, the function itself has to return a ScalarLinearExpression
which can be obtained by selecting single values of the variables are combining them:
[17]:
x[0]
[17]:
ScalarVariable: var0
For example
[18]:
coords = pd.RangeIndex(10), ["a", "b"]
b = m.add_variables(0, 100, coords)
def bound(m, i, j):
if i % 2:
return (i / 2) * b[i, j]
else:
return i * b[i, j]
expr = m.linexpr(bound, coords)
expr
[18]:
LinearExpression (dim_0: 10, dim_1: 2):
---------------------------------------
[0, a]: 0.0 var3[0, a]
[0, b]: 0.0 var3[0, b]
[1, a]: 0.5 var3[1, a]
[1, b]: 0.5 var3[1, b]
[2, a]: 2.0 var3[2, a]
[2, b]: 2.0 var3[2, b]
[3, a]: 1.5 var3[3, a]
...
[6, b]: 6.0 var3[6, b]
[7, a]: 3.5 var3[7, a]
[7, b]: 3.5 var3[7, b]
[8, a]: 8.0 var3[8, a]
[8, b]: 8.0 var3[8, b]
[9, a]: 4.5 var3[9, a]
[9, b]: 4.5 var3[9, b]
Note that the function’s first argument has to be the model itself, even though it might not be used in the function.
This functionality is also supported by the .add_constraints
function. When passing a function as a first argument, .add_constraints
expects coords
to by non-empty. The function itself has to return a AnonymousScalarConstraint
, as done by
[19]:
x[0] <= 3
[19]:
AnonymousScalarConstraint: 1.0 var0 <= 3
For example
[20]:
coords = pd.RangeIndex(10), ["a", "b"]
b = m.add_variables(0, 100, coords)
def bound(m, i, j):
if i % 2:
return (i / 2) * b[i, j] >= i
else:
return i * b[i, j] == 0.
con = m.add_constraints(bound, coords=coords)
con
[20]:
Constraint `con1` (dim_0: 10, dim_1: 2)
---------------------------------------
[0, a]: 0.0 var4[0, a] = 0.0
[0, b]: 0.0 var4[0, b] = 0.0
[1, a]: 0.5 var4[1, a] >= 1.0
[1, b]: 0.5 var4[1, b] >= 1.0
[2, a]: 2.0 var4[2, a] = 0.0
[2, b]: 2.0 var4[2, b] = 0.0
[3, a]: 1.5 var4[3, a] >= 3.0
...
[6, b]: 6.0 var4[6, b] = 0.0
[7, a]: 3.5 var4[7, a] >= 7.0
[7, b]: 3.5 var4[7, b] >= 7.0
[8, a]: 8.0 var4[8, a] = 0.0
[8, b]: 8.0 var4[8, b] = 0.0
[9, a]: 4.5 var4[9, a] >= 9.0
[9, b]: 4.5 var4[9, b] >= 9.0
[ ]: