Repository: Pyomo/pyomo

Issue:
Pyomo crashes when solver returns suffix but no value for variables
When the user requests an irreducibly infeasible set of constraints (IIS) for an infeasible mixed-integer program, cplexamp sometimes returns the IIS but not values for the variables. This causes pyomo to crash while reading the solution. Specifically, `pyomo.opt.plugins.sol` only creates entries in `soln_variable` for variables whose values have been returned by the solver, but then it tries to attach the IIS suffix value to a non-existent item in `soln_variable`, causing an uncaught `KeyError()`.

The code below illustrates this problem:

    from pyomo.environ import *
    m = ConcreteModel()
    m.x1 = Var(within=NonNegativeReals)
    m.x2 = Var(within=NonNegativeIntegers)
    m.constraint = Constraint(rule=lambda m: m.x1 + m.x2 <= -1)
    m.objective = Objective(rule=lambda m: m.x1 + m.x2, sense=minimize)
    m.iis = Suffix(direction=Suffix.IMPORT)
    solver = SolverFactory('cplexamp')
    res = solver.solve(m, options_string='iisfind=1', tee=True)
    print("IIS:")
    print("\n".join(sorted(c.name for c in m.iis)))

I have added a pull request to fix this: https://github.com/Pyomo/pyomo/pull/596


Discussion:

Resolving patch:
diff --git a/pyomo/opt/plugins/sol.py b/pyomo/opt/plugins/sol.py
--- a/pyomo/opt/plugins/sol.py
+++ b/pyomo/opt/plugins/sol.py
@@ -2,8 +2,8 @@
 #
 #  Pyomo: Python Optimization Modeling Objects
 #  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
-#  Under the terms of Contract DE-NA0003525 with National Technology and 
-#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 
+#  Under the terms of Contract DE-NA0003525 with National Technology and
+#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
 #  rights in this software.
 #  This software is distributed under the 3-clause BSD License.
 #  ___________________________________________________________________________
@@ -213,7 +213,10 @@ def _load(self, fin, res, soln, suffixes):
                     if kind == 0: # Var
                         for cnt in xrange(nvalues):
                             suf_line = fin.readline().split()
-                            soln_variable["v"+suf_line[0]][suffix_name] = \
+                            key = "v"+suf_line[0]
+                            if key not in soln_variable:
+                                soln_variable[key] = {}
+                            soln_variable[key][suffix_name] = \
                                 convert_function(suf_line[1])
                     elif kind == 1: # Con
                         for cnt in xrange(nvalues):
@@ -221,9 +224,14 @@ def _load(self, fin, res, soln, suffixes):
                             key = "c"+suf_line[0]
                             if key not in soln_constraint:
                                 soln_constraint[key] = {}
-                            # convert the first letter of the suffix name to upper case,
-                            # mainly for pretty-print / output purposes. these are lower-cased
-                            # when loaded into real suffixes, so it is largely redundant.
+                            # GH: About the comment below: This makes for a
+                            # confusing results object and more confusing tests.
+                            # We should not muck with the names of suffixes
+                            # coming out of the sol file.
+                            #
+                            #   convert the first letter of the suffix name to upper case,
+                            #   mainly for pretty-print / output purposes. these are lower-cased
+                            #   when loaded into real suffixes, so it is largely redundant.
                             translated_suffix_name = suffix_name[0].upper() + suffix_name[1:]
                             soln_constraint[key][translated_suffix_name] = \
                                 convert_function(suf_line[1])


Test patch:
diff --git a/pyomo/opt/tests/base/iis_no_variable_values.sol b/pyomo/opt/tests/base/iis_no_variable_values.sol
new file mode 100644
--- /dev/null
+++ b/pyomo/opt/tests/base/iis_no_variable_values.sol
@@ -0,0 +1,34 @@
+CPLEX 12.8.0.0: integer infeasible.
+0 MIP simplex iterations
+0 branch-and-bound nodes
+Returning an IIS of 2 variables and 1 constraints.
+No basis.
+
+Options
+3
+1
+1
+0
+1
+0
+2
+0
+objno 0 220
+suffix 0 2 4 181 11
+iis
+
+0	non	not in the iis
+1	low	at lower bound
+2	fix	fixed
+3	upp	at upper bound
+4	mem	member
+5	pmem	possible member
+6	plow	possibly at lower bound
+7	pupp	possibly at upper bound
+8	bug
+
+0 1
+1 1
+suffix 1 1 4 0 0
+iis
+0 4
diff --git a/pyomo/opt/tests/base/test_sol.py b/pyomo/opt/tests/base/test_sol.py
--- a/pyomo/opt/tests/base/test_sol.py
+++ b/pyomo/opt/tests/base/test_sol.py
@@ -2,8 +2,8 @@
 #
 #  Pyomo: Python Optimization Modeling Objects
 #  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
-#  Under the terms of Contract DE-NA0003525 with National Technology and 
-#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 
+#  Under the terms of Contract DE-NA0003525 with National Technology and
+#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
 #  rights in this software.
 #  This software is distributed under the 3-clause BSD License.
 #  ___________________________________________________________________________
@@ -106,5 +106,34 @@ def test_bad_objnoline(self):
             with self.assertRaises(ValueError):
                 soln = reader(currdir+"bad_objnoline.sol")
 
+    def test_iis_no_variable_values(self):
+        with pyomo.opt.ReaderFactory("sol") as reader:
+            if reader is None:
+                raise IOError("Reader 'sol' is not registered")
+            result = reader(currdir+"iis_no_variable_values.sol", suffixes=["iis"])
+            soln = result.solution(0)
+            self.assertEqual(len(list(soln.variable['v0'].keys())), 1)
+            self.assertEqual(soln.variable['v0']['iis'], 1)
+            self.assertEqual(len(list(soln.variable['v1'].keys())), 1)
+            self.assertEqual(soln.variable['v1']['iis'], 1)
+            self.assertEqual(len(list(soln.constraint['c0'].keys())), 1)
+            self.assertEqual(soln.constraint['c0']['Iis'], 4)
+            import pyomo.kernel as pmo
+            m = pmo.block()
+            m.v0 = pmo.variable()
+            m.v1 = pmo.variable()
+            m.c0 = pmo.constraint()
+            m.iis = pmo.suffix(direction=pmo.suffix.IMPORT)
+            from pyomo.core.expr.symbol_map import SymbolMap
+            soln.symbol_map = SymbolMap()
+            soln.symbol_map.addSymbol(m.v0, 'v0')
+            soln.symbol_map.addSymbol(m.v1, 'v1')
+            soln.symbol_map.addSymbol(m.c0, 'c0')
+            m.load_solution(soln)
+            pmo.pprint(m.iis)
+            self.assertEqual(m.iis[m.v0], 1)
+            self.assertEqual(m.iis[m.v1], 1)
+            self.assertEqual(m.iis[m.c0], 4)
+
 if __name__ == "__main__":
     unittest.main()