diff --git a/CHANGELOG.md b/CHANGELOG.md index ae37c3270..44af6c4b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added `addConsCumulative()` for SCIP cumulative constraints (#1222) - `Expr` and `GenExpr` support `__pos__` magic method like `+Expr` or `+GenExpr` - Added type annotations to most methods on the `Model` class +- Added tests for `getRowLinear()` and extended existing testing for `isActive()` ### Fixed - Fixed Cython 3.3 compatibility (#1248) - Made `test_markDoNotAggrVar_and_getStatus` robust to SCIP presolve changes by discovering the aggregated/multi-aggregated variables instead of hardcoding them diff --git a/tests/test_cons.py b/tests/test_cons.py index 243014427..b036e08b0 100644 --- a/tests/test_cons.py +++ b/tests/test_cons.py @@ -1,4 +1,5 @@ -from pyscipopt import Model, quicksum +from pyscipopt import Eventhdlr, Model, quicksum, SCIP_EVENTTYPE, SCIP_PARAMSETTING +from pyscipopt.scip import Row import random import pytest @@ -398,6 +399,74 @@ def test_getValsLinear(): assert m.getValsLinear(c2) == {'x': 1, 'z': 4} -@pytest.mark.skip(reason="TODO: test getRowLinear()") -def test_getRowLinear(): - assert True + +def test_getRowLinear_for_linear_constraints(): + class RowAddedEvent(Eventhdlr): + def __init__(self, cons): + super().__init__() + self.cons = cons + self.matched = {con: False for con in cons} + + def eventinit(self): + self.model.catchEvent(SCIP_EVENTTYPE.ROWADDEDLP, self) + + def eventexit(self): + self.model.dropEvent(SCIP_EVENTTYPE.ROWADDEDLP, self) + + def eventexec(self, event): + row = event.getRow() + + for con in self.cons: + try: + trans_con = self.model.getTransformedCons(con) + row_from_con = self.model.getRowLinear(trans_con) + except Warning: + continue # skip if the constraint has not yet been transformed into a row + + assert isinstance(row_from_con, Row) + if ( + row == row_from_con + and row.getNNonz() == row_from_con.getNNonz() + and row.getVals() == row_from_con.getVals() + and row.getRhs() == row_from_con.getRhs() + and row.getLhs() == row_from_con.getLhs() + ): + self.matched[con] = True + + m = Model() + x = m.addVar("x", lb=0, ub=10) + y = m.addVar("y", lb=0, ub=10) + z = m.addVar("z", lb=0, ub=10) + + m.setObjective(x + 2 * y + 3 * z, "maximize") + con_1 = m.addCons(x + y <= 5) + con_2 = m.addCons(2 * x + 3 * y - z <= 12) + con_3 = m.addCons(x - y <= 2) + cons = [con_1, con_2, con_3] + + hdlr = RowAddedEvent(cons) + m.includeEventhdlr(hdlr, "rowadded", "row added to LP") + + # turn off presolve to ensure that the constraints are not removed or modified before the LP is created + m.setPresolve(SCIP_PARAMSETTING.OFF) + m.optimize() + + for con in cons: + assert con.isLinear() + assert all(hdlr.matched.values()), "Not all constraints matched the added rows in the LP" + + +def test_getRowLinear_for_nonlinear_constraints(): + m = Model() + x = m.addVar("x", lb=0, ub=2) + y = m.addVar("y", lb=0, ub=4) + + con_1 = m.addCons(x * y + 3 * y <= 5) + con_2 = m.addCons(x**2 + y**2 <= 10) + + assert con_1.isNonlinear() + assert con_2.isNonlinear() + with pytest.raises(Warning): + m.getRowLinear(con_1) + with pytest.raises(Warning): + m.getRowLinear(con_2) \ No newline at end of file diff --git a/tests/test_vars.py b/tests/test_vars.py index b32acfd67..220ae9167 100644 --- a/tests/test_vars.py +++ b/tests/test_vars.py @@ -1,6 +1,7 @@ -from pyscipopt import Model, SCIP_PARAMSETTING, SCIP_BRANCHDIR, SCIP_IMPLINTTYPE +from pyscipopt import Model, SCIP_BRANCHDIR, SCIP_IMPLINTTYPE from helpers.utils import random_mip_1 + def test_variablebounds(): m = Model() @@ -121,16 +122,66 @@ def test_getNBranchingsCurrentRun(): assert n_branchings == m.getNNodes() - 1 + +# test for a small model so that AGGREGATED and FIXED statuses are easily created def test_isActive(): m = Model() - x = m.addVar(vtype='C', lb=0.0, ub=1.0) - # newly added variables should be active - assert x.isActive() - m.freeProb() - - # TODO lacks tests for cases when returned false due to - # - fixed (probably during probing) - # - aggregated + x = m.addVar("x", lb=0, ub=20) + y = m.addVar("y", lb=0, ub=20) + z = m.addVar("z", lb=0, ub=15) + original_vars = [x, y, z] + + m.addCons(y - x == 0) + m.addCons(z + x == 10) + + m.presolve() + + # original variables are active (i.e., neither aggregated nor fixed) + for var in original_vars: + assert var.getStatus() == "ORIGINAL" + assert var.isActive() + + transformed_vars = [m.getTransformedVar(var) for var in original_vars] + + # at the time of writing this test, the presolve step aggregates two variables and fixes one variable + aggregated_vars = [var for var in transformed_vars if var.getStatus() == "AGGREGATED"] + assert aggregated_vars, "presolve no longer aggregates variables; update the test model" + for var in aggregated_vars: + assert not var.isActive() + + fixed_vars = [var for var in transformed_vars if var.getStatus() == "FIXED"] + assert fixed_vars, "presolve no longer fixes variables; update the test model" + for var in fixed_vars: + assert not var.isActive() + + +# test for a bigger model so that LOOSE and COLUMN statuses are created +def test_isActive_mip(): + model = random_mip_1(small=True) + + vars = model.getVars() + + for var in vars: + assert var.getStatus() == "ORIGINAL" + assert var.isActive() + + model.presolve() + # at the time of writing this test, all variables are LOOSE after presolve + transformed_vars = [model.getTransformedVar(var) for var in vars] + for var in transformed_vars: + assert var.getStatus() == "LOOSE", ( + f"Expected all variables to be LOOSE after presolve, but got: {[mip_var.getStatus() for mip_var in transformed_vars]}; update the test model" + ) + assert var.isActive() + + model.optimize() + # at the time of writing this test, all variables are COLUMN after optimization + for var in transformed_vars: + assert var.getStatus() == "COLUMN", ( + f"Expected all variables to be COLUMN after optimization, but got: {[mip_var.getStatus() for mip_var in transformed_vars]}; update the test model" + ) + assert var.isActive() + def test_markDoNotAggrVar_and_getStatus(): model = Model()