From 41c3b63027e9482615bbc7bc1b2064aaedbcd50b Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 28 May 2024 14:28:23 +0100 Subject: [PATCH 1/7] Add recipe for infeasibilities --- src/pyscipopt/recipes/infeasibilities.py | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/pyscipopt/recipes/infeasibilities.py diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py new file mode 100644 index 000000000..b33677f77 --- /dev/null +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -0,0 +1,42 @@ +from pyscipopt import Model, quicksum + + +def get_infeasible_constraints(orig_model: Model, verbose=False): + """ + Given a model, adds slack variables to all the constraints and minimizes their sum. + Non-zero slack variables correspond to infeasible constraints. + """ + + model = Model(sourceModel=orig_model, origcopy=True) # to preserve the model + slack = {} + aux = {} + for c in model.getConss(): + + slack[c.name] = model.addVar(lb=-float("inf"), name=c.name) + + model.addConsCoeff(c, slack[c.name], 1) + + # getting the absolute value because of <= and >= constraints + aux[c.name] = model.addVar(obj=1) + model.addCons(aux[c.name] >= slack[c.name]) + model.addCons(aux[c.name] >= -slack[c.name]) + + + model.hideOutput() + model.setPresolve(0) # just to be safe, maybe we can use presolving + model.optimize() + + n_infeasibilities_detected = 0 + for v in aux: + if model.isGT(model.getVal(aux[v]), 0): + n_infeasibilities_detected += 1 + print("Constraint %s is causing an infeasibility." % v) + + if verbose: + if n_infeasibilities_detected > 0: + print("If the constraint names are unhelpful, consider giving them\ + a suitable name when creating the model with model.addCons(..., name=\"the_name_you_want\")") + else: + print("Model is feasible.") + + return n_infeasibilities_detected, aux \ No newline at end of file From 53f61d624572c482eb70c430b460ac83bd2e9f97 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 28 May 2024 14:28:30 +0100 Subject: [PATCH 2/7] Add tests --- CHANGELOG.md | 1 + tests/test_recipe_infeasibilities.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/test_recipe_infeasibilities.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8405580d4..a1508233f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Added recipe with reformulation for detecting infeasible constraints - Wrapped SCIPcreateOrigSol and added tests - Added verbose option for writeProblem and writeParams - Expanded locale test diff --git a/tests/test_recipe_infeasibilities.py b/tests/test_recipe_infeasibilities.py new file mode 100644 index 000000000..db6e405c4 --- /dev/null +++ b/tests/test_recipe_infeasibilities.py @@ -0,0 +1,27 @@ +from pyscipopt import Model +from pyscipopt.recipes.infeasibilities import get_infeasible_constraints + + +def test_get_infeasible_constraints(): + m = Model() + + x = m.addVar(lb=0) + m.addCons(x <= 4) + + n_infeasibilities_detected = get_infeasible_constraints(m)[0] + assert n_infeasibilities_detected == 0 + + m.addCons(x <= -1) + + n_infeasibilities_detected = get_infeasible_constraints(m)[0] + assert n_infeasibilities_detected == 1 + + m.addCons(x == 2) + + n_infeasibilities_detected = get_infeasible_constraints(m)[0] + assert n_infeasibilities_detected == 1 + + m.addCons(x == -4) + + n_infeasibilities_detected = get_infeasible_constraints(m)[0] + assert n_infeasibilities_detected == 3 # with x == -4, x == 2 also becomes infeasible \ No newline at end of file From 2bc90145e82a5ed635430ca37e26b6b0f6613349 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 28 May 2024 14:40:22 +0100 Subject: [PATCH 3/7] Set feasibility emphasis --- src/pyscipopt/recipes/infeasibilities.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py index b33677f77..d1ddf966a 100644 --- a/src/pyscipopt/recipes/infeasibilities.py +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -1,4 +1,4 @@ -from pyscipopt import Model, quicksum +from pyscipopt import Model, SCIP_PARAMEMPHASIS def get_infeasible_constraints(orig_model: Model, verbose=False): @@ -24,6 +24,8 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): model.hideOutput() model.setPresolve(0) # just to be safe, maybe we can use presolving + model.setEmphasis(SCIP_PARAMEMPHASIS.PHASEFEAS) # focusing on model feasibility + #model.setParam("limits/solutions", 1) # SCIP sometimes returns the incorrect stage when models are prematurely stopped model.optimize() n_infeasibilities_detected = 0 From 14d0b520c5b6b76f5bc4f14f756531175ea1518f Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Tue, 28 May 2024 15:03:31 +0100 Subject: [PATCH 4/7] Changed comment --- src/pyscipopt/recipes/infeasibilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py index d1ddf966a..63debc5f7 100644 --- a/src/pyscipopt/recipes/infeasibilities.py +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -25,7 +25,7 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): model.hideOutput() model.setPresolve(0) # just to be safe, maybe we can use presolving model.setEmphasis(SCIP_PARAMEMPHASIS.PHASEFEAS) # focusing on model feasibility - #model.setParam("limits/solutions", 1) # SCIP sometimes returns the incorrect stage when models are prematurely stopped + #model.setParam("limits/solutions", 1) # PySCIPOpt sometimes incorrectly raises an error when a model is prematurely stopped. See PR # 815. model.optimize() n_infeasibilities_detected = 0 From 195016dad1debe3c124d4284f8d4c49d4fbfd236 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Wed, 5 Jun 2024 18:09:39 +0100 Subject: [PATCH 5/7] Add binary variable --- src/pyscipopt/recipes/infeasibilities.py | 31 ++++++++++++++---------- src/pyscipopt/scip.pxi | 4 +-- tests/test_recipe_infeasibilities.py | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py index 63debc5f7..34153aed0 100644 --- a/src/pyscipopt/recipes/infeasibilities.py +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -3,36 +3,41 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): """ - Given a model, adds slack variables to all the constraints and minimizes their sum. - Non-zero slack variables correspond to infeasible constraints. + Given a model, adds slack variables to all the constraints and minimizes a binary variable that indicates if they're positive. + Positive slack variables correspond to infeasible constraints. """ - + model = Model(sourceModel=orig_model, origcopy=True) # to preserve the model - slack = {} - aux = {} + slack = {} + aux = {} + binary = {} + aux_binary = {} + for c in model.getConss(): slack[c.name] = model.addVar(lb=-float("inf"), name=c.name) - model.addConsCoeff(c, slack[c.name], 1) + binary[c.name] = model.addVar(obj=1, vtype="B") # Binary variable to get minimum infeasible constraints. See PR #857.) # getting the absolute value because of <= and >= constraints - aux[c.name] = model.addVar(obj=1) + aux[c.name] = model.addVar() model.addCons(aux[c.name] >= slack[c.name]) model.addCons(aux[c.name] >= -slack[c.name]) - + + # modeling aux > 0 => binary = 1 constraint. See https://or.stackexchange.com/q/12142/5352 for an explanation + aux_binary[c.name] = model.addVar(ub=1) + model.addCons(binary[c.name]+aux_binary[c.name] == 1) + model.addConsSOS1([aux[c.name], aux_binary[c.name]]) model.hideOutput() model.setPresolve(0) # just to be safe, maybe we can use presolving - model.setEmphasis(SCIP_PARAMEMPHASIS.PHASEFEAS) # focusing on model feasibility - #model.setParam("limits/solutions", 1) # PySCIPOpt sometimes incorrectly raises an error when a model is prematurely stopped. See PR # 815. model.optimize() n_infeasibilities_detected = 0 - for v in aux: - if model.isGT(model.getVal(aux[v]), 0): + for c in binary: + if model.isGT(model.getVal(binary[c]), 0): n_infeasibilities_detected += 1 - print("Constraint %s is causing an infeasibility." % v) + print("Constraint %s is causing an infeasibility." % c) if verbose: if n_infeasibilities_detected > 0: diff --git a/src/pyscipopt/scip.pxi b/src/pyscipopt/scip.pxi index 95545b2da..5b9c1b398 100644 --- a/src/pyscipopt/scip.pxi +++ b/src/pyscipopt/scip.pxi @@ -1607,7 +1607,6 @@ cdef class Model: PY_SCIP_CALL(SCIPtightenVarLb(self._scip, var.scip_var, lb, force, &infeasible, &tightened)) return infeasible, tightened - def tightenVarUb(self, Variable var, ub, force=False): """Tighten the upper bound in preprocessing or current node, if the bound is tighter. @@ -1624,7 +1623,6 @@ cdef class Model: PY_SCIP_CALL(SCIPtightenVarUb(self._scip, var.scip_var, ub, force, &infeasible, &tightened)) return infeasible, tightened - def tightenVarUbGlobal(self, Variable var, ub, force=False): """Tighten the global upper bound, if the bound is tighter. @@ -2556,7 +2554,6 @@ cdef class Model: PY_SCIP_CALL(SCIPreleaseCons(self._scip, &(cons).scip_cons)) return disj_cons - def getConsNVars(self, Constraint constraint): """ Gets number of variables in a constraint. @@ -2699,6 +2696,7 @@ cdef class Model: PY_SCIP_CALL(SCIPaddCons(self._scip, scip_cons)) return Constraint.create(scip_cons) + def addConsSOS2(self, vars, weights=None, name="SOS2cons", initial=True, separate=True, enforce=True, check=True, propagate=True, local=False, dynamic=False, diff --git a/tests/test_recipe_infeasibilities.py b/tests/test_recipe_infeasibilities.py index db6e405c4..59d3262ff 100644 --- a/tests/test_recipe_infeasibilities.py +++ b/tests/test_recipe_infeasibilities.py @@ -24,4 +24,4 @@ def test_get_infeasible_constraints(): m.addCons(x == -4) n_infeasibilities_detected = get_infeasible_constraints(m)[0] - assert n_infeasibilities_detected == 3 # with x == -4, x == 2 also becomes infeasible \ No newline at end of file + assert n_infeasibilities_detected == 2 \ No newline at end of file From fecd0c9698d28119581b5009b3be9151d6e0bd6e Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Thu, 6 Jun 2024 15:36:25 +0100 Subject: [PATCH 6/7] Overwrite old objective --- src/pyscipopt/recipes/infeasibilities.py | 6 ++++-- tests/test_recipe_infeasibilities.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py index 34153aed0..2b1aa6439 100644 --- a/src/pyscipopt/recipes/infeasibilities.py +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -1,4 +1,4 @@ -from pyscipopt import Model, SCIP_PARAMEMPHASIS +from pyscipopt import Model, quicksum def get_infeasible_constraints(orig_model: Model, verbose=False): @@ -8,6 +8,7 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): """ model = Model(sourceModel=orig_model, origcopy=True) # to preserve the model + slack = {} aux = {} binary = {} @@ -17,7 +18,7 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): slack[c.name] = model.addVar(lb=-float("inf"), name=c.name) model.addConsCoeff(c, slack[c.name], 1) - binary[c.name] = model.addVar(obj=1, vtype="B") # Binary variable to get minimum infeasible constraints. See PR #857.) + binary[c.name] = model.addVar(vtype="B") # Binary variable to get minimum infeasible constraints. See PR #857. # getting the absolute value because of <= and >= constraints aux[c.name] = model.addVar() @@ -29,6 +30,7 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): model.addCons(binary[c.name]+aux_binary[c.name] == 1) model.addConsSOS1([aux[c.name], aux_binary[c.name]]) + model.setObjective(quicksum(binary[c.name] for c in orig_model.getConss())) model.hideOutput() model.setPresolve(0) # just to be safe, maybe we can use presolving model.optimize() diff --git a/tests/test_recipe_infeasibilities.py b/tests/test_recipe_infeasibilities.py index 59d3262ff..e86db13f9 100644 --- a/tests/test_recipe_infeasibilities.py +++ b/tests/test_recipe_infeasibilities.py @@ -6,6 +6,8 @@ def test_get_infeasible_constraints(): m = Model() x = m.addVar(lb=0) + m.setObjective(2*x) + m.addCons(x <= 4) n_infeasibilities_detected = get_infeasible_constraints(m)[0] From 6b886e81ba0bf2e1a4973da3c3f810131fa0d980 Mon Sep 17 00:00:00 2001 From: Joao-Dionisio Date: Fri, 14 Jun 2024 11:08:11 +0100 Subject: [PATCH 7/7] Mark comments --- src/pyscipopt/recipes/infeasibilities.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pyscipopt/recipes/infeasibilities.py b/src/pyscipopt/recipes/infeasibilities.py index 2b1aa6439..fde70868c 100644 --- a/src/pyscipopt/recipes/infeasibilities.py +++ b/src/pyscipopt/recipes/infeasibilities.py @@ -16,7 +16,7 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): for c in model.getConss(): - slack[c.name] = model.addVar(lb=-float("inf"), name=c.name) + slack[c.name] = model.addVar(lb=-float("inf"), name="s_"+c.name) model.addConsCoeff(c, slack[c.name], 1) binary[c.name] = model.addVar(vtype="B") # Binary variable to get minimum infeasible constraints. See PR #857. @@ -26,13 +26,12 @@ def get_infeasible_constraints(orig_model: Model, verbose=False): model.addCons(aux[c.name] >= -slack[c.name]) # modeling aux > 0 => binary = 1 constraint. See https://or.stackexchange.com/q/12142/5352 for an explanation - aux_binary[c.name] = model.addVar(ub=1) + aux_binary[c.name] = model.addVar(vtype="B") model.addCons(binary[c.name]+aux_binary[c.name] == 1) model.addConsSOS1([aux[c.name], aux_binary[c.name]]) model.setObjective(quicksum(binary[c.name] for c in orig_model.getConss())) model.hideOutput() - model.setPresolve(0) # just to be safe, maybe we can use presolving model.optimize() n_infeasibilities_detected = 0