Repository: Pyomo/pyomo

Issue:
Appropriate behavior for activate() and deactivate() for nested disjunctions?
```
from pyomo.environ import *
m = ConcreteModel()
m.d1 = Disjunct()
m.d2 = Disjunct()
m.d1.sub1 = Disjunct()
m.d1.sub2 = Disjunct()
m.d1.disj = Disjunction(expr=[m.d1.sub1, m.d1.sub2])
m.disj = Disjunction(expr=[m.d1, m.d2])
```

What should happen when disjuncts containing nested disjunctions are deactivated? `m.d1.deactivate()`.
The reclassifier hack complains about `m.d1.sub1` and `m.d1.sub2` not being expanded.


Discussion:

Resolving patch:
diff --git a/pyomo/gdp/plugins/gdp_var_mover.py b/pyomo/gdp/plugins/gdp_var_mover.py
--- a/pyomo/gdp/plugins/gdp_var_mover.py
+++ b/pyomo/gdp/plugins/gdp_var_mover.py
@@ -18,7 +18,9 @@
 import textwrap
 from pyomo.common.plugin import alias
 from pyomo.core.base import Transformation, Block, Constraint
-from pyomo.gdp import Disjunct
+from pyomo.gdp import Disjunct, GDP_Error
+from pyomo.core import TraversalStrategy
+from pyomo.common.deprecation import deprecated
 
 from six import itervalues
 
@@ -35,6 +37,8 @@ class HACK_GDP_Var_Mover(Transformation):
 
     alias('gdp.varmover', doc=textwrap.fill(textwrap.dedent(__doc__.strip())))
 
+    @deprecated(msg="The gdp.varmover transformation has been deprecated in "
+                "favor of the gdp.reclassify transformation.")
     def _apply_to(self, instance, **kwds):
         assert not kwds
         count = 0
@@ -61,13 +65,19 @@ class HACK_GDP_Disjunct_Reclassifier(Transformation):
           doc=textwrap.fill(textwrap.dedent(__doc__.strip())))
 
     def _apply_to(self, instance, **kwds):
-        assert not kwds
+        assert not kwds  # no keywords expected to the transformation
         disjunct_generator = instance.component_objects(
-            Disjunct, descend_into=(Block, Disjunct))
+            Disjunct, descend_into=(Block, Disjunct),
+            descent_order=TraversalStrategy.PostfixDFS)
         for disjunct_component in disjunct_generator:
+            # Check that the disjuncts being reclassified are all relaxed or
+            # are not on an active block.
             for disjunct in itervalues(disjunct_component._data):
-                if disjunct.active:
-                    logger.error("""
+                if (disjunct.active and
+                        self._disjunct_not_relaxed(disjunct) and
+                        self._disjunct_on_active_block(disjunct) and
+                        self._disjunct_not_fixed_true(disjunct)):
+                    raise GDP_Error("""
                     Reclassifying active Disjunct "%s" as a Block.  This
                     is generally an error as it indicates that the model
                     was not completely relaxed before applying the
@@ -91,3 +101,31 @@ def _apply_to(self, instance, **kwds):
                     Constraint, descend_into=Block, active=True)
                 for con in cons_in_disjunct:
                     con.deactivate()
+
+    def _disjunct_not_fixed_true(self, disjunct):
+        # Return true if the disjunct indicator variable is not fixed to True
+        return not (disjunct.indicator_var.fixed and
+                    disjunct.indicator_var.value == 1)
+
+    def _disjunct_not_relaxed(self, disjunct):
+        # Return True if the disjunct was not relaxed by a transformation.
+        return not getattr(
+            disjunct, '_gdp_transformation_info', {}).get('relaxed', False)
+
+    def _disjunct_on_active_block(self, disjunct):
+        # Check first to make sure that the disjunct is not a
+        # descendent of an inactive Block or fixed and deactivated
+        # Disjunct, before raising a warning.
+        parent_block = disjunct.parent_block()
+        while parent_block is not None:
+            if parent_block.type() is Block and not parent_block.active:
+                return False
+            elif (parent_block.type() is Disjunct and not parent_block.active
+                  and parent_block.indicator_var.value == 0
+                  and parent_block.indicator_var.fixed):
+                return False
+            else:
+                # Step up one level in the hierarchy
+                parent_block = parent_block.parent_block()
+                continue
+        return True


Test patch:
diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py
--- a/pyomo/gdp/tests/models.py
+++ b/pyomo/gdp/tests/models.py
@@ -63,10 +63,9 @@ def c_rule(d, s):
     m.b.disjunction = Disjunction(expr=[m.b.simpledisj1, m.b.simpledisj2])
     return m
 
-# same concept as above, but bounded variables
-
 
 def makeTwoTermDisj_IndexedConstraints_BoundedVars():
+    # same concept as above, but bounded variables
     m = ConcreteModel()
     m.s = Set(initialize=[1, 2])
     m.lbs = Param(m.s, initialize={1: 2, 2: 4})
@@ -372,6 +371,32 @@ def d2_rule(disjunct, flag):
             disjunct.c = Constraint(expr=m.a == 100)
     m.disjunct2 = Disjunct([0, 1], rule=d2_rule)
 
+    m.disjunction1 = Disjunction(expr=[m.disjunct1[0], m.disjunct1[1]])
+    m.disjunction2 = Disjunction(expr=[m.disjunct2[0], m.disjunct1[1]])
+    # Deactivate unused disjunct like we are supposed to
+    m.disjunct2[1].deactivate()
+    return m
+
+
+def makeDisjunctInMultipleDisjunctions_no_deactivate():
+    m = ConcreteModel()
+    m.a = Var(bounds=(-10, 50))
+
+    def d1_rule(disjunct, flag):
+        m = disjunct.model()
+        if flag:
+            disjunct.c = Constraint(expr=m.a == 0)
+        else:
+            disjunct.c = Constraint(expr=m.a >= 5)
+    m.disjunct1 = Disjunct([0, 1], rule=d1_rule)
+
+    def d2_rule(disjunct, flag):
+        if not flag:
+            disjunct.c = Constraint(expr=m.a >= 30)
+        else:
+            disjunct.c = Constraint(expr=m.a == 100)
+    m.disjunct2 = Disjunct([0, 1], rule=d2_rule)
+
     m.disjunction1 = Disjunction(expr=[m.disjunct1[0], m.disjunct1[1]])
     m.disjunction2 = Disjunction(expr=[m.disjunct2[0], m.disjunct1[1]])
     return m
diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py
--- a/pyomo/gdp/tests/test_bigm.py
+++ b/pyomo/gdp/tests/test_bigm.py
@@ -1273,14 +1273,22 @@ def test_untransformed_disj_active(self):
         # We have an extra disjunct not in any of the disjunctions.
         # He doesn't get transformed, and so he should still be active
         # so the writers will scream. His constraint, also, is still active.
-        m = models.makeDisjunctInMultipleDisjunctions()
-        TransformationFactory('gdp.bigm').apply_to(m, targets=(m,))
+        m = models.makeDisjunctInMultipleDisjunctions_no_deactivate()
+        TransformationFactory('gdp.bigm').apply_to(m, targets=(
+            m.disjunction1, m.disjunction2))
 
         self.assertTrue(m.disjunct2[1].active)
         self.assertTrue(m.disjunct2[1].c.active)
         # and it means his container is active
         self.assertTrue(m.disjunct2.active)
 
+    def test_untransformed_disj_error(self):
+        # If we try to transform the whole model without deactivating the extra
+        # disjunct, the reclassify transformation should complain.
+        m = models.makeDisjunctInMultipleDisjunctions_no_deactivate()
+        with self.assertRaises(GDP_Error):
+            TransformationFactory('gdp.bigm').apply_to(m)
+
     def test_transformation_block_structure(self):
         m = models.makeDisjunctInMultipleDisjunctions()
         TransformationFactory('gdp.bigm').apply_to(m)
@@ -1416,7 +1424,7 @@ def test_create_using(self):
 
 class TestTargets_SingleDisjunction(unittest.TestCase, CommonTests):
     def test_only_targets_inactive(self):
-        m = models.makeDisjunctInMultipleDisjunctions()
+        m = models.makeDisjunctInMultipleDisjunctions_no_deactivate()
         TransformationFactory('gdp.bigm').apply_to(
             m,
             targets=[ComponentUID(m.disjunction1)])
diff --git a/pyomo/gdp/tests/test_reclassify.py b/pyomo/gdp/tests/test_reclassify.py
new file mode 100644
--- /dev/null
+++ b/pyomo/gdp/tests/test_reclassify.py
@@ -0,0 +1,70 @@
+# -*- coding: UTF-8 -*-
+"""Tests disjunct reclassifier transformation."""
+import pyutilib.th as unittest
+from pyomo.core import (Block, ConcreteModel, TransformationFactory)
+from pyomo.gdp import Disjunct, Disjunction, GDP_Error
+from pyomo.gdp.plugins.gdp_var_mover import HACK_GDP_Disjunct_Reclassifier
+from pyomo.gdp.plugins import bigm
+
+
+class TestDisjunctReclassify(unittest.TestCase):
+    """Tests reclassification of disjuncts."""
+
+    def test_deactivated_parent_disjunct(self):
+        m = ConcreteModel()
+        m.d1 = Disjunct()
+        m.d1.sub1 = Disjunct()
+        m.d1.sub2 = Disjunct()
+        m.d1.disj = Disjunction(expr=[m.d1.sub1, m.d1.sub2])
+        m.d1.deactivate()
+        TransformationFactory('gdp.reclassify').apply_to(m)
+        self.assertIs(m.d1.type(), Block)
+        self.assertIs(m.d1.sub1.type(), Block)
+        self.assertIs(m.d1.sub2.type(), Block)
+
+    def test_deactivated_parent_block(self):
+        m = ConcreteModel()
+        m.d1 = Block()
+        m.d1.sub1 = Disjunct()
+        m.d1.sub2 = Disjunct()
+        m.d1.disj = Disjunction(expr=[m.d1.sub1, m.d1.sub2])
+        m.d1.deactivate()
+        TransformationFactory('gdp.reclassify').apply_to(m)
+        self.assertIs(m.d1.type(), Block)
+        self.assertIs(m.d1.sub1.type(), Block)
+        self.assertIs(m.d1.sub2.type(), Block)
+
+    def test_active_parent_disjunct(self):
+        m = ConcreteModel()
+        m.d1 = Disjunct()
+        m.d1.sub1 = Disjunct()
+        m.d1.sub2 = Disjunct()
+        m.d1.disj = Disjunction(expr=[m.d1.sub1, m.d1.sub2])
+        with self.assertRaises(GDP_Error):
+            TransformationFactory('gdp.reclassify').apply_to(m)
+
+    def test_active_parent_disjunct_target(self):
+        m = ConcreteModel()
+        m.d1 = Disjunct()
+        m.d1.sub1 = Disjunct()
+        m.d1.sub2 = Disjunct()
+        m.d1.disj = Disjunction(expr=[m.d1.sub1, m.d1.sub2])
+        TransformationFactory('gdp.bigm').apply_to(m, targets=m.d1.disj)
+        m.d1.indicator_var.fix(1)
+        TransformationFactory('gdp.reclassify').apply_to(m)
+        self.assertIs(m.d1.type(), Block)
+        self.assertIs(m.d1.sub1.type(), Block)
+        self.assertIs(m.d1.sub2.type(), Block)
+
+    def test_active_parent_block(self):
+        m = ConcreteModel()
+        m.d1 = Block()
+        m.d1.sub1 = Disjunct()
+        m.d1.sub2 = Disjunct()
+        m.d1.disj = Disjunction(expr=[m.d1.sub1, m.d1.sub2])
+        with self.assertRaises(GDP_Error):
+            TransformationFactory('gdp.reclassify').apply_to(m)
+
+
+if __name__ == '__main__':
+    unittest.main()