Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 73 additions & 4 deletions tests/test_cons.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
69 changes: 60 additions & 9 deletions tests/test_vars.py
Original file line number Diff line number Diff line change
@@ -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()

Expand Down Expand Up @@ -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()
Expand Down
Loading