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]:
<linopy.LinearExpression>
Dimensions:  (_term: 1)
Dimensions without coordinates: _term
Data:
    coeffs   (_term) int64 3
    vars     (_term) int64 0

When applying one of the operators <=, >=, == to the expression, an anomymous constraint is built:

[4]:
con = lhs <= 10
con
[4]:
Anonymous Constraint:
---------------------

coeffs   (_term) int64 3
vars     (_term) int64 0
<=
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]:
<linopy.LinearExpression>
Dimensions:  (_term: 1)
Dimensions without coordinates: _term
Data:
    coeffs   (_term) int64 3
    vars     (_term) int64 0
[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]:
<linopy.Constraint 'my-constraint' ()>
array(0)

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]:
<linopy.Constraint 'the-same-constraint' ()>
array(1)

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]:
<linopy.LinearExpression>
Dimensions:  (_term: 1)
Dimensions without coordinates: _term
Data:
    coeffs   (_term) int64 3
    vars     (_term) int64 0

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), dims='my-dim')
coeff * x
[10]:
<linopy.LinearExpression>
Dimensions:  (my-dim: 5, _term: 1)
Dimensions without coordinates: my-dim, _term
Data:
    coeffs   (my-dim, _term) int64 0 1 2 3 4
    vars     (my-dim, _term) int64 0 0 0 0 0

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]:
<linopy.LinearExpression>
Dimensions:  (_term: 3)
Dimensions without coordinates: _term
Data:
    coeffs   (_term) int64 3 -2 6
    vars     (_term) int64 0 1 2

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), dims='additional-dim')
tuples = (coeff, x), (-2, y), (6, z)
expr = m.linexpr(*tuples)
expr
[14]:
<linopy.LinearExpression>
Dimensions:  (additional-dim: 3, _term: 3)
Dimensions without coordinates: additional-dim, _term
Data:
    coeffs   (additional-dim, _term) int64 0 -2 6 1 -2 6 2 -2 6
    vars     (additional-dim, _term) int64 0 1 2 0 1 2 0 1 2

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]:
<linopy.LinearExpression>
Dimensions:         (additional-dim: 3, _term: 3)
Coordinates:
  * additional-dim  (additional-dim) int64 0 1 2
Dimensions without coordinates: _term
Data:
    coeffs          (additional-dim, _term) int64 0 -2 6 1 -2 6 2 -2 6
    vars            (additional-dim, _term) int64 0 1 2 0 1 2 0 1 2

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]:
3 * x[0]
[17]:
ScalarLinearExpression(coeffs=(3,), vars=(0,), coords=None)

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 - 1) * b[i - 1, j]
     else:
         return i * b[i, j]

expr = m.linexpr(bound, coords)
expr
[18]:
<linopy.LinearExpression>
Dimensions:  (dim_0: 10, dim_1: 2, _term: 1)
Coordinates:
  * dim_0    (dim_0) int64 0 1 2 3 4 5 6 7 8 9
  * dim_1    (dim_1) <U1 'a' 'b'
Dimensions without coordinates: _term
Data:
    coeffs   (dim_0, dim_1, _term) int64 0 0 0 0 2 2 2 2 4 4 4 4 6 6 6 6 8 8 8 8
    vars     (dim_0, dim_1, _term) int64 3 4 3 4 7 8 7 ... 16 15 16 19 20 19 20

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(lhs=ScalarLinearExpression(coeffs=(1,), vars=(0,), coords=None), sign='<=', rhs=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 - 1) * b[i - 1, j] >= i
     else:
         return i * b[i, j]  == 0.

con = m.add_constraints(bound, coords=coords)
con.lhs
[20]:
<linopy.LinearExpression>
Dimensions:  (dim_0: 10, dim_1: 2, _term: 1)
Coordinates:
  * dim_0    (dim_0) int64 0 1 2 3 4 5 6 7 8 9
  * dim_1    (dim_1) <U1 'a' 'b'
Dimensions without coordinates: _term
Data:
    coeffs   (dim_0, dim_1, _term) int64 0 0 0 0 2 2 2 2 4 4 4 4 6 6 6 6 8 8 8 8
    vars     (dim_0, dim_1, _term) int64 23 24 23 24 27 28 ... 35 36 39 40 39 40