diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f917ec92..d34c1a9aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added support for knapsack constraints - Added isPositive(), isNegative(), isFeasLE(), isFeasLT(), isFeasGE(), isFeasGT(), isHugeValue(), and tests - Added SCIP_LOCKTYPE, addVarLocksType(), getNLocksDown(), getNLocksUp(), getNLocksDownType(), getNLocksUpType(), and tests +- Added addMatrixConsIndicator(), and tests ### Fixed - Raised an error when an expression is used when a variable is required ### Changed diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 7c7216c83..68f7acba1 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -6809,6 +6809,169 @@ cdef class Model: return pyCons + + def addMatrixConsIndicator(self, cons: MatrixExprCons, binvar: Union[Variable, MatrixVariable] = None, + activeone: Union[bool, np.ndarray] = True, name: Union[str, np.ndarray] = "", + initial: Union[bool, np.ndarray] = True, separate: Union[bool, np.ndarray] = True, + enforce: Union[bool, np.ndarray] = True, check: Union[bool, np.ndarray] = True, + propagate: Union[bool, np.ndarray] = True, local: Union[bool, np.ndarray] = False, + dynamic: Union[bool, np.ndarray] = False, removable: Union[bool, np.ndarray] = False, + stickingatnode: Union[bool, np.ndarray] = False) -> MatrixConstraint: + """Add an indicator matrix constraint for the linear inequality `cons`. + + The `binvar` argument models the redundancy of the linear constraint. A solution + for which `binvar` is 1 must satisfy the constraint. + + Parameters + ---------- + cons : MatrixExprCons + a linear inequality of the form "<=". + binvar : Variable or MatrixVariable, optional + binary indicator variable / matrix variable, or None if it should be created. (Default value = None) + activeone : bool or np.ndarray, optional + the matrix constraint should be active if binvar is 1 (0 if activeone = False). + name : str or np.ndarray, optional + name of the matrix constraint. (Default value = "") + initial : bool or np.ndarray, optional + should the LP relaxation of matrix constraint be in the initial LP? (Default value = True) + separate : bool or np.ndarray, optional + should the matrix constraint be separated during LP processing? (Default value = True) + enforce : bool or np.ndarray, optional + should the matrix constraint be enforced during node processing? (Default value = True) + check : bool or np.ndarray, optional + should the matrix constraint be checked for feasibility? (Default value = True) + propagate : bool or np.ndarray, optional + should the matrix constraint be propagated during node processing? (Default value = True) + local : bool or np.ndarray, optional + is the matrix constraint only valid locally? (Default value = False) + dynamic : bool or np.ndarray, optional + is the matrix constraint subject to aging? (Default value = False) + removable : bool or np.ndarray, optional + should the relaxation be removed from the LP due to aging or cleanup? (Default value = False) + stickingatnode : bool or np.ndarray, optional + should the matrix 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) + + Returns + ------- + MatrixConstraint + The newly created Indicator MatrixConstraint object. + """ + + assert isinstance(cons, MatrixExprCons), ( + f"given constraint is not MatrixExprCons but {cons.__class__.__name__}" + ) + + shape = cons.shape + + if isinstance(binvar, MatrixVariable): + assert binvar.shape == shape + if isinstance(activeone, np.ndarray): + assert activeone.shape == shape + if isinstance(name, np.ndarray): + assert name.shape == shape + if isinstance(initial, np.ndarray): + assert initial.shape == shape + if isinstance(separate, np.ndarray): + assert separate.shape == shape + if isinstance(enforce, np.ndarray): + assert enforce.shape == shape + if isinstance(check, np.ndarray): + assert check.shape == shape + if isinstance(propagate, np.ndarray): + assert propagate.shape == shape + if isinstance(local, np.ndarray): + assert local.shape == shape + if isinstance(dynamic, np.ndarray): + assert dynamic.shape == shape + if isinstance(removable, np.ndarray): + assert removable.shape == shape + if isinstance(stickingatnode, np.ndarray): + assert stickingatnode.shape == shape + + if not isinstance(binvar, MatrixVariable): + matrix_binvar = np.full(shape, binvar, dtype=Variable) + else: + matrix_binvar = binvar + + if not isinstance(activeone, np.ndarray): + matrix_activeone = np.full(shape, activeone, dtype=bool) + else: + matrix_activeone = activeone + + if isinstance(name, str): + matrix_names = np.full(shape, name, dtype=object) + if name != "": + for idx in np.ndindex(shape): + matrix_names[idx] = f"{name}_{'_'.join(map(str, idx))}" + else: + matrix_names = name + + if not isinstance(initial, np.ndarray): + matrix_initial = np.full(shape, initial, dtype=bool) + else: + matrix_initial = initial + + if not isinstance(enforce, np.ndarray): + matrix_enforce = np.full(shape, enforce, dtype=bool) + else: + matrix_enforce = enforce + + if not isinstance(separate, np.ndarray): + matrix_separate = np.full(shape, separate, dtype=bool) + else: + matrix_separate = separate + + if not isinstance(check, np.ndarray): + matrix_check = np.full(shape, check, dtype=bool) + else: + matrix_check = check + + if not isinstance(propagate, np.ndarray): + matrix_propagate = np.full(shape, propagate, dtype=bool) + else: + matrix_propagate = propagate + + if not isinstance(local, np.ndarray): + matrix_local = np.full(shape, local, dtype=bool) + else: + matrix_local = local + + if not isinstance(dynamic, np.ndarray): + matrix_dynamic = np.full(shape, dynamic, dtype=bool) + else: + matrix_dynamic = dynamic + + if not isinstance(removable, np.ndarray): + matrix_removable = np.full(shape, removable, dtype=bool) + else: + matrix_removable = removable + + if not isinstance(stickingatnode, np.ndarray): + matrix_stickingatnode = np.full(shape, stickingatnode, dtype=bool) + else: + matrix_stickingatnode = stickingatnode + + matrix_cons = np.empty(shape, dtype=object) + for idx in np.ndindex(shape): + matrix_cons[idx] = self.addConsIndicator( + cons[idx], + binvar=matrix_binvar[idx], + activeone=matrix_activeone[idx], + name=matrix_names[idx], + initial=matrix_initial[idx], + separate=matrix_separate[idx], + enforce=matrix_enforce[idx], + check=matrix_check[idx], + propagate=matrix_propagate[idx], + local=matrix_local[idx], + dynamic=matrix_dynamic[idx], + removable=matrix_removable[idx], + stickingatnode=matrix_stickingatnode[idx], + ) + + return matrix_cons.view(MatrixConstraint) + def getLinearConsIndicator(self, Constraint cons): """ Get the linear constraint corresponding to the indicator constraint. diff --git a/tests/test_matrix_variable.py b/tests/test_matrix_variable.py index fbc59bcf0..9b403db16 100644 --- a/tests/test_matrix_variable.py +++ b/tests/test_matrix_variable.py @@ -336,3 +336,32 @@ def test_performance(): orig_time = end_orig - start_orig assert m.isGT(orig_time, matrix_time) + + +def test_matrix_cons_indicator(): + m = Model() + x = m.addMatrixVar((2, 3), vtype="I", ub=10) + y = m.addMatrixVar(x.shape, vtype="I", ub=10) + is_equal = m.addMatrixVar((1, 2), vtype="B") + + # shape of cons is not equal to shape of is_equal + with pytest.raises(Exception): + m.addMatrixConsIndicator(x >= y, is_equal) + + for i in range(2): + m.addMatrixConsIndicator(x[i] >= y[i], is_equal[0, i]) + m.addMatrixConsIndicator(x[i] <= y[i], is_equal[0, i]) + + m.addMatrixConsIndicator(x[i] >= 5, is_equal[0, i]) + m.addMatrixConsIndicator(y[i] <= 5, is_equal[0, i]) + + for i in range(3): + m.addMatrixConsIndicator(x[:, i] >= y[:, i], is_equal[0]) + m.addMatrixConsIndicator(x[:, i] <= y[:, i], is_equal[0]) + + m.setObjective(is_equal.sum(), "maximize") + m.optimize() + + assert m.getVal(is_equal).sum() == 2 + assert (m.getVal(x) == m.getVal(y)).all().all() + assert (m.getVal(x) == np.array([[5, 5, 5], [5, 5, 5]])).all().all()