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
m = Model()
Given a variable x which has to by lower than 10/3, the constraint would be formulated as
or
or
of which all formulations can be written out with linopy just like that.
[2]:
x = m.add_variables(name="x")
When applying one of the operators <=, >=, == to the expression, an unassigned constraint is built:
[3]:
con = 3 * x <= 10
con
[3]:
Constraint (unassigned)
-----------------------
+3 x ≤ 10.0
Unasssigned means, it is not yet added to the model. We can inspect the elements of the anonymous constraint:
[4]:
con.lhs
[4]:
LinearExpression
----------------
+3 x
[5]:
con.rhs
[5]:
<xarray.DataArray 'rhs' ()> Size: 8B array(10.)
We can now add the constraint to the model by passing the unassigned Constraint to the .add_constraint function.
[6]:
c = m.add_constraints(con, name="my-constraint")
c
[6]:
Constraint `my-constraint`
--------------------------
+3 x ≤ 10.0
The same output would be generated if passing lhs, sign and rhs as separate arguments to the function:
[7]:
m.add_constraints(3 * x <= 10, name="the-same-constraint")
[7]:
Constraint `the-same-constraint`
--------------------------------
+3 x ≤ 10.0
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
[8]:
c.lhs
[8]:
LinearExpression
----------------
+3 x
to inspect the lhs of a defined constraint.
When moving the constant value to the left hand side in the initialization, it will be pulled to the right hand side as soon as the constraint is defined
[9]:
3 * x - 10
[9]:
LinearExpression
----------------
+3 x - 10
[10]:
3 * x - 10 <= 0
[10]:
Constraint (unassigned)
-----------------------
+3 x ≤ 10.0
Like this, the all defined constraints have a clear separation between variable on the left, and constants on the right.
All constraints are added to the .constraints container from where all assigned constraints can be accessed.
[11]:
m.constraints
[11]:
linopy.model.Constraints
------------------------
* my-constraint
* the-same-constraint
[12]:
m.constraints["my-constraint"]
[12]:
Constraint `my-constraint`
--------------------------
+3 x ≤ 10.0
Coordinate Alignment in Constraints#
As an alternative to the <=, >=, == operators, linopy provides .le(), .ge(), and .eq() methods on variables and expressions. These methods accept a join parameter ("inner", "outer", "left", "right") for explicit control over how coordinates are aligned when creating constraints. See the :doc:coordinate-alignment guide for details.
CSR Backend (Advanced)#
By default, linopy stores each constraint as an xarray.Dataset (Constraint). This is flexible and allows full label-based indexing, but can use significant memory when constraints have many terms.
For large models, linopy provides an alternative CSR backend via the CSRConstraint class. It stores the constraint coefficients as a scipy CSR sparse matrix with flat numpy arrays for the right-hand side and signs. This can reduce memory usage by up to 90% and speeds up matrix generation for direct solver APIs by 30–120x.
CSRConstraint is immutable — once frozen, the constraint data cannot be modified in place. You can always convert back to the mutable xarray-backed Constraint if needed.
Freezing individual constraints#
Pass freeze=True to add_constraints to store a single constraint in CSR format:
[13]:
import numpy as np
from linopy import Model
m2 = Model()
y = m2.add_variables(coords=[np.arange(100)], name="y")
m2.add_constraints(y <= 10, name="upper", freeze=True)
print(type(m2.constraints["upper"]))
m2.constraints["upper"]
<class 'linopy.constraints.CSRConstraint'>
[13]:
Constraint `upper` [dim_0: 100]:
--------------------------------
[0]: +1 y[0] ≤ 10.0
[1]: +1 y[1] ≤ 10.0
[2]: +1 y[2] ≤ 10.0
[3]: +1 y[3] ≤ 10.0
[4]: +1 y[4] ≤ 10.0
[5]: +1 y[5] ≤ 10.0
[6]: +1 y[6] ≤ 10.0
...
[93]: +1 y[93] ≤ 10.0
[94]: +1 y[94] ≤ 10.0
[95]: +1 y[95] ≤ 10.0
[96]: +1 y[96] ≤ 10.0
[97]: +1 y[97] ≤ 10.0
[98]: +1 y[98] ≤ 10.0
[99]: +1 y[99] ≤ 10.0
Freezing all constraints globally#
Set freeze_constraints=True on the Model to automatically freeze every constraint added via add_constraints:
[14]:
m3 = Model(freeze_constraints=True)
z = m3.add_variables(coords=[np.arange(50)], name="z")
m3.add_constraints(z >= 0, name="lower")
m3.add_constraints(z <= 100, name="upper")
print(type(m3.constraints["lower"]))
print(type(m3.constraints["upper"]))
<class 'linopy.constraints.CSRConstraint'>
<class 'linopy.constraints.CSRConstraint'>
Converting between representations#
Use .freeze() and .mutable() to convert between the two representations. The conversion is lossless:
[15]:
frozen = m3.constraints["lower"]
print(f"Frozen type: {type(frozen).__name__}")
thawed = frozen.mutable()
print(f"Mutable type: {type(thawed).__name__}")
refrozen = thawed.freeze()
print(f"Re-frozen type: {type(refrozen).__name__}")
Frozen type: CSRConstraint
Mutable type: Constraint
Re-frozen type: CSRConstraint
API differences from Constraint#
CSRConstraint deliberately exposes a narrower API than the xarray-backed Constraint:
No in-place mutation. Setters such as
con.coeffs = ...,con.vars = ...,con.sign = ...,con.rhs = ..., andcon.lhs = ...are only available onConstraint.No label-based indexing.
con.loc[...]is only available onConstraint.Accessing ``.coeffs`` / ``.vars`` triggers reconstruction. On a
CSRConstraintthese properties rebuild the full xarrayDataseton demand and emit aPerformanceWarning. For solver-oriented workflows prefercon.to_matrix()or work with the CSR data directly.
If you need any of the above, call .mutable() first to get a Constraint:
con = m.constraints["my_constraint"].mutable()
con.loc[{"time": 0}] # label-based indexing now available
con.rhs = 5 # mutation now available
When to use the CSR backend#
The CSR backend is most beneficial when:
Your model has many constraints with many terms.
Memory is a bottleneck.
You use a direct solver API (e.g. HiGHS, Gurobi Python bindings) rather than file-based I/O.
For small models the overhead is negligible and the default xarray-backed Constraint is perfectly fine.
Additionally, if you don’t need variable and constraint names in the solver (e.g. for batch solves), you can disable name export for extra speed:
m = Model(freeze_constraints=True, set_names_in_solver_io=False)