Skip to content

Commit 5229e68

Browse files
committed
Add numpy-style add variables and constraints
1 parent 1d29d43 commit 5229e68

File tree

10 files changed

+189
-22
lines changed

10 files changed

+189
-22
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ classifiers = [
2020
Homepage = "https://github.com/metab0t/pyoptinterface"
2121

2222
[project.optional-dependencies]
23-
test = ["pytest", "numpy"]
23+
matrix = ["numpy", "scipy"]
24+
test = ["pytest", "numpy", "scipy"]
2425
highs = ["highsbox"]
2526
nlp = ["llvmlite", "tccbox"]
2627

src/pyoptinterface/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
make_tupledict,
2525
)
2626

27-
from pyoptinterface._src.aml import make_nd_variable, quicksum, quicksum_
27+
from pyoptinterface._src.aml import quicksum, quicksum_
2828

2929
# Alias of ConstraintSense
3030
Eq = ConstraintSense.Equal
@@ -58,7 +58,6 @@
5858
"ConstraintAttribute",
5959
"tupledict",
6060
"make_tupledict",
61-
"make_nd_variable",
6261
"quicksum",
6362
"quicksum_",
6463
"Eq",

src/pyoptinterface/_src/aml.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,39 @@
22
from .tupledict import make_tupledict
33

44
from collections.abc import Collection
5+
from typing import Tuple, Union
56

67

7-
def make_nd_variable(
8+
def make_variable_ndarray(
9+
model, shape: Union[Tuple[int, ...], int], domain=None, lb=None, ub=None, name=None, start=None
10+
):
11+
import numpy as np
12+
13+
if isinstance(shape, int):
14+
shape = (shape,)
15+
16+
variables = np.empty(shape, dtype=object)
17+
18+
kw_args = dict()
19+
if domain is not None:
20+
kw_args["domain"] = domain
21+
if lb is not None:
22+
kw_args["lb"] = lb
23+
if ub is not None:
24+
kw_args["ub"] = ub
25+
if start is not None:
26+
kw_args["start"] = start
27+
28+
for index in np.ndindex(shape):
29+
if name is not None:
30+
suffix = str(index)
31+
kw_args["name"] = f"{name}{suffix}"
32+
variables[index] = model.add_variable(**kw_args)
33+
34+
return variables
35+
36+
37+
def make_variable_tupledict(
838
model, *coords: Collection, domain=None, lb=None, ub=None, name=None, start=None
939
):
1040
kw_args = dict()

src/pyoptinterface/_src/copt.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
_direct_get_entity_attribute,
2121
_direct_set_entity_attribute,
2222
)
23-
from .aml import make_nd_variable
23+
from .aml import make_variable_tupledict, make_variable_ndarray
24+
from .matrix import add_matrix_constraints
2425

2526

2627
def detected_libraries():
@@ -355,7 +356,8 @@ def __init__(self, env=None):
355356
self._env = env
356357
self.mip_start_values: Dict[VariableIndex, float] = dict()
357358

358-
self.add_variables = types.MethodType(make_nd_variable, self)
359+
def add_variables(self, *args, **kwargs):
360+
return make_variable_tupledict(self, *args, **kwargs)
359361

360362
@staticmethod
361363
def supports_variable_attribute(attribute: VariableAttribute, settable=False):
@@ -522,3 +524,8 @@ def cb_get_info(self, what):
522524
return self.cb_get_info_double(what)
523525
else:
524526
raise ValueError(f"Unknown callback info type: {what}")
527+
528+
529+
Model.add_variables = make_variable_tupledict
530+
Model.add_m_variables = make_variable_ndarray
531+
Model.add_m_linear_constraints = add_matrix_constraints

src/pyoptinterface/_src/gurobi.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# only on windows
33
import os
44
import platform
5-
import types
65
from pathlib import Path
76
import re
87
import logging
@@ -25,7 +24,8 @@
2524
_direct_set_entity_attribute,
2625
)
2726
from .constraint_bridge import bridge_soc_quadratic_constraint
28-
from .aml import make_nd_variable
27+
from .aml import make_variable_tupledict, make_variable_ndarray
28+
from .matrix import add_matrix_constraints
2929

3030

3131
def detected_libraries():
@@ -510,12 +510,6 @@ def __init__(self, env=None):
510510
# We must keep a reference to the environment to prevent it from being garbage collected
511511
self._env = env
512512

513-
self.add_second_order_cone_constraint = types.MethodType(
514-
bridge_soc_quadratic_constraint, self
515-
)
516-
517-
self.add_variables = types.MethodType(make_nd_variable, self)
518-
519513
@staticmethod
520514
def supports_variable_attribute(attribute: VariableAttribute, settable=False):
521515
if settable:
@@ -722,3 +716,9 @@ def cb_get_info(self, what):
722716
return self.cb_get_info_double(what)
723717
else:
724718
raise ValueError(f"Unknown callback info type: {what}")
719+
720+
721+
Model.add_variables = make_variable_tupledict
722+
Model.add_m_variables = make_variable_ndarray
723+
Model.add_m_linear_constraints = add_matrix_constraints
724+
Model.add_second_order_cone_constraint = bridge_soc_quadratic_constraint

src/pyoptinterface/_src/highs.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
_direct_get_entity_attribute,
2626
_direct_set_entity_attribute,
2727
)
28-
from .aml import make_nd_variable
28+
from .aml import make_variable_tupledict, make_variable_ndarray
29+
from .matrix import add_matrix_constraints
2930

3031

3132
def detected_libraries():
@@ -288,8 +289,6 @@ def __init__(self):
288289

289290
self.mip_start_values: Dict[VariableIndex, float] = dict()
290291

291-
self.add_variables = types.MethodType(make_nd_variable, self)
292-
293292
@staticmethod
294293
def supports_variable_attribute(attribute: VariableAttribute, settable=False):
295294
if settable:
@@ -437,3 +436,8 @@ def optimize(self):
437436
self.set_primal_start(variables, values)
438437
mip_start.clear()
439438
super().optimize()
439+
440+
441+
Model.add_variables = make_variable_tupledict
442+
Model.add_m_variables = make_variable_ndarray
443+
Model.add_m_linear_constraints = add_matrix_constraints

src/pyoptinterface/_src/ipopt.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
_direct_get_entity_attribute,
4040
_direct_set_entity_attribute,
4141
)
42-
from .aml import make_nd_variable
42+
from .aml import make_variable_tupledict, make_variable_ndarray
43+
from .matrix import add_matrix_constraints
4344

4445

4546
def detected_libraries():
@@ -451,7 +452,6 @@ def __init__(self, jit: str = "LLVM"):
451452
else:
452453
raise ValueError(f"JIT engine can only be 'C' or 'LLVM', got {jit}")
453454
self.jit = jit
454-
self.add_variables = types.MethodType(make_nd_variable, self)
455455

456456
self.function_indices: Set[FunctionIndex] = set()
457457
self.function_cppad_autodiff_graphs: Dict[FunctionIndex, CppADAutodiffGraph] = (
@@ -727,3 +727,8 @@ def set_raw_parameter(self, param_name: str, value):
727727
self.set_raw_option_string(param_name, value)
728728
else:
729729
raise ValueError(f"Unsupported parameter type: {ty}")
730+
731+
732+
Model.add_variables = make_variable_tupledict
733+
Model.add_m_variables = make_variable_ndarray
734+
Model.add_m_linear_constraints = add_matrix_constraints

src/pyoptinterface/_src/matrix.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from .tupledict import tupledict
2+
from .core_ext import ScalarAffineFunction
3+
4+
5+
def iterate_sparse_matrix_rows(A):
6+
"""
7+
Iterate over rows of a sparse matrix and get non-zero elements for each row.
8+
9+
A is a 2-dimensional scipy sparse matrix
10+
isinstance(A, scipy.sparse.sparray) = True and A.ndim = 2
11+
"""
12+
from scipy.sparse import csr_array
13+
14+
if not isinstance(A, csr_array):
15+
A = csr_array(A) # Convert to CSR format if not already
16+
17+
for i in range(A.shape[0]):
18+
row_start = A.indptr[i]
19+
row_end = A.indptr[i + 1]
20+
row_indices = A.indices[row_start:row_end]
21+
row_data = A.data[row_start:row_end]
22+
yield row_indices, row_data
23+
24+
25+
def add_matrix_constraints(model, A, x, sense, b):
26+
"""
27+
add constraints Ax <= / = / >= b
28+
29+
A is a 2-dimensional numpy array or scipy sparse matrix
30+
x is an iterable of variables
31+
sense is one of (poi.Leq, poi.Eq, poi.Geq)
32+
b is an iterable of values or a single scalar
33+
"""
34+
import numpy as np
35+
from scipy.sparse import sparray
36+
37+
is_ndarray = isinstance(A, np.ndarray)
38+
is_sparse = isinstance(A, sparray)
39+
40+
if not is_ndarray and not is_sparse:
41+
raise ValueError("A must be a numpy array or scipy.sparse array")
42+
43+
ndim = A.ndim
44+
if ndim != 2:
45+
raise ValueError("A must be a 2-dimensional array")
46+
47+
M, N = A.shape
48+
49+
# turn x into a list if x is an iterable
50+
if isinstance(x, np.ndarray):
51+
xdim = x.ndim
52+
if xdim != 1:
53+
raise ValueError("x must be a 1-dimensional array")
54+
elif isinstance(x, tupledict):
55+
x = list(x.values())
56+
else:
57+
x = list(x)
58+
59+
if len(x) != N:
60+
raise ValueError("x must have length equal to the number of columns of A")
61+
62+
# check b
63+
if np.isscalar(b):
64+
b = np.full(M, b)
65+
elif len(b) != M:
66+
raise ValueError("b must have length equal to the number of rows of A")
67+
68+
constraints = np.empty(M, dtype=object)
69+
70+
if is_ndarray:
71+
for i in range(M):
72+
expr = ScalarAffineFunction()
73+
row = A[i]
74+
for coef, var in zip(row, x):
75+
expr.add_term(var, coef)
76+
con = model.add_linear_constraint(expr, sense, b[i])
77+
constraints[i] = con
78+
elif is_sparse:
79+
for i, (row_indices, row_data), rhs in zip(
80+
range(M), iterate_sparse_matrix_rows(A), b
81+
):
82+
expr = ScalarAffineFunction()
83+
for j, coef in zip(row_indices, row_data):
84+
expr.add_term(x[j], coef)
85+
con = model.add_linear_constraint(expr, sense, rhs)
86+
constraints[i] = con
87+
88+
return constraints

src/pyoptinterface/_src/mosek.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
_direct_get_entity_attribute,
2222
_direct_set_entity_attribute,
2323
)
24-
from .aml import make_nd_variable
24+
from .aml import make_variable_tupledict, make_variable_ndarray
25+
from .matrix import add_matrix_constraints
2526

2627

2728
def detected_libraries():
@@ -357,8 +358,6 @@ def __init__(self, env=None):
357358
self.last_solve_return_code: Optional[int] = None
358359
self.silent = True
359360

360-
self.add_variables = types.MethodType(make_nd_variable, self)
361-
362361
@staticmethod
363362
def supports_variable_attribute(attribute: VariableAttribute, settable=False):
364363
if settable:
@@ -498,3 +497,8 @@ def get_raw_information(self, param_name: str):
498497
def optimize(self):
499498
ret = super().optimize()
500499
self.last_solve_return_code = ret
500+
501+
502+
Model.add_variables = make_variable_tupledict
503+
Model.add_m_variables = make_variable_ndarray
504+
Model.add_m_linear_constraints = add_matrix_constraints

tests/test_matrix_api.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pyoptinterface as poi
2+
import numpy as np
3+
from scipy.sparse import eye_array
4+
from pytest import approx
5+
6+
7+
def test_matrix_api(model_interface):
8+
model = model_interface
9+
10+
N = 10
11+
x = model.add_m_variables(N, lb=0.0)
12+
A = np.eye(N)
13+
ub = 3.0
14+
lb = 1.0
15+
A_sparse = eye_array(N)
16+
model.add_m_linear_constraints(A, x, poi.Leq, ub)
17+
model.add_m_linear_constraints(A_sparse, x, poi.Geq, lb)
18+
19+
obj = poi.quicksum(x)
20+
21+
model.set_objective(obj)
22+
model.optimize()
23+
obj_value = model.get_model_attribute(poi.ModelAttribute.ObjectiveValue)
24+
assert obj_value == approx(N * lb)
25+
26+
model.set_objective(-obj)
27+
model.optimize()
28+
obj_value = model.get_model_attribute(poi.ModelAttribute.ObjectiveValue)
29+
assert obj_value == approx(-N * ub)

0 commit comments

Comments
 (0)