Skip to content

Commit c0a03cf

Browse files
authored
Merge pull request #508 from steffanschlein/add-conss-list
Add convenience function to add multiple constraints at once
2 parents 2eba0f4 + a13a5f1 commit c0a03cf

File tree

3 files changed

+155
-2
lines changed

3 files changed

+155
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44
### Added
5+
- add convenience function `Model.addConss()` to add multiple constraints at once
56
### Fixed
67
### Changed
78
### Removed

src/pyscipopt/scip.pyx

+71-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ from cpython.pycapsule cimport PyCapsule_New, PyCapsule_IsValid, PyCapsule_GetPo
1212
from libc.stdlib cimport malloc, free
1313
from libc.stdio cimport fdopen
1414

15+
from collections.abc import Iterable
16+
from itertools import repeat
17+
1518
include "expr.pxi"
1619
include "lp.pxi"
1720
include "benders.pxi"
@@ -1979,18 +1982,19 @@ cdef class Model:
19791982
stickingatnode=False):
19801983
"""Add a linear or quadratic constraint.
19811984
1982-
:param cons: list of coefficients
1985+
:param cons: constraint object
19831986
:param name: the name of the constraint, generic name if empty (Default value = '')
19841987
:param initial: should the LP relaxation of constraint be in the initial LP? (Default value = True)
19851988
:param separate: should the constraint be separated during LP processing? (Default value = True)
19861989
:param enforce: should the constraint be enforced during node processing? (Default value = True)
1987-
:param check: should the constraint be checked during for feasibility? (Default value = True)
1990+
:param check: should the constraint be checked for feasibility? (Default value = True)
19881991
:param propagate: should the constraint be propagated during node processing? (Default value = True)
19891992
:param local: is the constraint only valid locally? (Default value = False)
19901993
:param modifiable: is the constraint modifiable (subject to column generation)? (Default value = False)
19911994
:param dynamic: is the constraint subject to aging? (Default value = False)
19921995
:param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False)
19931996
:param stickingatnode: should the constraint always be kept at the node where it was added, even if it may be moved to a more global node? (Default value = False)
1997+
:return The added @ref scip#Constraint "Constraint" object.
19941998
19951999
"""
19962000
assert isinstance(cons, ExprCons), "given constraint is not ExprCons but %s" % cons.__class__.__name__
@@ -2018,6 +2022,71 @@ cdef class Model:
20182022
else:
20192023
return self._addNonlinearCons(cons, **kwargs)
20202024

2025+
def addConss(self, conss, name='', initial=True, separate=True,
2026+
enforce=True, check=True, propagate=True, local=False,
2027+
modifiable=False, dynamic=False, removable=False,
2028+
stickingatnode=False):
2029+
"""Adds multiple linear or quadratic constraints.
2030+
2031+
Each of the constraints is added to the model using Model.addCons().
2032+
2033+
For all parameters, except @p conss, this method behaves differently depending on the type of the passed argument:
2034+
1. If the value is iterable, it must be of the same length as @p conss. For each constraint, Model.addCons() will be called with the value at the corresponding index.
2035+
2. Else, the (default) value will be applied to all of the constraints.
2036+
2037+
:param conss An iterable of constraint objects. Any iterable will be converted into a list before further processing.
2038+
:param name: the names of the constraints, generic name if empty (Default value = ''). If a single string is passed, it will be suffixed by an underscore and the enumerated index of the constraint (starting with 0).
2039+
:param initial: should the LP relaxation of constraints be in the initial LP? (Default value = True)
2040+
:param separate: should the constraints be separated during LP processing? (Default value = True)
2041+
:param enforce: should the constraints be enforced during node processing? (Default value = True)
2042+
:param check: should the constraints be checked for feasibility? (Default value = True)
2043+
:param propagate: should the constraints be propagated during node processing? (Default value = True)
2044+
:param local: are the constraints only valid locally? (Default value = False)
2045+
:param modifiable: are the constraints modifiable (subject to column generation)? (Default value = False)
2046+
:param dynamic: are the constraints subject to aging? (Default value = False)
2047+
:param removable: should the relaxation be removed from the LP due to aging or cleanup? (Default value = False)
2048+
:param stickingatnode: should the constraints always be kept at the node where it was added, even if it may be @oved to a more global node? (Default value = False)
2049+
:return A list of added @ref scip#Constraint "Constraint" objects.
2050+
2051+
:see addCons()
2052+
"""
2053+
def ensure_iterable(elem, length):
2054+
if isinstance(elem, Iterable):
2055+
return elem
2056+
else:
2057+
return list(repeat(elem, length))
2058+
2059+
assert isinstance(conss, Iterable), "Given constraint list is not iterable."
2060+
2061+
conss = list(conss)
2062+
n_conss = len(conss)
2063+
2064+
if isinstance(name, str):
2065+
if name == "":
2066+
name = ["" for idx in range(n_conss)]
2067+
else:
2068+
name = ["%s_%s" % (name, idx) for idx in range(n_conss)]
2069+
initial = ensure_iterable(initial, n_conss)
2070+
separate = ensure_iterable(separate, n_conss)
2071+
enforce = ensure_iterable(enforce, n_conss)
2072+
check = ensure_iterable(check, n_conss)
2073+
propagate = ensure_iterable(propagate, n_conss)
2074+
local = ensure_iterable(local, n_conss)
2075+
modifiable = ensure_iterable(modifiable, n_conss)
2076+
dynamic = ensure_iterable(dynamic, n_conss)
2077+
removable = ensure_iterable(removable, n_conss)
2078+
stickingatnode = ensure_iterable(stickingatnode, n_conss)
2079+
2080+
constraints = []
2081+
for i, cons in enumerate(conss):
2082+
constraints.append(
2083+
self.addCons(cons, name[i], initial[i], separate[i], enforce[i],
2084+
check[i], propagate[i], local[i], modifiable[i],
2085+
dynamic[i], removable[i], stickingatnode[i])
2086+
)
2087+
2088+
return constraints
2089+
20212090
def _addLinCons(self, ExprCons lincons, **kwargs):
20222091
assert isinstance(lincons, ExprCons), "given constraint is not ExprCons but %s" % lincons.__class__.__name__
20232092

tests/test_model.py

+83
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,89 @@ def test_model():
7979
assert s.getStatus() == 'unbounded'
8080

8181

82+
def test_multiple_cons_simple():
83+
def assert_conss_eq(a, b):
84+
assert a.name == b.name
85+
assert a.isInitial() == b.isInitial()
86+
assert a.isSeparated() == b.isSeparated()
87+
assert a.isEnforced() == b.isEnforced()
88+
assert a.isChecked() == b.isChecked()
89+
assert a.isPropagated() == b.isPropagated()
90+
assert a.isLocal() == b.isLocal()
91+
assert a.isModifiable() == b.isModifiable()
92+
assert a.isDynamic() == b.isDynamic()
93+
assert a.isRemovable() == b.isRemovable()
94+
assert a.isStickingAtNode() == b.isStickingAtNode()
95+
96+
s = Model()
97+
s_x = s.addVar("x", vtype = 'C', obj = 1.0)
98+
s_y = s.addVar("y", vtype = 'C', obj = 2.0)
99+
s_cons = s.addCons(s_x + 2 * s_y <= 1.0)
100+
101+
m = Model()
102+
m_x = m.addVar("x", vtype = 'C', obj = 1.0)
103+
m_y = m.addVar("y", vtype = 'C', obj = 2.0)
104+
m_conss = m.addConss([m_x + 2 * m_y <= 1.0])
105+
106+
assert len(m_conss) == 1
107+
assert_conss_eq(s_cons, m_conss[0])
108+
109+
s.freeProb()
110+
m.freeProb()
111+
112+
113+
def test_multiple_cons_names():
114+
m = Model()
115+
x = m.addVar("x", vtype = 'C', obj = 1.0)
116+
y = m.addVar("y", vtype = 'C', obj = 2.0)
117+
118+
names = list("abcdef")
119+
conss = m.addConss([x + 2 * y <= 1 for i in range(len(names))], names)
120+
121+
assert len(conss) == len(names)
122+
assert all([c.name == n for c, n in zip(conss, names)])
123+
124+
m.freeProb()
125+
126+
m = Model()
127+
x = m.addVar("x", vtype = 'C', obj = 1.0)
128+
y = m.addVar("y", vtype = 'C', obj = 2.0)
129+
130+
name = "abcdef"
131+
conss = m.addConss([x + 2 * y <= 1 for i in range(5)], name)
132+
133+
assert len(conss) == 5
134+
assert all([c.name.startswith(name + "_") for c in conss])
135+
136+
137+
def test_multiple_cons_params():
138+
"""Test if setting the remaining parameters works as expected"""
139+
def assert_conss_neq(a, b):
140+
assert a.isInitial() != b.isInitial()
141+
assert a.isSeparated() != b.isSeparated()
142+
assert a.isEnforced() != b.isEnforced()
143+
assert a.isChecked() != b.isChecked()
144+
assert a.isPropagated() != b.isPropagated()
145+
assert a.isModifiable() != b.isModifiable()
146+
assert a.isDynamic() != b.isDynamic()
147+
assert a.isRemovable() != b.isRemovable()
148+
assert a.isStickingAtNode() != b.isStickingAtNode()
149+
150+
kwargs = dict(initial=True, separate=True,
151+
enforce=True, check=True, propagate=True, local=False,
152+
modifiable=False, dynamic=False, removable=False,
153+
stickingatnode=False)
154+
155+
m = Model()
156+
x = m.addVar("x", vtype = 'C', obj = 1.0)
157+
y = m.addVar("y", vtype = 'C', obj = 2.0)
158+
159+
conss = m.addConss([x + 2 * y <= 1], **kwargs)
160+
conss += m.addConss([x + 2 * y <= 1], **{k: not v for k, v in kwargs.items()})
161+
162+
assert_conss_neq(conss[0], conss[1])
163+
164+
82165
def test_model_ptr():
83166
model1 = Model()
84167
ptr1 = model1.to_ptr(give_ownership=True)

0 commit comments

Comments
 (0)