Repository: Pyomo/pyomo

Issue:
fourier_motzkin_elimination needs a doc attribute
You can detect the absence if you try
pyomo help --transformations


Discussion:

Resolving patch:
diff --git a/pyomo/contrib/fme/__init__.py b/pyomo/contrib/fme/__init__.py
--- a/pyomo/contrib/fme/__init__.py
+++ b/pyomo/contrib/fme/__init__.py
@@ -1 +0,0 @@
-import pyomo.contrib.fme.fourier_motzkin_elimination
diff --git a/pyomo/contrib/fme/fourier_motzkin_elimination.py b/pyomo/contrib/fme/fourier_motzkin_elimination.py
--- a/pyomo/contrib/fme/fourier_motzkin_elimination.py
+++ b/pyomo/contrib/fme/fourier_motzkin_elimination.py
@@ -10,7 +10,7 @@
 
 from pyomo.core import (Var, Block, Constraint, Param, Set, Suffix, Expression,
                         Objective, SortComponents, value, ConstraintList)
-from pyomo.core.base import (TransformationFactory, _VarData)
+from pyomo.core.base import TransformationFactory, _VarData
 from pyomo.core.base.block import _BlockData
 from pyomo.core.base.param import _ParamData
 from pyomo.core.base.constraint import _ConstraintData
@@ -20,6 +20,34 @@
 from pyomo.repn.standard_repn import generate_standard_repn
 from pyomo.core.kernel.component_map import ComponentMap
 from pyomo.core.kernel.component_set import ComponentSet
+from pyomo.opt import TerminationCondition
+
+import logging
+
+from six import iteritems
+import inspect
+
+logger = logging.getLogger('pyomo.contrib.fourier_motzkin_elimination')
+
+def _check_var_bounds_filter(constraint):
+    """Check if the constraint is already implied by the variable bounds"""
+    # this is one of our constraints, so we know that it is >=.
+    min_lhs = 0
+    for v, coef in iteritems(constraint['map']):
+        if coef > 0:
+            if v.lb is None:
+                return True # we don't have var bounds with which to imply the
+                            # constraint...
+            min_lhs += coef*v.lb
+        elif coef < 0:
+            if v.ub is None:
+                return True # we don't have var bounds with which to imply the
+                            # constraint...
+            min_lhs += coef*v.ub
+    # we do need value here since we didn't control v.lb and v.ub above.
+    if value(min_lhs) >= constraint['lower']:
+        return False # constraint implied by var bounds
+    return True
 
 def vars_to_eliminate_list(x):
     if isinstance(x, (Var, _VarData)):
@@ -68,6 +96,20 @@ class Fourier_Motzkin_Elimination_Transformation(Transformation):
         Note that these variables must all be continuous and the model must be 
         linear."""
     ))
+    CONFIG.declare('constraint_filtering_callback', ConfigValue(
+        default=_check_var_bounds_filter,
+        description="A callback that determines whether or not new "
+        "constraints generated by Fourier-Motzkin elimination are added "
+        "to the model",
+        doc="""
+        Specify None in order for no constraint filtering to occur during the
+        transformation.
+
+        Specify a function that accepts a constraint (represented in the >= 
+        dictionary form used in this transformation) and returns a Boolean
+        indicating whether or not to add it to the model.
+        """
+    ))
 
     def __init__(self):
         """Initialize transformation object"""
@@ -77,6 +119,7 @@ def _apply_to(self, instance, **kwds):
         config = self.CONFIG(kwds.pop('options', {}))
         config.set_value(kwds)
         vars_to_eliminate = config.vars_to_eliminate
+        self.constraint_filter = config.constraint_filtering_callback
         if vars_to_eliminate is None:
             raise RuntimeError("The Fourier-Motzkin Elimination transformation "
                                "requires the argument vars_to_eliminate, a "
@@ -100,13 +143,13 @@ def _apply_to(self, instance, **kwds):
                 descend_into=Block,
                 sort=SortComponents.deterministic,
                 active=True):
-            if obj.type() in ctypes_not_to_transform:
+            if obj.ctype in ctypes_not_to_transform:
                 continue
-            elif obj.type() is Constraint:
+            elif obj.ctype is Constraint:
                 cons_list = self._process_constraint(obj)
                 constraints.extend(cons_list)
                 obj.deactivate() # the truth will be on our transformation block
-            elif obj.type() is Var:
+            elif obj.ctype is Var:
                 # variable bounds are constraints, but we only need them if this
                 # is a variable we are projecting out
                 if obj not in vars_to_eliminate:
@@ -126,13 +169,23 @@ def _apply_to(self, instance, **kwds):
                     "handle purely algebraic models. That is, only "
                     "Sets, Params, Vars, Constraints, Expressions, Blocks, "
                     "and Objectives may be active on the model." % (obj.name, 
-                                                                    obj.type()))
+                                                                    obj.ctype))
 
         new_constraints = self._fourier_motzkin_elimination(constraints,
                                                             vars_to_eliminate)
 
         # put the new constraints on the transformation block
         for cons in new_constraints:
+            if self.constraint_filter is not None:
+                try:
+                    keep = self.constraint_filter(cons)
+                except:
+                    logger.error("Problem calling constraint filter callback "
+                                 "on constraint with right-hand side %s and "
+                                 "body:\n%s" % (cons['lower'], cons['body']))
+                    raise
+                if not keep:
+                    continue
             body = cons['body']
             lhs = sum(coef*var for (coef, var) in zip(body.linear_coefs,
                                                       body.linear_vars)) + \
@@ -153,7 +206,7 @@ def _apply_to(self, instance, **kwds):
                 projected_constraints.add(lhs >= lower)
 
     def _process_constraint(self, constraint):
-        """Transforms a pyomo Constraint objective into a list of dictionaries
+        """Transforms a pyomo Constraint object into a list of dictionaries
         representing only >= constraints. That is, if the constraint has both an
         ub and a lb, it is transformed into two constraints. Otherwise it is
         flipped if it is <=. Each dictionary contains the keys 'lower',
@@ -163,10 +216,12 @@ def _process_constraint(self, constraint):
         """
         body = constraint.body
         std_repn = generate_standard_repn(body)
-        cons_dict = {'lower': constraint.lower,
+        # make sure that we store the lower bound's value so that we need not
+        # worry again during the transformation
+        cons_dict = {'lower': value(constraint.lower),
                      'body': std_repn
                      }
-        upper = constraint.upper
+        upper = value(constraint.upper)
         constraints_to_add = [cons_dict]
         if upper is not None:
             # if it has both bounds
@@ -191,14 +246,18 @@ def _move_constant_and_add_map(self, cons_dict):
         and moves the constant to the RHS
         """
         body = cons_dict['body']
-        constant = body.constant
+        constant = value(body.constant)
         cons_dict['lower'] -= constant
         body.constant = 0
 
         # store a map of vars to coefficients. We can't use this in place of
         # standard repn because determinism, but this will save a lot of linear
-        # time searches later.
-        cons_dict['map'] = ComponentMap(zip(body.linear_vars, body.linear_coefs))
+        # time searches later. Note also that we will take the value of the
+        # coeficient here so that we never have to worry about it again during
+        # the transformation.
+        cons_dict['map'] = ComponentMap(zip(body.linear_vars, 
+                                            [value(coef) for coef in
+                                             body.linear_coefs]))
 
     def _fourier_motzkin_elimination(self, constraints, vars_to_eliminate):
         """Performs FME on the constraint list in the argument 
@@ -322,3 +381,89 @@ def _add_linear_constraints(self, cons1, cons2):
         ans['lower'] = cons1['lower'] + cons2['lower']
 
         return ans
+
+    def post_process_fme_constraints(self, m, solver_factory, tolerance=0):
+        """Function that solves a sequence of LPs problems to check if
+        constraints are implied by each other. Deletes any that are.
+
+        Parameters
+        ----------------
+        m: A model, already transformed with FME. Note that if constraints 
+           have been added, activated, or deactivated, we will check for 
+           redundancy against the whole active part of the model. If you call 
+           this straight after FME, you are only checking within the projected 
+           constraints, but otherwise it is up to the user.
+        solver_factory: A SolverFactory object (constructed with a solver 
+                        which can solve the continuous relaxation of the 
+                        active constraints on the model. That is, if you 
+                        had nonlinear constraints unrelated to the variables 
+                        being projected, you need to either deactivate them or 
+                        provide a solver which will do the right thing.)
+        tolerance: Tolerance at which we decide a constraint is implied by the
+                   others. Default is 0, meaning we remove the constraint if 
+                   the LP solve finds the constraint can be tight but not 
+                   violated. Setting this to a small positive value would
+                   remove constraints more conservatively. Setting it to a 
+                   negative value would result in a relaxed problem.
+        """
+        # make sure m looks like what we expect
+        if not hasattr(m, "_pyomo_contrib_fme_transformation"):
+            raise RuntimeError("It looks like model %s has not been "
+                               "transformed with the "
+                               "fourier_motzkin_elimination transformation!"
+                               % m.name)
+        transBlock = m._pyomo_contrib_fme_transformation
+        constraints = transBlock.projected_constraints
+    
+        # relax integrality so that we can do this with LP solves.
+        TransformationFactory('core.relax_integer_vars').apply_to(
+            m, transform_deactivated_blocks=True)
+        # deactivate any active objectives on the model, and save what we did so
+        # we can undo it after.
+        active_objs = []
+        for obj in m.component_data_objects(Objective, descend_into=True):
+            if obj.active:
+                active_objs.append(obj)
+            obj.deactivate()
+        # add placeholder for our own objective
+        obj_name = unique_component_name(m, '_fme_post_process_obj')
+        obj = Objective(expr=0)
+        m.add_component(obj_name, obj)
+        for i in constraints:
+            # If someone wants us to ignore it and leave it in the model, we
+            # can.
+            if not constraints[i].active:
+                continue
+            # deactivate the constraint
+            constraints[i].deactivate()
+            m.del_component(obj)
+            # make objective to maximize its infeasibility
+            obj = Objective(expr=constraints[i].body - constraints[i].lower)
+            m.add_component(obj_name, obj)
+            results = solver_factory.solve(m)
+            print(results.solver.termination_condition)
+            if results.solver.termination_condition == \
+               TerminationCondition.unbounded:
+                obj_val = -float('inf')
+            elif results.solver.termination_condition != \
+               TerminationCondition.optimal:
+                raise RuntimeError("Unsuccessful subproblem solve when checking"
+                                   "constraint %s.\n\t"
+                                   "Termination Condition: %s" %
+                                   (constraints[i].name, 
+                                    results.solver.termination_condition))
+            else:
+                obj_val = value(obj)
+            # if we couldn't make it infeasible, it's useless
+            if obj_val >= tolerance:
+                m.del_component(constraints[i])
+                del constraints[i]
+            else:
+                constraints[i].activate()
+
+        # clean up
+        m.del_component(obj)
+        for obj in active_objs:
+            obj.activate()
+        # undo relax integrality
+        TransformationFactory('core.relax_integer_vars').apply_to(m, undo=True)
diff --git a/pyomo/contrib/fme/plugins.py b/pyomo/contrib/fme/plugins.py
--- a/pyomo/contrib/fme/plugins.py
+++ b/pyomo/contrib/fme/plugins.py
@@ -1,7 +1,2 @@
-from pyomo.core.base import TransformationFactory
-from .fourier_motzkin_elimination import \
-    Fourier_Motzkin_Elimination_Transformation
-
 def load():
-    TransformationFactory.register('contrib.fourier_motzkin_elimination')(
-        Fourier_Motzkin_Elimination_Transformation)
+    import pyomo.contrib.fme.fourier_motzkin_elimination


Test patch:
diff --git a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py
--- a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py
+++ b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py
@@ -13,15 +13,23 @@
 currdir = dirname(abspath(__file__))+os.sep
 
 import pyutilib.th as unittest
+from pyomo.common.log import LoggingIntercept
 from pyomo.core import (Var, Constraint, Param, ConcreteModel, NonNegativeReals,
-                        Binary, value, Block)
+                        Binary, value, Block, Objective)
 from pyomo.core.base import TransformationFactory
 from pyomo.core.expr.current import log
 from pyomo.gdp import Disjunction, Disjunct
 from pyomo.repn.standard_repn import generate_standard_repn
 from pyomo.core.kernel.component_set import ComponentSet
+from pyomo.opt import SolverFactory, check_available_solvers
+import pyomo.contrib.fme.fourier_motzkin_elimination
+
+from six import StringIO
+import logging
 import random
 
+solvers = check_available_solvers('glpk')
+
 class TestFourierMotzkinElimination(unittest.TestCase):
     def setUp(self):
         # will need this so we know transformation block names in the test that
@@ -65,10 +73,13 @@ def test_no_vars_specified(self):
             apply_to,
             m)
 
-    def check_projected_constraints(self, m):
+    unfiltered_indices = [1, 2, 3, 6]
+    filtered_indices = [1, 2, 3, 4]
+
+    def check_projected_constraints(self, m, indices):
         constraints = m._pyomo_contrib_fme_transformation.projected_constraints
         # x - 0.01y <= 1
-        cons = constraints[1]
+        cons = constraints[indices[0]]
         self.assertEqual(value(cons.lower), -1)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -82,7 +93,7 @@ def check_projected_constraints(self, m):
         self.assertEqual(coefs[1], 0.01)
 
         # y <= 1000*(1 - u_1)
-        cons = constraints[2]
+        cons = constraints[indices[1]]
         self.assertEqual(value(cons.lower), -1000)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -95,7 +106,7 @@ def check_projected_constraints(self, m):
         self.assertEqual(coefs[1], -1000)
 
         # -x + 0.01y + 1 <= 1000*(1 - u_2)
-        cons = constraints[3]
+        cons = constraints[indices[2]]
         self.assertEqual(value(cons.lower), -999)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -110,7 +121,7 @@ def check_projected_constraints(self, m):
         self.assertEqual(coefs[2], -1000)
 
         # u_2 + 100u_1 >= 1
-        cons = constraints[6]
+        cons = constraints[indices[3]]
         self.assertEqual(value(cons.lower), 1)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -126,27 +137,44 @@ def test_transformed_constraints_indexed_var_arg(self):
         m = self.makeModel()
         TransformationFactory('contrib.fourier_motzkin_elimination').apply_to( 
             m,
-            vars_to_eliminate = m.lamb)
-
+            vars_to_eliminate = m.lamb,
+            constraint_filtering_callback=None)
         # we get some trivial constraints too, but let's check that the ones
         # that should be there really are
-        self.check_projected_constraints(m)
+        self.check_projected_constraints(m, self.unfiltered_indices)
 
     def test_transformed_constraints_varData_list_arg(self):
         m = self.makeModel()
         TransformationFactory('contrib.fourier_motzkin_elimination').apply_to( 
             m,
-            vars_to_eliminate = [m.lamb[1], m.lamb[2]])
+            vars_to_eliminate = [m.lamb[1], m.lamb[2]],
+            constraint_filtering_callback=None)
 
-        self.check_projected_constraints(m)
+        self.check_projected_constraints(m, self.unfiltered_indices)
 
     def test_transformed_constraints_indexedVar_list(self):
         m = self.makeModel()
         TransformationFactory('contrib.fourier_motzkin_elimination').apply_to( 
             m,
-            vars_to_eliminate = [m.lamb])
+            vars_to_eliminate = [m.lamb],
+            constraint_filtering_callback=None)
+
+        self.check_projected_constraints(m, self.unfiltered_indices)
+
+    def test_default_constraint_filtering(self):
+        # We will filter constraints which are trivial based on variable bounds
+        # during the transformation. This checks that we removed the constraints
+        # we expect.
+        m = self.makeModel()
+        TransformationFactory('contrib.fourier_motzkin_elimination').apply_to( 
+            m,
+            vars_to_eliminate = m.lamb)
 
-        self.check_projected_constraints(m)
+        # we still have all the right constraints
+        self.check_projected_constraints(m, self.filtered_indices)
+        # but now we *only* have the right constraints
+        constraints = m._pyomo_contrib_fme_transformation.projected_constraints
+        self.assertEqual(len(constraints), 4)
 
     def test_original_constraints_deactivated(self):
         m = self.makeModel()
@@ -224,6 +252,44 @@ def test_components_we_do_not_understand_error(self):
             m, 
             vars_to_eliminate=m.x)
 
+    def test_bad_constraint_filtering_callback_error(self):
+        m = self.makeModel()
+        def not_a_callback(cons):
+            raise RuntimeError("I don't know how to do my job.")
+        fme = TransformationFactory('contrib.fourier_motzkin_elimination')
+        log = StringIO()
+        with LoggingIntercept(log, 'pyomo.contrib.fourier_motzkin_elimination',
+                              logging.ERROR):
+            self.assertRaisesRegexp(
+                RuntimeError,
+                "I don't know how to do my job.",
+                fme.apply_to,
+                m,
+                vars_to_eliminate=m.x,
+                constraint_filtering_callback=not_a_callback)
+        self.assertRegexpMatches(
+            log.getvalue(),
+            "Problem calling constraint filter callback "
+            "on constraint with right-hand side -1.0 and body:*")
+
+    def test_constraint_filtering_callback_not_callable_error(self):
+        m = self.makeModel()
+        fme = TransformationFactory('contrib.fourier_motzkin_elimination')
+        log = StringIO()
+        with LoggingIntercept(log, 'pyomo.contrib.fourier_motzkin_elimination',
+                              logging.ERROR):
+            self.assertRaisesRegexp(
+                TypeError,
+                "'int' object is not callable",
+                fme.apply_to,
+                m,
+                vars_to_eliminate=m.x,
+                constraint_filtering_callback=5)
+        self.assertRegexpMatches(
+            log.getvalue(),
+            "Problem calling constraint filter callback "
+            "on constraint with right-hand side -1.0 and body:*")
+
     def test_combine_three_inequalities_and_flatten_blocks(self):
         m = ConcreteModel()
         m.x = Var()
@@ -248,50 +314,9 @@ def test_combine_three_inequalities_and_flatten_blocks(self):
         self.assertIsNone(cons.upper)
         self.assertIs(cons.body, m.x)
 
-    def test_project_disaggregated_vars(self):
-        """This is a little bit more of an integration test with GDP, 
-        but also an example of why FME is 'useful.' We will give a GDP, 
-        take chull relaxation, and then project out the disaggregated 
-        variables."""
-
-        m = ConcreteModel()
-        m.p = Var([1, 2], bounds=(0, 10))
-        m.time1 = Disjunction(expr=[m.p[1] >= 1, m.p[1] == 0])
-
-        m.on = Disjunct()
-        m.on.above_min = Constraint(expr=m.p[2] >= 1)
-        m.on.ramping = Constraint(expr=m.p[2] - m.p[1] <= 3)
-        m.on.on_before = Constraint(expr=m.p[1] >= 1)
-
-        m.startup = Disjunct()
-        m.startup.startup_limit = Constraint(expr=(1, m.p[2], 2))
-        m.startup.off_before = Constraint(expr=m.p[1] == 0)
-
-        m.off = Disjunct()
-        m.off.off = Constraint(expr=m.p[2] == 0)
-        m.time2 = Disjunction(expr=[m.on, m.startup, m.off])
-
-        TransformationFactory('gdp.chull').apply_to(m)
-        relaxationBlocks1 = m._pyomo_gdp_chull_relaxation.relaxedDisjuncts
-        relaxationBlocks2 = m._pyomo_gdp_chull_relaxation_4.relaxedDisjuncts
-        disaggregatedVars = ComponentSet(
-            [relaxationBlocks1[0].component("p[1]"),
-             relaxationBlocks1[1].component("p[1]"),
-             relaxationBlocks2[0].component("p[1]"),
-             relaxationBlocks2[0].component("p[2]"),
-             relaxationBlocks2[1].component("p[1]"),
-             relaxationBlocks2[1].component("p[2]"),
-             relaxationBlocks2[2].component("p[1]"),
-             relaxationBlocks2[2].component("p[2]")])
-        TransformationFactory('contrib.fourier_motzkin_elimination').apply_to(
-            m, vars_to_eliminate=disaggregatedVars)
-
-        constraints = m._pyomo_contrib_fme_transformation.projected_constraints
-        # we of course get tremendous amounts of garbage, but we make sure that
-        # what should be here is:
-
+    def check_chull_projected_constraints(self, m, constraints, indices):
         # p[1] >= on.ind_var
-        cons = constraints[22]
+        cons = constraints[indices[0]]
         self.assertEqual(cons.lower, 0)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -304,7 +329,7 @@ def test_project_disaggregated_vars(self):
         self.assertEqual(body.linear_coefs[1], -1)
 
         # p[1] <= 10*on.ind_var + 10*off.ind_var
-        cons = constraints[20]
+        cons = constraints[indices[1]]
         self.assertEqual(cons.lower, 0)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -319,7 +344,7 @@ def test_project_disaggregated_vars(self):
         self.assertEqual(body.linear_coefs[2], -1)
 
         # p[1] >= time1_disjuncts[0].ind_var
-        cons = constraints[58]
+        cons = constraints[indices[2]]
         self.assertEqual(cons.lower, 0)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -332,7 +357,7 @@ def test_project_disaggregated_vars(self):
         self.assertEqual(body.linear_coefs[0], 1)
 
         # p[1] <= 10*time1_disjuncts[0].ind_var
-        cons = constraints[61]
+        cons = constraints[indices[3]]
         self.assertEqual(cons.lower, 0)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -345,7 +370,7 @@ def test_project_disaggregated_vars(self):
         self.assertEqual(body.linear_coefs[1], -1)
 
         # p[2] - p[1] <= 3*on.ind_var + 2*startup.ind_var
-        cons = constraints[56]
+        cons = constraints[indices[4]]
         self.assertEqual(value(cons.lower), 0)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -362,7 +387,7 @@ def test_project_disaggregated_vars(self):
         self.assertEqual(body.linear_coefs[2], 2)
 
         # p[2] >= on.ind_var + startup.ind_var
-        cons = constraints[38]
+        cons = constraints[indices[5]]
         self.assertEqual(cons.lower, 0)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -377,7 +402,7 @@ def test_project_disaggregated_vars(self):
         self.assertEqual(body.linear_coefs[2], -1)
 
         # p[2] <= 10*on.ind_var + 2*startup.ind_var
-        cons = constraints[32]
+        cons = constraints[indices[6]]
         self.assertEqual(cons.lower, 0)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -392,7 +417,7 @@ def test_project_disaggregated_vars(self):
         self.assertEqual(body.linear_coefs[2], -1)
 
         # 1 <= time1_disjuncts[0].ind_var + time_1.disjuncts[1].ind_var
-        cons = constraints[1]
+        cons = constraints[indices[7]]
         self.assertEqual(cons.lower, 1)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -405,7 +430,7 @@ def test_project_disaggregated_vars(self):
         self.assertEqual(body.linear_coefs[1], 1)
 
         # 1 >= time1_disjuncts[0].ind_var + time_1.disjuncts[1].ind_var
-        cons = constraints[2]
+        cons = constraints[indices[8]]
         self.assertEqual(cons.lower, -1)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -418,7 +443,7 @@ def test_project_disaggregated_vars(self):
         self.assertEqual(body.linear_coefs[1], -1)
 
         # 1 <= on.ind_var + startup.ind_var + off.ind_var
-        cons = constraints[4]
+        cons = constraints[indices[9]]
         self.assertEqual(cons.lower, 1)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -433,7 +458,7 @@ def test_project_disaggregated_vars(self):
         self.assertEqual(body.linear_coefs[2], 1)
 
         # 1 >= on.ind_var + startup.ind_var + off.ind_var
-        cons = constraints[5]
+        cons = constraints[indices[10]]
         self.assertEqual(cons.lower, -1)
         self.assertIsNone(cons.upper)
         body = generate_standard_repn(cons.body)
@@ -447,6 +472,105 @@ def test_project_disaggregated_vars(self):
         self.assertIs(body.linear_vars[2], m.off.indicator_var)
         self.assertEqual(body.linear_coefs[2], -1)
 
+    def create_chull_model(self):
+        m = ConcreteModel()
+        m.p = Var([1, 2], bounds=(0, 10))
+        m.time1 = Disjunction(expr=[m.p[1] >= 1, m.p[1] == 0])
+
+        m.on = Disjunct()
+        m.on.above_min = Constraint(expr=m.p[2] >= 1)
+        m.on.ramping = Constraint(expr=m.p[2] - m.p[1] <= 3)
+        m.on.on_before = Constraint(expr=m.p[1] >= 1)
+
+        m.startup = Disjunct()
+        m.startup.startup_limit = Constraint(expr=(1, m.p[2], 2))
+        m.startup.off_before = Constraint(expr=m.p[1] == 0)
+
+        m.off = Disjunct()
+        m.off.off = Constraint(expr=m.p[2] == 0)
+        m.time2 = Disjunction(expr=[m.on, m.startup, m.off])
+
+        m.obj = Objective(expr=m.p[1] + m.p[2])
+
+        chull = TransformationFactory('gdp.chull')
+        chull.apply_to(m)
+        disaggregatedVars = ComponentSet(
+            [chull.get_disaggregated_var(m.p[1], m.time1.disjuncts[0]),
+             chull.get_disaggregated_var(m.p[1], m.time1.disjuncts[1]),
+             chull.get_disaggregated_var(m.p[1], m.on),
+             chull.get_disaggregated_var(m.p[2], m.on),
+             chull.get_disaggregated_var(m.p[1], m.startup),
+             chull.get_disaggregated_var(m.p[2], m.startup),
+             chull.get_disaggregated_var(m.p[1], m.off),
+             chull.get_disaggregated_var(m.p[2], m.off)
+         ])
+                                          
+        # from nose.tools import set_trace
+        # set_trace()
+        # disaggregatedVars = ComponentSet([relaxationBlocks[0].component("p[1]"),
+        #                                   relaxationBlocks[1].component("p[1]"),
+        #                                   relaxationBlocks[2].component("p[1]"),
+        #                                   relaxationBlocks[2].component("p[2]"),
+        #                                   relaxationBlocks[3].component("p[1]"),
+        #                                   relaxationBlocks[3].component("p[2]"),
+        #                                   relaxationBlocks[4].component("p[1]"),
+        #                                   relaxationBlocks[4].component("p[2]")])
+
+        return m, disaggregatedVars
+
+    def test_project_disaggregated_vars(self):
+        """This is a little bit more of an integration test with GDP, 
+        but also an example of why FME is 'useful.' We will give a GDP, 
+        take chull relaxation, and then project out the disaggregated 
+        variables."""
+        m, disaggregatedVars = self.create_chull_model()
+        
+        filtered = TransformationFactory('contrib.fourier_motzkin_elimination').\
+                   create_using(m, vars_to_eliminate=disaggregatedVars)
+        TransformationFactory('contrib.fourier_motzkin_elimination').apply_to(
+            m, vars_to_eliminate=disaggregatedVars,
+            constraint_filtering_callback=None)
+
+        constraints = m._pyomo_contrib_fme_transformation.projected_constraints
+        # we of course get tremendous amounts of garbage, but we make sure that
+        # what should be here is:
+        self.check_chull_projected_constraints(m, constraints, [22, 20, 58, 61,
+                                                                56, 38, 32, 1, 2,
+                                                                4, 5])
+        # and when we filter, it's still there.
+        constraints = filtered._pyomo_contrib_fme_transformation.\
+                      projected_constraints
+        self.check_chull_projected_constraints(filtered, constraints, [6, 5, 16,
+                                                                       17, 15,
+                                                                       11, 8, 1,
+                                                                       2, 3, 4])
+    
+    @unittest.skipIf(not 'glpk' in solvers, 'glpk not available')
+    def test_post_processing(self):
+        m, disaggregatedVars = self.create_chull_model()
+        fme = TransformationFactory('contrib.fourier_motzkin_elimination')
+        fme.apply_to(m, vars_to_eliminate=disaggregatedVars)
+        # post-process
+        fme.post_process_fme_constraints(m, SolverFactory('glpk'))
+
+        constraints = m._pyomo_contrib_fme_transformation.projected_constraints
+        self.assertEqual(len(constraints), 11)
+
+        # They should be the same as the above, but now these are *all* the
+        # constraints
+        self.check_chull_projected_constraints(m, constraints, [6, 5, 16, 17,
+                                                                15, 11, 8, 1, 2,
+                                                                3, 4])
+
+        # and check that we didn't change the model
+        for disj in m.component_data_objects(Disjunct):
+            self.assertIs(disj.indicator_var.domain, Binary)
+        self.assertEqual(len([o for o in m.component_data_objects(Objective)]),
+                         1)
+        self.assertIsInstance(m.component("obj"), Objective)
+        self.assertTrue(m.obj.active)
+        
+    @unittest.skipIf(not 'glpk' in solvers, 'glpk not available')
     def test_model_with_unrelated_nonlinear_expressions(self):
         m = ConcreteModel()
         m.x = Var([1, 2, 3], bounds=(0,3))
@@ -462,8 +586,9 @@ def cons(m, i):
         # This is vacuous, but I just want something that's not quadratic
         m.cons4 = Constraint(expr=m.x[3] <= log(m.y + 1))
 
-        TransformationFactory('contrib.fourier_motzkin_elimination').\
-            apply_to(m, vars_to_eliminate=m.x)
+        fme = TransformationFactory('contrib.fourier_motzkin_elimination')
+        fme.apply_to(m, vars_to_eliminate=m.x,
+                     constraint_filtering_callback=None)
         constraints = m._pyomo_contrib_fme_transformation.projected_constraints
 
         # 0 <= y <= 3
@@ -532,3 +657,27 @@ def cons(m, i):
             for i in constraints:
                 self.assertLessEqual(value(constraints[i].lower),
                                      value(constraints[i].body))
+        m.y.fixed = False
+        m.z.fixed = False
+        
+        # check post process these are non-convex, so I don't want to deal with
+        # it... (and this is a good test that I *don't* deal with it.)
+        constraints[4].deactivate()
+        constraints[3].deactivate()
+        constraints[1].deactivate()
+        # NOTE also that some of the suproblems in this test are unbounded: We
+        # need to keep those constraints.
+        fme.post_process_fme_constraints(m, SolverFactory('glpk'))
+        # we needed all the constraints, so we kept them all
+        self.assertEqual(len(constraints), 6)
+
+        # last check that if someone activates something on the model in
+        # between, we just use it. (I struggle to imagine why you would do this
+        # because why withold the information *during* FME, but if there's some
+        # reason, we may as well use all the information we've got.)
+        m.some_new_cons = Constraint(expr=m.y <= 2)
+        fme.post_process_fme_constraints(m, SolverFactory('glpk'))
+        # now we should have lost one constraint
+        self.assertEqual(len(constraints), 5)
+        # and it should be the y <= 3 one...
+        self.assertIsNone(dict(constraints).get(5))