Repository: Pyomo/pyomo

Issue:
Cannot clone individual blocks within an indexed block
## Summary

`_BlockData` does not have a `_parent` attribute, so it cannot be cloned.

<!-- Explain, in a clear and concise way, the command you ran and the result you were trying to achieve.
Example: "I ran `pyomo solve` to optimize a model and ..." -->

### Steps to reproduce the issue

```
In [1]: import pyomo.environ as pe

In [2]: m = pe.ConcreteModel()

In [3]: m.b = pe.Block([1,2,3])

In [4]: m.b[1].clone()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-bb20e18a9495> in <module>
----> 1 m.b[1].clone()

~/repos/pyomo/pyomo/core/base/block.py in clone(self)
   1232         # should be preserved as singletons.
   1233         #
-> 1234         save_parent, self._parent = self._parent, None
   1235         try:
   1236             new_block = copy.deepcopy(

~/repos/pyomo/pyomo/core/base/block.py in __getattr__(self, val)
    522         # throw the "normal" AttributeError
    523         raise AttributeError("'%s' object has no attribute '%s'"
--> 524                              % (self.__class__.__name__, val))
    525
    526     def __setattr__(self, name, val):

AttributeError: '_BlockData' object has no attribute '_parent'
```

<!-- Please include the command run and/or provide an example script that demonstrates the problem. -->

Cannot clone individual blocks within an indexed block
## Summary

`_BlockData` does not have a `_parent` attribute, so it cannot be cloned.

<!-- Explain, in a clear and concise way, the command you ran and the result you were trying to achieve.
Example: "I ran `pyomo solve` to optimize a model and ..." -->

### Steps to reproduce the issue

```
In [1]: import pyomo.environ as pe

In [2]: m = pe.ConcreteModel()

In [3]: m.b = pe.Block([1,2,3])

In [4]: m.b[1].clone()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-bb20e18a9495> in <module>
----> 1 m.b[1].clone()

~/repos/pyomo/pyomo/core/base/block.py in clone(self)
   1232         # should be preserved as singletons.
   1233         #
-> 1234         save_parent, self._parent = self._parent, None
   1235         try:
   1236             new_block = copy.deepcopy(

~/repos/pyomo/pyomo/core/base/block.py in __getattr__(self, val)
    522         # throw the "normal" AttributeError
    523         raise AttributeError("'%s' object has no attribute '%s'"
--> 524                              % (self.__class__.__name__, val))
    525
    526     def __setattr__(self, name, val):

AttributeError: '_BlockData' object has no attribute '_parent'
```

<!-- Please include the command run and/or provide an example script that demonstrates the problem. -->

Cannot clone individual blocks within an indexed block
## Summary

`_BlockData` does not have a `_parent` attribute, so it cannot be cloned.

<!-- Explain, in a clear and concise way, the command you ran and the result you were trying to achieve.
Example: "I ran `pyomo solve` to optimize a model and ..." -->

### Steps to reproduce the issue

```
In [1]: import pyomo.environ as pe

In [2]: m = pe.ConcreteModel()

In [3]: m.b = pe.Block([1,2,3])

In [4]: m.b[1].clone()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-bb20e18a9495> in <module>
----> 1 m.b[1].clone()

~/repos/pyomo/pyomo/core/base/block.py in clone(self)
   1232         # should be preserved as singletons.
   1233         #
-> 1234         save_parent, self._parent = self._parent, None
   1235         try:
   1236             new_block = copy.deepcopy(

~/repos/pyomo/pyomo/core/base/block.py in __getattr__(self, val)
    522         # throw the "normal" AttributeError
    523         raise AttributeError("'%s' object has no attribute '%s'"
--> 524                              % (self.__class__.__name__, val))
    525
    526     def __setattr__(self, name, val):

AttributeError: '_BlockData' object has no attribute '_parent'
```

<!-- Please include the command run and/or provide an example script that demonstrates the problem. -->



Discussion:
@michaelbynum: what return type would you expect for `m.b[1].clone()`?  The `clone()` infrastructure (since it is based on `deepcopy()`) would give you a `_BlockData`: but that raises another pickle, as the current Pyomo design won't pick up a bare `_BlockData` on a model.  That is `m.c = m.b[1].clone()` would result in `type(m.c) == _BlockData` (and not `ScalarBlock`), so it wouldn't be picked up / indexed by the `PseudoMap` that stores the `m` Block's components.

Should we add special handling so that  cloning `_BlockData` returns a `ScalarBlock`?
Excellent question. I'm not sure. Returning a `ScalarBlock` might make sense.
@michaelbynum: what return type would you expect for `m.b[1].clone()`?  The `clone()` infrastructure (since it is based on `deepcopy()`) would give you a `_BlockData`: but that raises another pickle, as the current Pyomo design won't pick up a bare `_BlockData` on a model.  That is `m.c = m.b[1].clone()` would result in `type(m.c) == _BlockData` (and not `ScalarBlock`), so it wouldn't be picked up / indexed by the `PseudoMap` that stores the `m` Block's components.

Should we add special handling so that  cloning `_BlockData` returns a `ScalarBlock`?
Excellent question. I'm not sure. Returning a `ScalarBlock` might make sense.
@michaelbynum: what return type would you expect for `m.b[1].clone()`?  The `clone()` infrastructure (since it is based on `deepcopy()`) would give you a `_BlockData`: but that raises another pickle, as the current Pyomo design won't pick up a bare `_BlockData` on a model.  That is `m.c = m.b[1].clone()` would result in `type(m.c) == _BlockData` (and not `ScalarBlock`), so it wouldn't be picked up / indexed by the `PseudoMap` that stores the `m` Block's components.

Should we add special handling so that  cloning `_BlockData` returns a `ScalarBlock`?
Excellent question. I'm not sure. Returning a `ScalarBlock` might make sense.

Resolving patch:
diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py
--- a/pyomo/core/base/block.py
+++ b/pyomo/core/base/block.py
@@ -1329,7 +1329,6 @@ def clone(self):
         # NonNegativeReals, etc) that are not "owned" by any blocks and
         # should be preserved as singletons.
         #
-        save_parent, self._parent = self._parent, None
         try:
             new_block = copy.deepcopy(
                 self, {
@@ -1342,8 +1341,13 @@ def clone(self):
                     '__block_scope__': {id(self): True, id(None): False},
                     '__paranoid__': True,
                     })
-        finally:
-            self._parent = save_parent
+
+        # We need to "detangle" the new block from the original block
+        # hierarchy
+        if self.parent_component() is self:
+            new_block._parent = None
+        else:
+            new_block._component = None
 
         return new_block
 


Test patch:
diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py
--- a/pyomo/core/tests/unit/test_block.py
+++ b/pyomo/core/tests/unit/test_block.py
@@ -2053,6 +2053,49 @@ def test_clone_subblock(self):
             sorted(id(x) for x in (m.x, m.y[1], nb.x, nb.y[1])),
         )
 
+    def test_clone_indexed_subblock(self):
+        m = ConcreteModel()
+        @m.Block([1,2,3])
+        def blk(b, i):
+            b.IDX = RangeSet(i)
+            b.x = Var(b.IDX)
+        m.c = Block(rule=m.blk[2].clone())
+
+        self.assertEqual([1, 2], list(m.c.IDX))
+        self.assertEqual(list(m.blk[2].IDX), list(m.c.IDX))
+        self.assertIsNot(m.blk[2].IDX, m.c.IDX)
+        self.assertIsNot(m.blk[2].x, m.c.x)
+        self.assertIsNot(m.blk[2].IDX, m.c.x.index_set())
+        self.assertIs(m.c.IDX, m.c.x.index_set())
+        self.assertIs(m.c.parent_component(), m.c)
+        self.assertIs(m.c.parent_block(), m)
+
+        m.c1 = Block()
+        m.c1.transfer_attributes_from(m.blk[3].clone())
+
+        self.assertEqual([1, 2, 3], list(m.c1.IDX))
+        self.assertEqual(list(m.blk[3].IDX), list(m.c1.IDX))
+        self.assertIsNot(m.blk[3].IDX, m.c1.IDX)
+        self.assertIsNot(m.blk[3].x, m.c1.x)
+        self.assertIsNot(m.blk[3].IDX, m.c1.x.index_set())
+        self.assertIs(m.c1.IDX, m.c1.x.index_set())
+        self.assertIs(m.c1.parent_component(), m.c1)
+        self.assertIs(m.c1.parent_block(), m)
+
+        @m.Block([1,2,3])
+        def d(b, i):
+            return b.model().blk[i].clone()
+
+        for i in [1, 2, 3]:
+            self.assertEqual(list(range(1, i+1)), list(m.d[i].IDX))
+            self.assertEqual(list(m.blk[i].IDX), list(m.d[i].IDX))
+            self.assertIsNot(m.blk[i].IDX, m.d[i].IDX)
+            self.assertIsNot(m.blk[i].x, m.d[i].x)
+            self.assertIsNot(m.blk[i].IDX, m.d[i].x.index_set())
+            self.assertIs(m.d[i].IDX, m.d[i].x.index_set())
+            self.assertIs(m.d[i].parent_component(), m.d)
+            self.assertIs(m.d[i].parent_block(), m)
+
     def test_clone_unclonable_attribute(self):
         class foo(object):
             def __deepcopy__(bogus):