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

$x \le \frac{10}{3}$

or

$3 x \le 10$

and not as

$x - \frac{3}{10} \le 0$

## 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()


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"]

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"]

def bound(m, i, j):
if i % 2:
return (i / 2) * b[i, j] >= i
else:
return i * b[i, j]  == 0.

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

[ ]: