Skip to content

Commit b192cf6

Browse files
committed
Document how to add linear constraints in matrix form
1 parent 6ff7b8b commit b192cf6

File tree

1 file changed

+112
-0
lines changed

1 file changed

+112
-0
lines changed

docs/source/faq.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,115 @@ env.start()
2929

3030
model = gurobi.Model(env)
3131
```
32+
33+
## How to add linear constraints in matrix form like $Ax \leq b$?
34+
35+
In YALMIP, you can use the matrix form $Ax \leq b$ to add linear constraints, which is quite convenient.
36+
37+
In PyOptInterface, you can use the following code to add linear constraints in matrix form:
38+
39+
```python
40+
import pyoptinterface as poi
41+
from pyoptinterface import gurobi
42+
43+
import numpy as np
44+
from scipy.sparse import csr_array, sparray, eye_array
45+
46+
47+
def iterate_sparse_matrix_rows(A):
48+
"""
49+
Iterate over rows of a sparse matrix and get non-zero elements for each row.
50+
51+
A is a 2-dimensional scipy sparse matrix
52+
isinstance(A, scipy.sparse.sparray) = True and A.ndim = 2
53+
"""
54+
if not isinstance(A, csr_array):
55+
A = csr_array(A) # Convert to CSR format if not already
56+
57+
for i in range(A.shape[0]):
58+
row_start = A.indptr[i]
59+
row_end = A.indptr[i + 1]
60+
row_indices = A.indices[row_start:row_end]
61+
row_data = A.data[row_start:row_end]
62+
yield row_indices, row_data
63+
64+
65+
def add_matrix_constraints(model, A, x, sense, b):
66+
"""
67+
add constraints Ax <= / = / >= b
68+
69+
A is a 2-dimensional numpy array or scipy sparse matrix
70+
x is an iterable of variables
71+
sense is one of (poi.Leq, poi.Eq, poi.Geq)
72+
b is an iterable of values or a single scalar
73+
"""
74+
75+
is_ndarray = isinstance(A, np.ndarray)
76+
is_sparse = isinstance(A, sparray)
77+
78+
if not is_ndarray and not is_sparse:
79+
raise ValueError("A must be a numpy array or scipy.sparse array")
80+
81+
ndim = A.ndim
82+
if ndim != 2:
83+
raise ValueError("A must be a 2-dimensional array")
84+
85+
M, N = A.shape
86+
87+
# turn x into a list if x is an iterable
88+
if isinstance(x, poi.tupledict):
89+
x = x.values()
90+
x = list(x)
91+
92+
if len(x) != N:
93+
raise ValueError("x must have length equal to the number of columns of A")
94+
95+
# check b
96+
if np.isscalar(b):
97+
b = np.full(M, b)
98+
elif len(b) != M:
99+
raise ValueError("b must have length equal to the number of rows of A")
100+
101+
constraints = []
102+
103+
if is_ndarray:
104+
for i in range(M):
105+
expr = poi.ExprBuilder()
106+
row = A[i]
107+
for coef, var in zip(row, x):
108+
expr.add_affine_term(var, coef)
109+
con = model.add_linear_constraint(expr, sense, b[i])
110+
constraints.append(con)
111+
elif is_sparse:
112+
for (row_indices, row_data), rhs in zip(iterate_sparse_matrix_rows(A), b):
113+
expr = poi.ExprBuilder()
114+
for j, coef in zip(row_indices, row_data):
115+
expr.add_affine_term(x[j], coef)
116+
con = model.add_linear_constraint(expr, sense, rhs)
117+
constraints.append(con)
118+
119+
return constraints
120+
121+
122+
def main():
123+
model = gurobi.Model()
124+
N = 200
125+
x = model.add_variables(range(N))
126+
A = np.eye(N)
127+
ub = 3.0
128+
lb = 1.0
129+
A_sparse = eye_array(N)
130+
add_matrix_constraints(model, A, x, poi.Leq, ub)
131+
add_matrix_constraints(model, A_sparse, x, poi.Geq, lb)
132+
133+
obj = poi.quicksum(x)
134+
model.set_objective(obj)
135+
model.optimize()
136+
137+
obj_value = model.get_model_attribute(poi.ModelAttribute.ObjectiveValue)
138+
print("Objective value: ", obj_value)
139+
140+
141+
if __name__ == "__main__":
142+
main()
143+
```

0 commit comments

Comments
 (0)