@@ -29,3 +29,115 @@ env.start()
2929
3030model = 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