Skip to content

Commit f773230

Browse files
authored
Merge pull request #857 from Joao-Dionisio/get_infeasibilities
Add recipe for getting infeasible constraints
2 parents 1200c4f + 6b886e8 commit f773230

File tree

4 files changed

+81
-3
lines changed

4 files changed

+81
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44
### Added
5+
- Added recipe with reformulation for detecting infeasible constraints
56
- Wrapped SCIPcreateOrigSol and added tests
67
- Added verbose option for writeProblem and writeParams
78
- Expanded locale test
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from pyscipopt import Model, quicksum
2+
3+
4+
def get_infeasible_constraints(orig_model: Model, verbose=False):
5+
"""
6+
Given a model, adds slack variables to all the constraints and minimizes a binary variable that indicates if they're positive.
7+
Positive slack variables correspond to infeasible constraints.
8+
"""
9+
10+
model = Model(sourceModel=orig_model, origcopy=True) # to preserve the model
11+
12+
slack = {}
13+
aux = {}
14+
binary = {}
15+
aux_binary = {}
16+
17+
for c in model.getConss():
18+
19+
slack[c.name] = model.addVar(lb=-float("inf"), name="s_"+c.name)
20+
model.addConsCoeff(c, slack[c.name], 1)
21+
binary[c.name] = model.addVar(vtype="B") # Binary variable to get minimum infeasible constraints. See PR #857.
22+
23+
# getting the absolute value because of <= and >= constraints
24+
aux[c.name] = model.addVar()
25+
model.addCons(aux[c.name] >= slack[c.name])
26+
model.addCons(aux[c.name] >= -slack[c.name])
27+
28+
# modeling aux > 0 => binary = 1 constraint. See https://or.stackexchange.com/q/12142/5352 for an explanation
29+
aux_binary[c.name] = model.addVar(vtype="B")
30+
model.addCons(binary[c.name]+aux_binary[c.name] == 1)
31+
model.addConsSOS1([aux[c.name], aux_binary[c.name]])
32+
33+
model.setObjective(quicksum(binary[c.name] for c in orig_model.getConss()))
34+
model.hideOutput()
35+
model.optimize()
36+
37+
n_infeasibilities_detected = 0
38+
for c in binary:
39+
if model.isGT(model.getVal(binary[c]), 0):
40+
n_infeasibilities_detected += 1
41+
print("Constraint %s is causing an infeasibility." % c)
42+
43+
if verbose:
44+
if n_infeasibilities_detected > 0:
45+
print("If the constraint names are unhelpful, consider giving them\
46+
a suitable name when creating the model with model.addCons(..., name=\"the_name_you_want\")")
47+
else:
48+
print("Model is feasible.")
49+
50+
return n_infeasibilities_detected, aux

src/pyscipopt/scip.pxi

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,7 +1607,6 @@ cdef class Model:
16071607
PY_SCIP_CALL(SCIPtightenVarLb(self._scip, var.scip_var, lb, force, &infeasible, &tightened))
16081608
return infeasible, tightened
16091609

1610-
16111610
def tightenVarUb(self, Variable var, ub, force=False):
16121611
"""Tighten the upper bound in preprocessing or current node, if the bound is tighter.
16131612
@@ -1624,7 +1623,6 @@ cdef class Model:
16241623
PY_SCIP_CALL(SCIPtightenVarUb(self._scip, var.scip_var, ub, force, &infeasible, &tightened))
16251624
return infeasible, tightened
16261625

1627-
16281626
def tightenVarUbGlobal(self, Variable var, ub, force=False):
16291627
"""Tighten the global upper bound, if the bound is tighter.
16301628
@@ -2556,7 +2554,6 @@ cdef class Model:
25562554
PY_SCIP_CALL(SCIPreleaseCons(self._scip, &(<Constraint>cons).scip_cons))
25572555
return disj_cons
25582556

2559-
25602557
def getConsNVars(self, Constraint constraint):
25612558
"""
25622559
Gets number of variables in a constraint.
@@ -2699,6 +2696,7 @@ cdef class Model:
26992696

27002697
PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons))
27012698
return Constraint.create(scip_cons)
2699+
27022700
def addConsSOS2(self, vars, weights=None, name="SOS2cons",
27032701
initial=True, separate=True, enforce=True, check=True,
27042702
propagate=True, local=False, dynamic=False,

tests/test_recipe_infeasibilities.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from pyscipopt import Model
2+
from pyscipopt.recipes.infeasibilities import get_infeasible_constraints
3+
4+
5+
def test_get_infeasible_constraints():
6+
m = Model()
7+
8+
x = m.addVar(lb=0)
9+
m.setObjective(2*x)
10+
11+
m.addCons(x <= 4)
12+
13+
n_infeasibilities_detected = get_infeasible_constraints(m)[0]
14+
assert n_infeasibilities_detected == 0
15+
16+
m.addCons(x <= -1)
17+
18+
n_infeasibilities_detected = get_infeasible_constraints(m)[0]
19+
assert n_infeasibilities_detected == 1
20+
21+
m.addCons(x == 2)
22+
23+
n_infeasibilities_detected = get_infeasible_constraints(m)[0]
24+
assert n_infeasibilities_detected == 1
25+
26+
m.addCons(x == -4)
27+
28+
n_infeasibilities_detected = get_infeasible_constraints(m)[0]
29+
assert n_infeasibilities_detected == 2

0 commit comments

Comments
 (0)