Skip to content

Commit ed56984

Browse files
Feature: new method addMatrixConsIndicator (#1024)
* ENH: new method `addMatrixConsIndicator` similar to `addConsIndicator` but for matrix * Update CHANGELOG.md * Update test_matrix_variable.py * use pyscipopt code and doc style Condensed the addMatrixConsIndicator method signature to a single line and updated the docstring for improved clarity and consistency. Parameter descriptions were streamlined and default values were explicitly stated. * use pyscipopt code and doc style * Add return type to add_matrix_constraint method Specified the return type of the Model.add_matrix_constraint method as MatrixConstraint to improve type clarity and support better static analysis. * Apply suggestions from code review Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com> --------- Co-authored-by: João Dionísio <57299939+Joao-Dionisio@users.noreply.github.com>
1 parent 112a8fc commit ed56984

File tree

3 files changed

+193
-0
lines changed

3 files changed

+193
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Added support for knapsack constraints
77
- Added isPositive(), isNegative(), isFeasLE(), isFeasLT(), isFeasGE(), isFeasGT(), isHugeValue(), and tests
88
- Added SCIP_LOCKTYPE, addVarLocksType(), getNLocksDown(), getNLocksUp(), getNLocksDownType(), getNLocksUpType(), and tests
9+
- Added addMatrixConsIndicator(), and tests
910
### Fixed
1011
- Raised an error when an expression is used when a variable is required
1112
### Changed

src/pyscipopt/scip.pxi

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6809,6 +6809,169 @@ cdef class Model:
68096809

68106810
return pyCons
68116811

6812+
6813+
def addMatrixConsIndicator(self, cons: MatrixExprCons, binvar: Union[Variable, MatrixVariable] = None,
6814+
activeone: Union[bool, np.ndarray] = True, name: Union[str, np.ndarray] = "",
6815+
initial: Union[bool, np.ndarray] = True, separate: Union[bool, np.ndarray] = True,
6816+
enforce: Union[bool, np.ndarray] = True, check: Union[bool, np.ndarray] = True,
6817+
propagate: Union[bool, np.ndarray] = True, local: Union[bool, np.ndarray] = False,
6818+
dynamic: Union[bool, np.ndarray] = False, removable: Union[bool, np.ndarray] = False,
6819+
stickingatnode: Union[bool, np.ndarray] = False) -> MatrixConstraint:
6820+
"""Add an indicator matrix constraint for the linear inequality `cons`.
6821+
6822+
The `binvar` argument models the redundancy of the linear constraint. A solution
6823+
for which `binvar` is 1 must satisfy the constraint.
6824+
6825+
Parameters
6826+
----------
6827+
cons : MatrixExprCons
6828+
a linear inequality of the form "<=".
6829+
binvar : Variable or MatrixVariable, optional
6830+
binary indicator variable / matrix variable, or None if it should be created. (Default value = None)
6831+
activeone : bool or np.ndarray, optional
6832+
the matrix constraint should be active if binvar is 1 (0 if activeone = False).
6833+
name : str or np.ndarray, optional
6834+
name of the matrix constraint. (Default value = "")
6835+
initial : bool or np.ndarray, optional
6836+
should the LP relaxation of matrix constraint be in the initial LP? (Default value = True)
6837+
separate : bool or np.ndarray, optional
6838+
should the matrix constraint be separated during LP processing? (Default value = True)
6839+
enforce : bool or np.ndarray, optional
6840+
should the matrix constraint be enforced during node processing? (Default value = True)
6841+
check : bool or np.ndarray, optional
6842+
should the matrix constraint be checked for feasibility? (Default value = True)
6843+
propagate : bool or np.ndarray, optional
6844+
should the matrix constraint be propagated during node processing? (Default value = True)
6845+
local : bool or np.ndarray, optional
6846+
is the matrix constraint only valid locally? (Default value = False)
6847+
dynamic : bool or np.ndarray, optional
6848+
is the matrix constraint subject to aging? (Default value = False)
6849+
removable : bool or np.ndarray, optional
6850+
should the relaxation be removed from the LP due to aging or cleanup? (Default value = False)
6851+
stickingatnode : bool or np.ndarray, optional
6852+
should the matrix constraint always be kept at the node where it was added,
6853+
even if it may be moved to a more global node? (Default value = False)
6854+
6855+
Returns
6856+
-------
6857+
MatrixConstraint
6858+
The newly created Indicator MatrixConstraint object.
6859+
"""
6860+
6861+
assert isinstance(cons, MatrixExprCons), (
6862+
f"given constraint is not MatrixExprCons but {cons.__class__.__name__}"
6863+
)
6864+
6865+
shape = cons.shape
6866+
6867+
if isinstance(binvar, MatrixVariable):
6868+
assert binvar.shape == shape
6869+
if isinstance(activeone, np.ndarray):
6870+
assert activeone.shape == shape
6871+
if isinstance(name, np.ndarray):
6872+
assert name.shape == shape
6873+
if isinstance(initial, np.ndarray):
6874+
assert initial.shape == shape
6875+
if isinstance(separate, np.ndarray):
6876+
assert separate.shape == shape
6877+
if isinstance(enforce, np.ndarray):
6878+
assert enforce.shape == shape
6879+
if isinstance(check, np.ndarray):
6880+
assert check.shape == shape
6881+
if isinstance(propagate, np.ndarray):
6882+
assert propagate.shape == shape
6883+
if isinstance(local, np.ndarray):
6884+
assert local.shape == shape
6885+
if isinstance(dynamic, np.ndarray):
6886+
assert dynamic.shape == shape
6887+
if isinstance(removable, np.ndarray):
6888+
assert removable.shape == shape
6889+
if isinstance(stickingatnode, np.ndarray):
6890+
assert stickingatnode.shape == shape
6891+
6892+
if not isinstance(binvar, MatrixVariable):
6893+
matrix_binvar = np.full(shape, binvar, dtype=Variable)
6894+
else:
6895+
matrix_binvar = binvar
6896+
6897+
if not isinstance(activeone, np.ndarray):
6898+
matrix_activeone = np.full(shape, activeone, dtype=bool)
6899+
else:
6900+
matrix_activeone = activeone
6901+
6902+
if isinstance(name, str):
6903+
matrix_names = np.full(shape, name, dtype=object)
6904+
if name != "":
6905+
for idx in np.ndindex(shape):
6906+
matrix_names[idx] = f"{name}_{'_'.join(map(str, idx))}"
6907+
else:
6908+
matrix_names = name
6909+
6910+
if not isinstance(initial, np.ndarray):
6911+
matrix_initial = np.full(shape, initial, dtype=bool)
6912+
else:
6913+
matrix_initial = initial
6914+
6915+
if not isinstance(enforce, np.ndarray):
6916+
matrix_enforce = np.full(shape, enforce, dtype=bool)
6917+
else:
6918+
matrix_enforce = enforce
6919+
6920+
if not isinstance(separate, np.ndarray):
6921+
matrix_separate = np.full(shape, separate, dtype=bool)
6922+
else:
6923+
matrix_separate = separate
6924+
6925+
if not isinstance(check, np.ndarray):
6926+
matrix_check = np.full(shape, check, dtype=bool)
6927+
else:
6928+
matrix_check = check
6929+
6930+
if not isinstance(propagate, np.ndarray):
6931+
matrix_propagate = np.full(shape, propagate, dtype=bool)
6932+
else:
6933+
matrix_propagate = propagate
6934+
6935+
if not isinstance(local, np.ndarray):
6936+
matrix_local = np.full(shape, local, dtype=bool)
6937+
else:
6938+
matrix_local = local
6939+
6940+
if not isinstance(dynamic, np.ndarray):
6941+
matrix_dynamic = np.full(shape, dynamic, dtype=bool)
6942+
else:
6943+
matrix_dynamic = dynamic
6944+
6945+
if not isinstance(removable, np.ndarray):
6946+
matrix_removable = np.full(shape, removable, dtype=bool)
6947+
else:
6948+
matrix_removable = removable
6949+
6950+
if not isinstance(stickingatnode, np.ndarray):
6951+
matrix_stickingatnode = np.full(shape, stickingatnode, dtype=bool)
6952+
else:
6953+
matrix_stickingatnode = stickingatnode
6954+
6955+
matrix_cons = np.empty(shape, dtype=object)
6956+
for idx in np.ndindex(shape):
6957+
matrix_cons[idx] = self.addConsIndicator(
6958+
cons[idx],
6959+
binvar=matrix_binvar[idx],
6960+
activeone=matrix_activeone[idx],
6961+
name=matrix_names[idx],
6962+
initial=matrix_initial[idx],
6963+
separate=matrix_separate[idx],
6964+
enforce=matrix_enforce[idx],
6965+
check=matrix_check[idx],
6966+
propagate=matrix_propagate[idx],
6967+
local=matrix_local[idx],
6968+
dynamic=matrix_dynamic[idx],
6969+
removable=matrix_removable[idx],
6970+
stickingatnode=matrix_stickingatnode[idx],
6971+
)
6972+
6973+
return matrix_cons.view(MatrixConstraint)
6974+
68126975
def getLinearConsIndicator(self, Constraint cons):
68136976
"""
68146977
Get the linear constraint corresponding to the indicator constraint.

tests/test_matrix_variable.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,3 +336,32 @@ def test_performance():
336336
orig_time = end_orig - start_orig
337337

338338
assert m.isGT(orig_time, matrix_time)
339+
340+
341+
def test_matrix_cons_indicator():
342+
m = Model()
343+
x = m.addMatrixVar((2, 3), vtype="I", ub=10)
344+
y = m.addMatrixVar(x.shape, vtype="I", ub=10)
345+
is_equal = m.addMatrixVar((1, 2), vtype="B")
346+
347+
# shape of cons is not equal to shape of is_equal
348+
with pytest.raises(Exception):
349+
m.addMatrixConsIndicator(x >= y, is_equal)
350+
351+
for i in range(2):
352+
m.addMatrixConsIndicator(x[i] >= y[i], is_equal[0, i])
353+
m.addMatrixConsIndicator(x[i] <= y[i], is_equal[0, i])
354+
355+
m.addMatrixConsIndicator(x[i] >= 5, is_equal[0, i])
356+
m.addMatrixConsIndicator(y[i] <= 5, is_equal[0, i])
357+
358+
for i in range(3):
359+
m.addMatrixConsIndicator(x[:, i] >= y[:, i], is_equal[0])
360+
m.addMatrixConsIndicator(x[:, i] <= y[:, i], is_equal[0])
361+
362+
m.setObjective(is_equal.sum(), "maximize")
363+
m.optimize()
364+
365+
assert m.getVal(is_equal).sum() == 2
366+
assert (m.getVal(x) == m.getVal(y)).all().all()
367+
assert (m.getVal(x) == np.array([[5, 5, 5], [5, 5, 5]])).all().all()

0 commit comments

Comments
 (0)