From b48462a91c73a59363097c3a7a7681eb685925e0 Mon Sep 17 00:00:00 2001 From: Adam Jemielita Date: Sun, 6 Sep 2026 11:02:57 +0200 Subject: [PATCH 1/4] implement tests for getRowLinear --- tests/test_cons.py | 77 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 4 deletions(-) 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 From 9a41e3b81494109baa1d0fa6b44f4baf0a05ea07 Mon Sep 17 00:00:00 2001 From: Adam Jemielita Date: Sun, 6 Sep 2026 12:03:01 +0200 Subject: [PATCH 2/4] extend testing for isActive --- tests/test_vars.py | 76 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/tests/test_vars.py b/tests/test_vars.py index b32acfd67..249a44b28 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,73 @@ 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 = [m.getTransformedVar(var) for var in original_vars] + transformed_statuses = [var.getStatus() for var in transformed] + + # at the time of writing this test, the presolve step aggregates two variables and fixes one variable + expected_aggregated_cnt = 2 + aggregated_cnt = transformed_statuses.count("AGGREGATED") + assert aggregated_cnt == expected_aggregated_cnt, ( + f"Expected {expected_aggregated_cnt} aggregated variables, but got: {aggregated_cnt}; update the test model" + ) + + expected_fixed_cnt = 1 + fixed_cnt = transformed_statuses.count("FIXED") + assert fixed_cnt == expected_fixed_cnt, ( + f"Expected {expected_fixed_cnt} fixed variables, but got: {fixed_cnt}; update the test model" + ) + + # aggregated and fixed variables should not be active + for var in transformed: + 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 = [model.getTransformedVar(var) for var in vars] + for var in transformed: + assert var.getStatus() == "LOOSE", ( + f"Expected all variables to be LOOSE after presolve, but got: {[mip_var.getStatus() for mip_var in transformed]}; 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: + assert var.getStatus() == "COLUMN", ( + f"Expected all variables to be COLUMN after optimization, but got: {[mip_var.getStatus() for mip_var in transformed]}; update the test model" + ) + assert var.isActive() + def test_markDoNotAggrVar_and_getStatus(): model = Model() From ae3b3073d8a98528d57e93ca0946ed40c6ef639e Mon Sep 17 00:00:00 2001 From: Adam Jemielita Date: Sun, 6 Sep 2026 12:40:50 +0200 Subject: [PATCH 3/4] update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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 From f85a82d807e4b1f32861d4ec7414976faba061d2 Mon Sep 17 00:00:00 2001 From: Adam Jemielita Date: Sun, 6 Sep 2026 12:51:07 +0200 Subject: [PATCH 4/4] switch to discovering statuses --- tests/test_vars.py | 35 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/tests/test_vars.py b/tests/test_vars.py index 249a44b28..220ae9167 100644 --- a/tests/test_vars.py +++ b/tests/test_vars.py @@ -141,24 +141,17 @@ def test_isActive(): assert var.getStatus() == "ORIGINAL" assert var.isActive() - transformed = [m.getTransformedVar(var) for var in original_vars] - transformed_statuses = [var.getStatus() for var in transformed] + 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 - expected_aggregated_cnt = 2 - aggregated_cnt = transformed_statuses.count("AGGREGATED") - assert aggregated_cnt == expected_aggregated_cnt, ( - f"Expected {expected_aggregated_cnt} aggregated variables, but got: {aggregated_cnt}; update the test model" - ) - - expected_fixed_cnt = 1 - fixed_cnt = transformed_statuses.count("FIXED") - assert fixed_cnt == expected_fixed_cnt, ( - f"Expected {expected_fixed_cnt} fixed variables, but got: {fixed_cnt}; update the test model" - ) - - # aggregated and fixed variables should not be active - for var in transformed: + 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() @@ -174,18 +167,18 @@ def test_isActive_mip(): model.presolve() # at the time of writing this test, all variables are LOOSE after presolve - transformed = [model.getTransformedVar(var) for var in vars] - for var in transformed: + 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]}; update the test model" + 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: + 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]}; update the test model" + 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()