Repository: Pyomo/pyomo

Issue:
TranformationFactory('core.fix_discrete') doesn't work for cloned models
# Summary
I have noticed that often times `TransformationFactory('core.fix_discrete')` doesn't work for a model, i.e. it doesn't actually fix the variables. I have found out that it happens to cloned models (whereas 'normal' models work as expected).

## Minimal working example
```python
from pyomo.environ import *
m = ConcreteModel()
m.y = Var(domain=Integers, bounds=(0,10), initialize=0)
m_clone = m.clone()
TransformationFactory('core.fix_discrete').apply_to(m_clone)
m_clone.pprint()
```
yields
```
1 Var Declarations
    y : Size=1, Index=None
        Key  : Lower : Value : Upper : Fixed : Stale : Domain
        None :     0 :     0 :    10 : False : False : Integers
1 Suffix Declarations
    _fixed_discrete_vars : Direction=Suffix.LOCAL, Datatype=Suffix.FLOAT
        Key  : Value
        None :    []
2 Declarations: y _fixed_discrete_vars
```
Notice that the variable `y` didn't actually get fixed. If the `TranformationFactory` is intead applied to model `m`, the behaviour is as expected.

## Looking into the code
When diving into the code, the problem ends up being when the domain of the variable `y` is checked against a list of domains [here](https://github.com/Pyomo/pyomo/blob/ea161615a67b93c2540384eae4cce55e998dc8cc/pyomo/core/plugins/transform/discrete_vars.py#L116-L119)

The expression `var.domain in _discrete_relaxation_map` will yield `False`, even though `var` is an Integer.


Discussion:
I think this is related to a discrepancy between `is` and `==` in the comparison due to the fact that `Integer` in the clone will have a different `id` than in the original.
This is a fundamental problem with the current Set system (continuous sets are not first-class `Set` components, and as such do not follow the block scoping rules within `Block.clone()`)

This will be addressed by #326.
What can I do until #326 is fixed? It seems like it might be a while. I don't want to 'hand-code' the functionality again for cloned models.
I have noticed that the expression `type(m.y) is type(m_clone.y)` is `True`, so that has lead me to the following 'dirty' fix:
Replace 
```python
var.domain in _discrete_relaxation_map
``` 
with 
```python
any(type(var.domain) is type(domain) for domain in _discrete_relaxation_map)
```
This will be slightly slower (although not by that much), but the transformation shouldn't be critical for performance. Additionally, imo it doesn't sacrifice readibility - instead it indicates that we are looking for domains in `_discrete_relaxation_map`, which was not very clear before.

I can submitt a PR if you think this is acceptable (for now).

Resolving patch:
diff --git a/pyomo/core/plugins/transform/discrete_vars.py b/pyomo/core/plugins/transform/discrete_vars.py
--- a/pyomo/core/plugins/transform/discrete_vars.py
+++ b/pyomo/core/plugins/transform/discrete_vars.py
@@ -95,26 +95,29 @@ def _apply_to(self, model, **kwds):
 #
 # This transformation fixes known discrete domains to their current values
 #
-@TransformationFactory.register('core.fix_discrete', 
-           doc="Fix known discrete domains to continuous counterparts" )
+@TransformationFactory.register('core.fix_discrete',
+           doc="Fix known discrete domains to continuous counterparts")
 class FixDiscreteVars(Transformation):
 
     def __init__(self):
         super(FixDiscreteVars, self).__init__()
 
-    def _apply_to(self, model, **kwds): 
+    def _apply_to(self, model, **kwds):
         options = kwds.pop('options', {})
         if kwds.get('undo', options.get('undo', False)):
             for v in model._fixed_discrete_vars[None]:
                 v.unfix()
             model.del_component("_fixed_discrete_vars")
             return
-        
+
         fixed_vars = []
         _base_model_vars = model.component_data_objects(
-            Var, active=True, descend_into=True )
+            Var, active=True, descend_into=True)
         for var in _base_model_vars:
-            if var.domain in _discrete_relaxation_map and not var.is_fixed():
+            # Instead of checking against `_discrete_relaxation_map.keys()` 
+            # we just check the item properties to fix #995 
+            # When #326 has been resolved, we can check against the dict-keys again
+            if not var.is_continuous() and not var.is_fixed():
                 fixed_vars.append(var)
                 var.fix()
         model._fixed_discrete_vars = Suffix(direction=Suffix.LOCAL)


Test patch:
diff --git a/pyomo/core/tests/transform/test_transform.py b/pyomo/core/tests/transform/test_transform.py
--- a/pyomo/core/tests/transform/test_transform.py
+++ b/pyomo/core/tests/transform/test_transform.py
@@ -52,6 +52,45 @@ def test_transform_dir(self):
         model = AbstractModel()
         self.assertTrue(set(TransformationFactory) >= set(['core.relax_integrality']))
 
+    def test_fix_discrete(self):
+        # Coverage of the _clear_attribute method
+        self.model.A = RangeSet(1,4)
+        self.model.a = Var()
+        self.model.b = Var(within=self.model.A)
+        self.model.c = Var(within=NonNegativeIntegers)
+        self.model.d = Var(within=Integers, bounds=(-2,3))
+        self.model.e = Var(within=Boolean)
+        self.model.f = Var(domain=Boolean)
+        instance=self.model.create_instance()
+        xfrm = TransformationFactory('core.fix_discrete')
+        rinst = xfrm.create_using(instance)
+        self.assertFalse(rinst.a.is_fixed())
+        self.assertTrue(rinst.b.is_fixed())
+        self.assertTrue(rinst.c.is_fixed())
+        self.assertTrue(rinst.d.is_fixed())
+        self.assertTrue(rinst.e.is_fixed())
+        self.assertTrue(rinst.f.is_fixed())
+
+    def test_fix_discrete_clone(self):
+        # Coverage of the _clear_attribute method
+        self.model.A = RangeSet(1,4)
+        self.model.a = Var()
+        self.model.b = Var(within=self.model.A)
+        self.model.c = Var(within=NonNegativeIntegers)
+        self.model.d = Var(within=Integers, bounds=(-2,3))
+        self.model.e = Var(within=Boolean)
+        self.model.f = Var(domain=Boolean)
+        instance=self.model.create_instance()
+        instance_clone = instance.clone()
+        xfrm = TransformationFactory('core.fix_discrete')
+        rinst = xfrm.create_using(instance_clone)
+        self.assertFalse(rinst.a.is_fixed())
+        self.assertTrue(rinst.b.is_fixed())
+        self.assertTrue(rinst.c.is_fixed())
+        self.assertTrue(rinst.d.is_fixed())
+        self.assertTrue(rinst.e.is_fixed())
+        self.assertTrue(rinst.f.is_fixed())
+
     def test_relax_integrality1(self):
         # Coverage of the _clear_attribute method
         self.model.A = RangeSet(1,4)
@@ -102,6 +141,49 @@ def test_relax_integrality2(self):
         self.assertEqual(rinst.e[1].bounds, instance.e[1].bounds)
         self.assertEqual(rinst.f[1].bounds, instance.f[1].bounds)
 
+    def test_relax_integrality_cloned(self):
+        # Coverage of the _clear_attribute method
+        self.model.A = RangeSet(1,4)
+        self.model.a = Var()
+        self.model.b = Var(within=self.model.A)
+        self.model.c = Var(within=NonNegativeIntegers)
+        self.model.d = Var(within=Integers, bounds=(-2,3))
+        self.model.e = Var(within=Boolean)
+        self.model.f = Var(domain=Boolean)
+        instance=self.model.create_instance()
+        instance_cloned = instance.clone()
+        xfrm = TransformationFactory('core.relax_integrality')
+        rinst = xfrm.create_using(instance_cloned)
+        self.assertEqual(type(rinst.a.domain), RealSet)
+        self.assertEqual(type(rinst.b.domain), RealSet)
+        self.assertEqual(type(rinst.c.domain), RealSet)
+        self.assertEqual(type(rinst.d.domain), RealSet)
+        self.assertEqual(type(rinst.e.domain), RealSet)
+        self.assertEqual(type(rinst.f.domain), RealSet)
+        self.assertEqual(rinst.a.bounds, instance_cloned.a.bounds)
+        self.assertEqual(rinst.b.bounds, instance_cloned.b.bounds)
+        self.assertEqual(rinst.c.bounds, instance_cloned.c.bounds)
+        self.assertEqual(rinst.d.bounds, instance_cloned.d.bounds)
+        self.assertEqual(rinst.e.bounds, instance_cloned.e.bounds)
+        self.assertEqual(rinst.f.bounds, instance_cloned.f.bounds)
+
+    def test_relax_integrality(self):
+        # Coverage of the _clear_attribute method
+        self.model.d = Var(within=Integers, bounds=(-2,3))
+        instance=self.model.create_instance()
+        instance_cloned = instance.clone()
+        xfrm = TransformationFactory('core.relax_integrality')
+        rinst = xfrm.create_using(instance_cloned)
+        self.assertEqual(type(rinst.d.domain), RealSet)
+
+    def test_relax_integrality_simple_cloned(self):
+        self.model.x = Var(within=Integers, bounds=(-2,3))
+        instance = self.model.create_instance()
+        instance_cloned = instance.clone()
+        xfrm = TransformationFactory('core.relax_discrete')
+        rinst = xfrm.create_using(instance_cloned)
+        self.assertNotEqual(type(rinst.x.domain), RealSet)
+
     def test_nonnegativity_transformation_1(self):
         self.model.a = Var()
         self.model.b = Var(within=NonNegativeIntegers)