Skip to content

Commit a3ec6d7

Browse files
committed
Add documentation for new features
1 parent 101cb4a commit a3ec6d7

File tree

8 files changed

+180
-119
lines changed

8 files changed

+180
-119
lines changed

docs/source/common_model_interface.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ continuous
3939
:return: the handle of the variable
4040
```
4141

42-
### Add multi-dimensional variables to the model as <project:#pyoptinterface.tupledict>
42+
### Add multidimensional variables to the model as <project:#pyoptinterface.tupledict>
4343

4444
```{py:function} model.add_variables(*coords, [lb=-inf, ub=+inf, domain=pyoptinterface.VariableDomain.Continuous, name=""])
4545
46-
add a multi-dimensional variable to the model
46+
add a multidimensional variable to the model
4747
4848
:param coords: the coordinates of the variable, can be a list of Iterables
4949
:param float lb: the lower bound of the variable, optional, defaults to $-\infty$
@@ -55,6 +55,22 @@ continuous
5555
:rtype: pyoptinterface.tupledict
5656
```
5757

58+
### Add multidimensional variables to the model as `numpy.ndarray`
59+
60+
```{py:function} model.add_m_variables(shape, [lb=-inf, ub=+inf, domain=pyoptinterface.VariableDomain.Continuous, name=""])
61+
62+
add a multidimensional variable to the model as `numpy.ndarray`
63+
64+
:param shape: the shape of the variable, can be a tuple of integers or an integer
65+
:param float lb: the lower bound of the variable, optional, defaults to $-\infty$
66+
:param float ub: the upper bound of the variable, optional, defaults to $+\infty$
67+
:param pyoptinterface.VariableDomain domain: the domain of the variable, optional, defaults to
68+
continuous
69+
:param str name: the name of the variable, optional
70+
:return: the multidimensional variable
71+
:rtype: numpy.ndarray
72+
```
73+
5874
### Get/set variable attributes
5975

6076
```{py:function} model.set_variable_attribute(var, attr, value)
@@ -137,6 +153,20 @@ pretty print an expression in a human-readable format
137153
- <project:#model.add_second_order_cone_constraint>
138154
- <project:#model.add_sos_constraint>
139155

156+
### Add linear constraints as matrix form to the model
157+
158+
```{py:function} model.add_m_linear_constraints(A, vars, sense, b, [name=""])
159+
160+
add linear constraints as matrix form to the model $Ax \le b$ or $Ax = b$ or $Ax \ge b$
161+
162+
:param A: the matrix of coefficients, can be a dense `numpy.ndarray` or a sparse matrix `scipy.sparse.sparray`
163+
:param vars: the variables in the constraints, can be a list or a 1-d `numpy.ndarray` returned by `add_m_variables`
164+
:param pyoptinterface.ConstraintSense sense: the sense of the constraints
165+
:param b: the right-hand side of the constraints, should be a 1-d `numpy.ndarray`
166+
:param str name: the name of the constraints, optional
167+
:return: the handles of linear constraints
168+
:rtype: numpy.ndarray
169+
```
140170

141171
### Get/set constraint attributes
142172

docs/source/constraint.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ from pyoptinterface import copt
2525
model = copt.Model()
2626
```
2727

28+
## Constraint Sense
29+
30+
The sense of a constraint can be one of the following:
31+
32+
- `poi.Eq`: equal
33+
- `poi.Leq`: less than or equal
34+
- `poi.Geq`: greater than or equal
35+
36+
They are the abbreviations of `poi.ConstraintSense.Equal`, `poi.ConstraintSense.LessEqual` or `poi.ConstraintSense.GreaterEqual` and can be used in the `sense` argument of the constraint creation functions.
37+
2838
## Linear Constraint
2939
It is defined as:
3040

@@ -42,16 +52,15 @@ It can be added to the model using the `add_linear_constraint` method of the `Mo
4252
x = model.add_variable(name="x")
4353
y = model.add_variable(name="y")
4454
45-
con = model.add_linear_constraint(2.0*x + 3.0*y, poi.ConstraintSense.LessEqual, 1.0)
55+
con = model.add_linear_constraint(2.0*x + 3.0*y, poi.Leq, 1.0)
4656
```
4757

4858
```{py:function} model.add_linear_constraint(expr, sense, rhs, [name=""])
4959
5060
add a linear constraint to the model
5161
5262
:param expr: the expression of the constraint
53-
:param pyoptinterface.ConstraintSense sense: the sense
54-
of the constraint, which can be `GreaterEqual`, `Equal`, or `LessEqual`
63+
:param pyoptinterface.ConstraintSense sense: the sense of the constraint
5564
:param float rhs: the right-hand side of the constraint
5665
:param str name: the name of the constraint, optional
5766
:return: the handle of the constraint
@@ -88,8 +97,7 @@ con = model.add_quadratic_constraint(expr, poi.ConstraintSense.LessEqual, 1.0)
8897
add a quadratic constraint to the model
8998
9099
:param expr: the expression of the constraint
91-
:param pyoptinterface.ConstraintSense sense: the sense
92-
of the constraint, which can be `GreaterEqual`, `Equal`, or `LessEqual`
100+
:param pyoptinterface.ConstraintSense sense: the sense of the constraint, which can be `GreaterEqual`, `Equal`, or `LessEqual`
93101
:param float rhs: the right-hand side of the constraint
94102
:param str name: the name of the constraint, optional
95103
:return: the handle of the constraint
@@ -193,6 +201,8 @@ standard [constraint attributes](#pyoptinterface.ConstraintAttribute):
193201
- float
194202
* - Dual
195203
- float
204+
* - IIS
205+
- bool
196206
:::
197207

198208
The most common attribute we will use is the `Dual` attribute, which represents the dual multiplier of the constraint after optimization.
@@ -235,3 +245,26 @@ model.set_normalized_rhs(con, 2.0)
235245
# modify the coefficient of the linear part of the constraint
236246
model.set_normalized_coefficient(con, x, 2.0)
237247
```
248+
249+
## Create constraint with comparison operator
250+
251+
In other modeling languages, we can create a constraint with a comparison operator, like:
252+
253+
```python
254+
model.addConsr(x + y <= 1)
255+
```
256+
257+
This is quite convenient, so PyOptInterface now supports to create constraint with comparison operators `<=`, `==`, `>=` as a shortcut to create a linear or quadratic constraint.
258+
259+
```{code-cell}
260+
model.add_linear_constraint(x + y <= 1)
261+
model.add_linear_constraint(x <= y)
262+
model.add_quadratic_constraint(x*x + y*y <= 1)
263+
```
264+
265+
:::{note}
266+
267+
Creating constraint with comparison operator may cause performance issue especially the left-hand side and right-hand side of the constraint are complex expressions. PyOptInterface needs to create a new expression by subtracting the right-hand side from the left-hand side, which may be time-consuming.
268+
269+
If that becomes the bottleneck of performance, it is recommended to construct the left-hand side expression with `ExprBuilder` and call `add_linear_constraint` or `add_quadratic_constraint` method to create constraints explicitly.
270+
:::

docs/source/faq.md

Lines changed: 1 addition & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -34,110 +34,4 @@ model = gurobi.Model(env)
3434

3535
In YALMIP, you can use the matrix form $Ax \leq b$ to add linear constraints, which is quite convenient.
3636

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.ScalarAffineFunction()
106-
row = A[i]
107-
for coef, var in zip(row, x):
108-
expr.add_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.ScalarAffineFunction()
114-
for j, coef in zip(row_indices, row_data):
115-
expr.add_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-
```
37+
In PyOptInterface, you can use [`model.add_m_linear_constraints`](<project:#model.add_m_linear_constraints>) to add linear constraints in matrix form.

docs/source/getting_started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ con = model.add_linear_constraint(x1+x2, poi.ConstraintSense.Equal, 1, name="con
245245
```
246246
`model.add_linear_constraint` adds a linear constraint to the model.
247247
- The first argument `x1+x2` is the left-hand side of the constraint.
248-
- The second argument is the sense of the constraint. It can be `poi.ConstraintSense.Equal`, `poi.ConstraintSense.LessEqual` or `poi.ConstraintSense.GreaterEqual`.
248+
- The second argument is the sense of the constraint. It can be `poi.ConstraintSense.Equal`, `poi.ConstraintSense.LessEqual` or `poi.ConstraintSense.GreaterEqual` which can also be written as `poi.Eq`, `poi.Leq`, and `poi.Geq`.
249249
- The third argument is the right-hand side of the constraint. It must be a constant.
250250
- The fourth argument is optional and can be used to specify the name of the constraint.
251251

docs/source/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ container.md
2020
numpy.md
2121
structure.md
2222
common_model_interface.md
23+
infeasibility.md
2324
callback.md
2425
gurobi.md
2526
copt.md

docs/source/infeasibility.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
file_format: mystnb
3+
kernelspec:
4+
name: python3
5+
---
6+
7+
# Infeasibility Analysis
8+
9+
The optimization model is not ways feasible, and the optimizer may tell us some information about the infeasibility to diagnose the problem. There are two ways to handle the infeasibilities:
10+
11+
- Find the IIS (Irreducible Infeasible Set) to identify the minimal set of constraints that cause the infeasibility.
12+
- Relax the constraints and solve a weaker problem to find out which constraints are violated and how much.
13+
14+
PyOptInterface currently supports the first method to find the IIS (only with Gurobi and COPT). The following code snippet shows how to find the IIS of an infeasible model:
15+
16+
```{code-cell}
17+
import pyoptinterface as poi
18+
from pyoptinterface import copt
19+
20+
model = copt.Model()
21+
22+
x = model.add_variable(lb=0.0, name="x")
23+
y = model.add_variable(lb=0.0, name="y")
24+
25+
con1 = model.add_linear_constraint(x + y, poi.Geq, 5.0)
26+
con2 = model.add_linear_constraint(x + 2 * y, poi.Leq, 1.0)
27+
28+
model.set_objective(x)
29+
30+
model.computeIIS()
31+
32+
con1_iis = model.get_constraint_attribute(con1, poi.ConstraintAttribute.IIS)
33+
con2_iis = model.get_constraint_attribute(con2, poi.ConstraintAttribute.IIS)
34+
35+
print(f"Constraint 1 IIS: {con1_iis}")
36+
print(f"Constraint 2 IIS: {con2_iis}")
37+
```
38+
39+
This code snippet creates an infeasible model with two constraints and finds the IIS of the model. Obviously, the constraints are contradictory because `x + 2 * y <= 1` and `x + y >= 5` cannot be satisfied at the same time when `x` and `y` are non-negative. The optimizer will detect that the model is infeasible and return the IIS, which is the set of constraints that cause the infeasibility. We can query whether a constraint is in the IIS by calling `get_constraint_attribute` with the `ConstraintAttribute.IIS` attribute.
40+
41+
Sometimes, the bounds of the variables are not consistent with the constraints, and we need to query the IIS of the bounds of variables by calling `get_variable_attribute` with the `VariableAttribute.IISLowerBound` and `VariableAttribute.IISUpperBound` attributes.
42+
43+
The following code snippet shows how to tell if the bounds of a variable are in the IIS:
44+
45+
```{code-cell}
46+
model = copt.Model()
47+
48+
x = model.add_variable(lb=0.0, ub=2.0, name="x")
49+
y = model.add_variable(lb=0.0, ub=3.0, name="y")
50+
51+
con1 = model.add_linear_constraint(x + y, poi.Geq, 6.0)
52+
53+
model.set_objective(x)
54+
55+
model.computeIIS()
56+
57+
con1_iis = model.get_constraint_attribute(con1, poi.ConstraintAttribute.IIS)
58+
x_lb_iis = model.get_variable_attribute(x, poi.VariableAttribute.IISLowerBound)
59+
x_ub_iis = model.get_variable_attribute(x, poi.VariableAttribute.IISUpperBound)
60+
y_lb_iis = model.get_variable_attribute(y, poi.VariableAttribute.IISLowerBound)
61+
y_ub_iis = model.get_variable_attribute(y, poi.VariableAttribute.IISUpperBound)
62+
63+
print(f"Constraint 1 IIS: {con1_iis}")
64+
print(f"Variable x lower bound IIS: {x_lb_iis}")
65+
print(f"Variable x upper bound IIS: {x_ub_iis}")
66+
print(f"Variable y lower bound IIS: {y_lb_iis}")
67+
print(f"Variable y upper bound IIS: {y_ub_iis}")
68+
```

docs/source/numpy.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ kernelspec:
44
name: python3
55
---
66

7-
# Numpy Container and N-queens Problem
7+
# Matrix Modeling
88

9-
In the previous [container](container.md) section, we have introduced the `tupledict` container to store and manipulate multi-dimensional data.
9+
In the previous [container](container.md) section, we have introduced the `tupledict` container to store and manipulate multidimensional data.
1010

11-
However, due to the Bring Your Own Container (BYOC) principle, variables and constraints in PyOptInterface can just simple Python objects that can be stored in Numpy `ndarrays` directly as a multi-dimensional array, and you can enjoy the features of Numpy such like [fancy-indexing](https://numpy.org/doc/stable/user/basics.indexing.html) automatically.
11+
However, due to the Bring Your Own Container (BYOC) principle, variables and constraints in PyOptInterface can just simple Python objects that can be stored in Numpy `ndarray` directly as a multidimensional array, and you can enjoy the features of Numpy such like [fancy-indexing](https://numpy.org/doc/stable/user/basics.indexing.html) automatically.
1212

13-
We will use N-queens problem as example to show how to use Numpy `ndarrays` as container to store 2-dimensional variables and construct optimization model.
13+
## N-queen problem
14+
15+
We will use N-queens problem as example to show how to use Numpy `ndarray` as container to store 2-dimensional variables and construct optimization model.
1416

1517
Firstly, we import the necessary modules:
1618

@@ -63,3 +65,32 @@ x_value = get_v(x)
6365
6466
print(x_value.astype(int))
6567
```
68+
69+
## Built-in functions to add variables and constraints as Numpy `ndarray`
70+
71+
Although you can construct the `ndarray` of variables and constraints manually, PyOptInterface provides built-in functions to simplify the process. The following code snippet shows how to use the built-in functions to add variables and constraints as Numpy `ndarray`:
72+
73+
```{code-cell}
74+
model = highs.Model()
75+
76+
x = model.add_m_variables(N)
77+
78+
A = np.eye(N)
79+
b_ub = np.ones(N)
80+
b_lb = np.ones(N)
81+
82+
model.add_m_linear_constraints(A, x, poi.Leq, b_ub)
83+
model.add_m_linear_constraints(A, x, poi.Geq, b_lb)
84+
85+
model.set_objective(poi.quicksum(x))
86+
87+
model.optimize()
88+
```
89+
90+
Here we use two built-in functions `add_m_variables` and `add_m_linear_constraints` to add variables and constraints as Numpy `ndarray` respectively.
91+
92+
The reference of these functions are listed in <project:#model.add_m_variables> and <project:#model.add_m_linear_constraints>.
93+
94+
`add_m_variables` returns a `ndarray` of variables with the specified shape.
95+
96+
`add_m_linear_constraints` adds multiple linear constraints to the model at once formulated as $Ax \le b$ or $Ax = b$ or $Ax \ge b$ where the matrix $A$ can be a dense `numpy.ndarray` or a sparse matrix `scipy.sparse.sparray`.

0 commit comments

Comments
 (0)