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
```

```
[ ]:
```

```
```