Repository: Pyomo/pyomo

Issue:
Multiple References to indicator_vars of nested Disjuncts
## Summary

In `pyomo.gdp`, if you transform a model which has a nested disjunction, the transformed model has a `Reference` to the inner-disjunct indicator_vars on both the transformation block of the nested Disjunct and of the parent Disjunct. As a result, of course, `component_data_objects` find this indicator_var twice, which is bad. (I discovered this by confusing a persistent solver.)

### Steps to reproduce the issue

```
from pyomo.environ import *
from pyomo.gdp import *

m = ConcreteModel()
m.x = Var(bounds=(0, 10))
m.y = Var(bounds=(0, 20))

m.disj1 = Disjunct()
m.disj1.disj1 = Disjunct()
m.disj1.disj1.c = Constraint(expr=m.x + m.y <= 18)
m.disj1.disj2 = Disjunct()
m.disj1.disj2.c = Constraint(expr=m.x <= 2)
m.disj1.disjunction = Disjunction(expr=[m.disj1.disj1, m.disj1.disj2])
m.disj2 = Disjunct()
m.disj2.c = Constraint(expr=m.y <= 16)
m.disjunction = Disjunction(expr=[m.disj1, m.disj2])

m.obj = Objective(expr=m.y, sense=maximize)

TransformationFactory('gdp.bigm').apply_to(m)

thevar = m.disj1.disj1.indicator_var
assert m.disj1.transformation_block().localVarReferences.component("disj1._pyomo_gdp_bigm_reformulation.relaxedDisjuncts[0].localVarReferences.disj1.disj1.indicator_var")[None] is thevar
assert m.disj1.disj1.transformation_block().localVarReferences.component("disj1.disj1.indicator_var")[None] is thevar
```

### Error Message

Both assertions in the example above are true. :(

### Information on your system

Pyomo version: main branch
Python version: 3.6.5
Operating system: linux
How Pyomo was installed (PyPI, conda, source): source
Solver (if applicable):


### Additional information

<!-- If you have any additional information, please list it here. -->


<!-- We encourage you to try, as much as possible, to reduce your problem to the minimal example that still reproduces the issue. That would help us a lot in fixing it quickly and effectively!

If you want to ask a question (how to use Pyomo, what it can do, etc.), please refer to our [online documentation](https://github.com/Pyomo/pyomo#getting-help) on ways to get help. -->




Discussion:
@jsiirola I'm not completely sure what to do about this yet--where is the correct place for the `Reference`?

Resolving patch:
diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py
--- a/pyomo/gdp/plugins/bigm.py
+++ b/pyomo/gdp/plugins/bigm.py
@@ -26,12 +26,12 @@
 from pyomo.core.base import Transformation, TransformationFactory, Reference
 import pyomo.core.expr.current as EXPR
 from pyomo.gdp import Disjunct, Disjunction, GDP_Error
-from pyomo.gdp.util import (
-    _warn_for_active_logical_constraint, target_list, is_child_of, get_src_disjunction,
-    get_src_constraint, get_transformed_constraints,
-    _get_constraint_transBlock, get_src_disjunct,
-    _warn_for_active_disjunction,
-    _warn_for_active_disjunct, )
+from pyomo.gdp.util import ( _warn_for_active_logical_constraint, target_list,
+                             is_child_of, get_src_disjunction,
+                             get_src_constraint, get_transformed_constraints,
+                             _get_constraint_transBlock, get_src_disjunct,
+                             _warn_for_active_disjunction,
+                             _warn_for_active_disjunct, )
 from pyomo.repn import generate_standard_repn
 
 from functools import wraps
@@ -467,21 +467,13 @@ def _transform_disjunct(self, obj, transBlock, bigM, arg_list, suffix_list):
 
     def _transform_block_components(self, block, disjunct, bigM, arg_list,
                                     suffix_list):
-        # Find all the variables declared here (including the indicator_var) and
-        # add a reference on the transformation block so these will be
-        # accessible when the Disjunct is deactivated. We don't descend into
-        # Disjuncts because we'll just reference the references which are
-        # already on their transformation blocks.
+        # We find any transformed disjunctions that might be here because we
+        # need to move their transformation blocks up onto the parent block
+        # before we transform anything else on this block. Note that we do this
+        # before we create references to local variables because we do not want
+        # duplicate references to indicator variables and local variables on
+        # nested disjuncts.
         disjunctBlock = disjunct._transformation_block()
-        varRefBlock = disjunctBlock.localVarReferences
-        for v in block.component_objects(Var, descend_into=Block, active=None):
-            varRefBlock.add_component(unique_component_name(
-                varRefBlock, v.getname(fully_qualified=True,
-                                       name_buffer=NAME_BUFFER)), Reference(v))
-
-        # Now need to find any transformed disjunctions that might be here
-        # because we need to move their transformation blocks up onto the parent
-        # block before we transform anything else on this block
         destinationBlock = disjunctBlock.parent_block()
         for obj in block.component_data_objects(
                 Disjunction,
@@ -499,6 +491,17 @@ def _transform_block_components(self, block, disjunct, bigM, arg_list,
             # we leave the transformation block because it still has the XOR
             # constraints, which we want to be on the parent disjunct.
 
+        # Find all the variables declared here (including the indicator_var) and
+        # add a reference on the transformation block so these will be
+        # accessible when the Disjunct is deactivated. We don't descend into
+        # Disjuncts because we just moved the references to their local
+        # variables up in the previous loop.
+        varRefBlock = disjunctBlock.localVarReferences
+        for v in block.component_objects(Var, descend_into=Block, active=None):
+            varRefBlock.add_component(unique_component_name(
+                varRefBlock, v.getname(fully_qualified=True,
+                                       name_buffer=NAME_BUFFER)), Reference(v))
+
         # Now look through the component map of block and transform everything
         # we have a handler for. Yell if we don't know how to handle it. (Note
         # that because we only iterate through active components, this means
@@ -555,7 +558,8 @@ def _warn_for_active_disjunct(self, innerdisjunct, outerdisjunct, bigMargs,
 
     def _warn_for_active_logical_statement(
             self, logical_statment, disjunct, infodict, bigMargs, suffix_list):
-        _warn_for_active_logical_constraint(logical_statment, disjunct, NAME_BUFFER)
+        _warn_for_active_logical_constraint(logical_statment, disjunct,
+                                            NAME_BUFFER)
 
     def _transform_block_on_disjunct(self, block, disjunct, bigMargs, arg_list,
                                      suffix_list):
diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py
--- a/pyomo/gdp/plugins/hull.py
+++ b/pyomo/gdp/plugins/hull.py
@@ -673,21 +673,13 @@ def _transform_block_components( self, block, disjunct, var_substitute_map,
         # As opposed to bigm, in hull the only special thing we need to do for
         # nested Disjunctions is to make sure that we move up local var
         # references and also references to the disaggregated variables so that
-        # all will be accessible after we transform this Disjunct.The indicator
+        # all will be accessible after we transform this Disjunct. The indicator
         # variables and disaggregated variables of the inner disjunction will
         # need to be disaggregated again, but the transformed constraints will
         # not be. But this way nothing will get double-bigm-ed. (If an
         # untransformed disjunction is lurking here, we will catch it below).
 
-        # add references to all local variables on block (including the
-        # indicator_var)
         disjunctBlock = disjunct._transformation_block()
-        varRefBlock = disjunctBlock.localVarReferences
-        for v in block.component_objects(Var, descend_into=Block, active=None):
-            varRefBlock.add_component(unique_component_name(
-                varRefBlock, v.getname(fully_qualified=True,
-                                       name_buffer=NAME_BUFFER)), Reference(v))
-
         destinationBlock = disjunctBlock.parent_block()
         for obj in block.component_data_objects(
                 Disjunction,
@@ -702,6 +694,16 @@ def _transform_block_components( self, block, disjunct, var_substitute_map,
 
             self._transfer_var_references(transBlock, destinationBlock)
 
+        # add references to all local variables on block (including the
+        # indicator_var). Note that we do this after we have moved up the
+        # transformation blocks for nested disjunctions, so that we don't have
+        # duplicate references.
+        varRefBlock = disjunctBlock.localVarReferences
+        for v in block.component_objects(Var, descend_into=Block, active=None):
+            varRefBlock.add_component(unique_component_name(
+                varRefBlock, v.getname(fully_qualified=True,
+                                       name_buffer=NAME_BUFFER)), Reference(v))
+
         # Look through the component map of block and transform everything we
         # have a handler for. Yell if we don't know how to handle it. (Note that
         # because we only iterate through active components, this means


Test patch:
diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py
--- a/pyomo/gdp/tests/common_tests.py
+++ b/pyomo/gdp/tests/common_tests.py
@@ -9,7 +9,9 @@
 #  ___________________________________________________________________________
 
 
-from pyomo.environ import TransformationFactory, ConcreteModel, Constraint, Var, Objective, Block, Any, RangeSet, Expression, value
+from pyomo.environ import (TransformationFactory, ConcreteModel, Constraint,
+                           Var, Objective, Block, Any, RangeSet, Expression,
+                           value)
 from pyomo.gdp import Disjunct, Disjunction, GDP_Error
 from pyomo.core.base import constraint, ComponentUID
 from pyomo.repn import generate_standard_repn
@@ -68,8 +70,8 @@ def checkb0TargetsInactive(self, m):
 
 def checkb0TargetsTransformed(self, m, transformation):
     trans = TransformationFactory('gdp.%s' % transformation)
-    disjBlock = m.b[0].component("_pyomo_gdp_%s_reformulation" % transformation).\
-                relaxedDisjuncts
+    disjBlock = m.b[0].component(
+        "_pyomo_gdp_%s_reformulation" % transformation).relaxedDisjuncts
     self.assertEqual(len(disjBlock), 2)
     self.assertIsInstance(disjBlock[0].component("b[0].disjunct[0].c"),
                           Constraint)
@@ -743,15 +745,15 @@ def check_indexedBlock_only_targets_transformed(self, transformation):
         m,
         targets=[m.b])
 
-    disjBlock1 = m.b[0].component("_pyomo_gdp_%s_reformulation" % transformation).\
-                 relaxedDisjuncts
+    disjBlock1 = m.b[0].component(
+        "_pyomo_gdp_%s_reformulation" % transformation).relaxedDisjuncts
     self.assertEqual(len(disjBlock1), 2)
     self.assertIsInstance(disjBlock1[0].component("b[0].disjunct[0].c"),
                           Constraint)
     self.assertIsInstance(disjBlock1[1].component("b[0].disjunct[1].c"),
                           Constraint)
-    disjBlock2 = m.b[1].component("_pyomo_gdp_%s_reformulation" % transformation).\
-                 relaxedDisjuncts
+    disjBlock2 = m.b[1].component(
+        "_pyomo_gdp_%s_reformulation" % transformation).relaxedDisjuncts
     self.assertEqual(len(disjBlock2), 2)
     self.assertIsInstance(disjBlock2[0].component("b[1].disjunct0.c"),
                           Constraint)
@@ -893,7 +895,8 @@ def check_trans_block_created(self, transformation):
     self.assertIsInstance(disjBlock, Block)
     self.assertEqual(len(disjBlock), 2)
     # and that it didn't get created on the model
-    self.assertIsNone(m.component('_pyomo_gdp_%s_reformulation' % transformation))
+    self.assertIsNone(
+        m.component('_pyomo_gdp_%s_reformulation' % transformation))
 
 
 # disjunction generation tests: These all suppose that you are doing some sort
@@ -1048,7 +1051,8 @@ def check_transformation_simple_block(self, transformation):
     TransformationFactory('gdp.%s' % transformation).apply_to(m.b)
 
     # transformation block not on m
-    self.assertIsNone(m.component("_pyomo_gdp_%s_reformulation" % transformation))
+    self.assertIsNone(
+        m.component("_pyomo_gdp_%s_reformulation" % transformation))
 
     # transformation block on m.b
     self.assertIsInstance(m.b.component("_pyomo_gdp_%s_reformulation" %
@@ -1058,7 +1062,8 @@ def check_transform_block_data(self, transformation):
     m = models.makeDisjunctionsOnIndexedBlock()
     TransformationFactory('gdp.%s' % transformation).apply_to(m.b[0])
 
-    self.assertIsNone(m.component("_pyomo_gdp_%s_reformulation" % transformation))
+    self.assertIsNone(
+        m.component("_pyomo_gdp_%s_reformulation" % transformation))
 
     self.assertIsInstance(m.b[0].component("_pyomo_gdp_%s_reformulation" %
                                            transformation), Block)
@@ -1068,7 +1073,8 @@ def check_simple_block_target(self, transformation):
     TransformationFactory('gdp.%s' % transformation).apply_to(m, targets=[m.b])
 
     # transformation block not on m
-    self.assertIsNone(m.component("_pyomo_gdp_%s_reformulation" % transformation))
+    self.assertIsNone(
+        m.component("_pyomo_gdp_%s_reformulation" % transformation))
 
     # transformation block on m.b
     self.assertIsInstance(m.b.component("_pyomo_gdp_%s_reformulation" %
@@ -1079,7 +1085,8 @@ def check_block_data_target(self, transformation):
     TransformationFactory('gdp.%s' % transformation).apply_to(m,
                                                               targets=[m.b[0]])
 
-    self.assertIsNone(m.component("_pyomo_gdp_%s_reformulation" % transformation))
+    self.assertIsNone(
+        m.component("_pyomo_gdp_%s_reformulation" % transformation))
 
     self.assertIsInstance(m.b[0].component("_pyomo_gdp_%s_reformulation" %
                                            transformation), Block)
@@ -1091,7 +1098,8 @@ def check_indexed_block_target(self, transformation):
     # We expect the transformation block on each of the BlockDatas. Because
     # it is always going on the parent block of the disjunction.
 
-    self.assertIsNone(m.component("_pyomo_gdp_%s_reformulation" % transformation))
+    self.assertIsNone(
+        m.component("_pyomo_gdp_%s_reformulation" % transformation))
 
     for i in [0,1]:
         self.assertIsInstance( m.b[i].component("_pyomo_gdp_%s_reformulation" %
@@ -1497,6 +1505,20 @@ def check_disjunctData_only_targets_transformed(self, transformation):
         self.assertIs(m.disjunct[1].innerdisjunct[i].transformation_block(),
                       disjBlock[j])
 
+def check_unique_reference_to_nested_indicator_var(self, transformation):
+    m = models.makeNestedDisjunctions_NestedDisjuncts()
+    TransformationFactory('gdp.%s' % transformation).apply_to(m)
+    # find the references to the nested indicator var
+    num_references_d3 = 0
+    num_references_d4 = 0
+    for v in m.component_data_objects(Var, active=True, descend_into=Block):
+        if v is m.d1.d3.indicator_var:
+            num_references_d3 += 1
+        if v is m.d1.d4.indicator_var:
+            num_references_d4 += 1
+    self.assertEqual(num_references_d3, 1)
+    self.assertEqual(num_references_d4, 1)
+
 # checks for handling of benign types that could be on disjuncts we're
 # transforming
 
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
@@ -1959,6 +1959,9 @@ def test_transformed_constraints(self):
         self.assertEqual(cons8.upper, 2)
         self.check_bigM_constraint(cons8, m.a, 21, m.disjunct[1].indicator_var)
 
+    def test_unique_reference_to_nested_indicator_var(self):
+        ct.check_unique_reference_to_nested_indicator_var(self, 'bigm')
+
     def test_disjunct_targets_inactive(self):
         ct.check_disjunct_targets_inactive(self, 'bigm')
 
diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py
--- a/pyomo/gdp/tests/test_hull.py
+++ b/pyomo/gdp/tests/test_hull.py
@@ -1060,6 +1060,9 @@ def test_mappings_between_disjunctions_and_xors(self):
         # version. (But this behaves the same as bigm)
         ct.check_mappings_between_disjunctions_and_xors(self, 'hull')
 
+    def test_unique_reference_to_nested_indicator_var(self):
+        ct.check_unique_reference_to_nested_indicator_var(self, 'hull')
+
     def test_disjunct_targets_inactive(self):
         ct.check_disjunct_targets_inactive(self, 'hull')