Skip to content

Commit dcd64b9

Browse files
authored
Merge branch 'master' into ExprLike
2 parents 61cea57 + 486a821 commit dcd64b9

4 files changed

Lines changed: 169 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
- `Expr` and `GenExpr` support NumPy binary functions (`np.add`, `np.subtract`, `np.multiply`, `np.divide`, `np.true_divide`, `np.power`, `np.less_equal`, `np.greater_equal`, `np.equal`)
2323
- Added `getBase()` and `setBase()` methods to `LP` class for getting/setting basis status
2424
- Added `getMemUsed()`, `getMemTotal()`, and `getMemExternEstim()` methods
25+
- Added `addMatrixConsDisjunction()` for elementwise disjunctions over matrix constraint expressions (`MatrixExprCons`/`ExprCons`) (#1084)
2526
- Added `isReoptEnabled()` and raising error if not enabled upon calling `reoptSolve()`
2627
- SOS1/SOS2 constraints are now realease after addition similar to the other constraint types
2728
### Fixed

src/pyscipopt/scip.pxi

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6643,6 +6643,123 @@ cdef class Model:
66436643
PY_SCIP_CALL(SCIPreleaseCons(self._scip, &(<Constraint>cons).scip_cons))
66446644
return disj_cons
66456645

6646+
def addMatrixConsDisjunction(self, conss: Iterable,
6647+
name: Union[str, np.ndarray] = '', initial: Union[bool, np.ndarray] = True,
6648+
relaxcons=None, enforce: Union[bool, np.ndarray] = True,
6649+
check: Union[bool, np.ndarray] = True, local: Union[bool, np.ndarray] = False,
6650+
modifiable: Union[bool, np.ndarray] = False,
6651+
dynamic: Union[bool, np.ndarray] = False):
6652+
"""
6653+
Add an elementwise disjunction of matrix constraint expressions.
6654+
6655+
Given an iterable of ``MatrixExprCons`` with a common shape, creates one
6656+
disjunction per index: for each position ``idx``, the resulting
6657+
disjunction enforces that at least one of ``conss[k][idx]`` holds.
6658+
``ExprCons`` entries are broadcast to every position. If every entry in
6659+
``conss`` is a plain ``ExprCons``, the call is forwarded to
6660+
:meth:`addConsDisjunction` and returns a single ``Constraint``.
6661+
6662+
Note: this method accepts constraint *expressions* (e.g., ``A @ x <= b``),
6663+
not ``MatrixConstraint``/``Constraint`` objects returned by
6664+
:meth:`addMatrixCons`/:meth:`addCons`.
6665+
6666+
Parameters
6667+
----------
6668+
conss : iterable of MatrixExprCons or ExprCons
6669+
Constraint expressions to combine elementwise into disjunctions. All
6670+
``MatrixExprCons`` entries must share the same shape.
6671+
name : str or np.ndarray, optional
6672+
Name of the disjunction constraints. (Default value = "")
6673+
initial : bool or np.ndarray, optional
6674+
Should the LP relaxation be in the initial LP? (Default value = True)
6675+
relaxcons : None, optional
6676+
NOT YET SUPPORTED. (Default value = None)
6677+
enforce : bool or np.ndarray, optional
6678+
Should the constraint be enforced during node processing? (Default value = True)
6679+
check : bool or np.ndarray, optional
6680+
Should the constraint be checked for feasibility? (Default value = True)
6681+
local : bool or np.ndarray, optional
6682+
Is the constraint only valid locally? (Default value = False)
6683+
modifiable : bool or np.ndarray, optional
6684+
Is the constraint modifiable (subject to column generation)? (Default value = False)
6685+
dynamic : bool or np.ndarray, optional
6686+
Is the constraint subject to aging? (Default value = False)
6687+
6688+
Returns
6689+
-------
6690+
MatrixConstraint or Constraint
6691+
A ``MatrixConstraint`` of the common shape when any entry of
6692+
``conss`` is a ``MatrixExprCons``; otherwise a single
6693+
``Constraint`` from the scalar fallback.
6694+
"""
6695+
if not isinstance(conss, Iterable):
6696+
raise TypeError(f"given constraint list is not iterable, but got {type(conss).__name__}")
6697+
conss = list(conss)
6698+
6699+
for cons in conss:
6700+
if not isinstance(cons, (ExprCons, MatrixExprCons)):
6701+
raise TypeError(
6702+
"element of conss is not MatrixExprCons nor ExprCons but %s"
6703+
% cons.__class__.__name__
6704+
)
6705+
6706+
if all(isinstance(cons, ExprCons) for cons in conss):
6707+
return self.addConsDisjunction(
6708+
conss, name=name, initial=initial, relaxcons=relaxcons,
6709+
enforce=enforce, check=check, local=local,
6710+
modifiable=modifiable, dynamic=dynamic,
6711+
)
6712+
6713+
shape = None
6714+
for cons in conss:
6715+
if isinstance(cons, MatrixExprCons):
6716+
if shape is None:
6717+
shape = cons.shape
6718+
elif cons.shape != shape:
6719+
raise ValueError(
6720+
"All MatrixExprCons in conss must have the same shape, "
6721+
"got %s and %s" % (shape, cons.shape)
6722+
)
6723+
6724+
for arr in (name, initial, enforce, check, local, modifiable, dynamic):
6725+
if isinstance(arr, np.ndarray):
6726+
assert arr.shape == shape
6727+
6728+
if isinstance(name, str):
6729+
matrix_names = np.full(shape, name, dtype=object)
6730+
if name != "":
6731+
for idx in np.ndindex(shape):
6732+
matrix_names[idx] = f"{name}_{'_'.join(map(str, idx))}"
6733+
else:
6734+
matrix_names = name
6735+
6736+
matrix_initial = initial if isinstance(initial, np.ndarray) else np.full(shape, initial, dtype=bool)
6737+
matrix_enforce = enforce if isinstance(enforce, np.ndarray) else np.full(shape, enforce, dtype=bool)
6738+
matrix_check = check if isinstance(check, np.ndarray) else np.full(shape, check, dtype=bool)
6739+
matrix_local = local if isinstance(local, np.ndarray) else np.full(shape, local, dtype=bool)
6740+
matrix_modifiable = modifiable if isinstance(modifiable, np.ndarray) else np.full(shape, modifiable, dtype=bool)
6741+
matrix_dynamic = dynamic if isinstance(dynamic, np.ndarray) else np.full(shape, dynamic, dtype=bool)
6742+
6743+
matrix_cons = np.empty(shape, dtype=object)
6744+
for idx in np.ndindex(shape):
6745+
elem_conss = [
6746+
cons[idx] if isinstance(cons, MatrixExprCons) else cons
6747+
for cons in conss
6748+
]
6749+
matrix_cons[idx] = self.addConsDisjunction(
6750+
elem_conss,
6751+
name=matrix_names[idx],
6752+
initial=matrix_initial[idx],
6753+
relaxcons=relaxcons,
6754+
enforce=matrix_enforce[idx],
6755+
check=matrix_check[idx],
6756+
local=matrix_local[idx],
6757+
modifiable=matrix_modifiable[idx],
6758+
dynamic=matrix_dynamic[idx],
6759+
)
6760+
6761+
return matrix_cons.view(MatrixConstraint)
6762+
66466763
def getConsNVars(self, Constraint constraint):
66476764
"""
66486765
Gets number of variables in a constraint.

src/pyscipopt/scip.pyi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,18 @@ class Model:
816816
removable: Incomplete = ...,
817817
stickingatnode: Incomplete = ...,
818818
) -> Incomplete: ...
819+
def addMatrixConsDisjunction(
820+
self,
821+
conss: Incomplete,
822+
name: Incomplete = ...,
823+
initial: Incomplete = ...,
824+
relaxcons: Incomplete = ...,
825+
enforce: Incomplete = ...,
826+
check: Incomplete = ...,
827+
local: Incomplete = ...,
828+
modifiable: Incomplete = ...,
829+
dynamic: Incomplete = ...,
830+
) -> Incomplete: ...
819831
def addMatrixConsIndicator(
820832
self,
821833
cons: Incomplete,

tests/test_matrix_variable.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,45 @@ def test_matrix_cons_indicator():
549549
assert m.getVal(z) == 1
550550

551551

552+
def test_matrix_cons_disjunction():
553+
m = Model()
554+
x = m.addMatrixVar((2, 1), vtype="C", lb=-10, ub=2)
555+
y = m.addMatrixVar((2, 1), vtype="C", lb=-10, ub=5)
556+
557+
# shape mismatch between MatrixExprCons entries
558+
b = m.addMatrixVar((3, 1), vtype="C")
559+
with pytest.raises(Exception):
560+
m.addMatrixConsDisjunction([x <= 1, b <= 1])
561+
562+
# require MatrixExprCons or ExprCons in the list
563+
with pytest.raises(TypeError):
564+
m.addMatrixConsDisjunction([x])
565+
566+
# MatrixExprCons -> elementwise MatrixConstraint
567+
o = m.addMatrixVar((2, 1), vtype="C")
568+
m.addMatrixCons(o <= x + y)
569+
cons = m.addMatrixConsDisjunction([x <= 1, x <= 0, y <= 0])
570+
assert isinstance(cons, MatrixConstraint)
571+
assert cons.shape == (2, 1)
572+
573+
m.setObjective(o.sum(), "maximize")
574+
m.optimize()
575+
for i in range(2):
576+
# each row picks the (x<=1) option: x=1, y=5, o=6
577+
assert m.isEQ(m.getVal(o[i, 0]), 6)
578+
579+
# ExprCons-only fallback returns a scalar Constraint
580+
m2 = Model()
581+
p = m2.addVar(vtype="C", lb=-10, ub=2)
582+
q = m2.addVar(vtype="C", lb=-10, ub=5)
583+
o2 = m2.addVar(vtype="C")
584+
m2.addCons(o2 <= p + q)
585+
m2.addMatrixConsDisjunction([p <= 1, p <= 0, q <= 0])
586+
m2.setObjective(o2, "maximize")
587+
m2.optimize()
588+
assert m2.isEQ(m2.getVal(o2), 6)
589+
590+
552591
def test_matrix_compare_with_expr():
553592
m = Model()
554593
var = m.addVar(vtype="B", ub=0)

0 commit comments

Comments
 (0)