Repository: Pyomo/pyomo

Issue:
Scaling transformation does not work with References
References break the scaling transformation because it renames the underlying components, then tries to scale the now-broken `Reference` components. Some options:
1. Rename all components (breaking references), then don't try to scale references
2. Don't rename components
3. Create new, renamed components but don't delete the old components

I think I'm in favor of option 1 for now as long as we document that the scaling transformation breaks references. Maybe we can have a `rename_scaled_components` option?


Discussion:
 I wonder if this exposes another problem - are the referenced variables scaled twice? I guess we don't know if it fails.
This supersedes https://github.com/Pyomo/pyomo/issues/1544
I think we try to scale the referenced variables twice, but fail when we get to the Reference. Maybe it's better to think about this as two issues then. We can fix the first, scaling referenced variables twice, by implementing option 1, then address the second, breaking references, separately. 

Resolving patch:
diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py
--- a/pyomo/core/plugins/transform/scaling.py
+++ b/pyomo/core/plugins/transform/scaling.py
@@ -99,7 +99,7 @@ def _get_float_scaling_factor(self, instance, component_data):
                 % (scaling_factor, component_data))
         return scaling_factor
 
-    def _apply_to(self, model, **kwds):
+    def _apply_to(self, model, rename=True):
         # create a map of component to scaling factor
         component_scaling_factor_map = ComponentMap()
 
@@ -117,19 +117,34 @@ def _apply_to(self, model, **kwds):
             raise ValueError("ScaleModel transformation: unknown scaling_method found"
                              "-- supported values: 'user' ")
 
-        # rename all the Vars, Constraints, and Objectives from foo to scaled_foo
-        scaled_component_to_original_name_map = \
-            rename_components(model=model,
-                              component_list=list(model.component_objects(ctype=[Var, Constraint, Objective])),
-                              prefix='scaled_')
+        if rename:
+            # rename all the Vars, Constraints, and Objectives
+            # from foo to scaled_foo
+            component_list = list(model.component_objects(
+                ctype=[Var, Constraint, Objective]))
+            scaled_component_to_original_name_map = rename_components(
+                    model=model,
+                    component_list=component_list,
+                    prefix='scaled_',
+                    )
+        else:
+            scaled_component_to_original_name_map = ComponentMap(
+                    [(comp, comp.name) for comp in 
+                        model.component_objects(
+                            ctype=[Var,Constraint, Objective])]
+                    )
 
         # scale the variable bounds and values and build the variable substitution map
         # for scaling vars in constraints
         variable_substitution_map = ComponentMap()
+        already_scaled = set()
         for variable in [var for var in model.component_objects(ctype=Var, descend_into=True)]:
             # set the bounds/value for the scaled variable
             for k in variable:
                 v = variable[k]
+                if id(v) in already_scaled:
+                    continue
+                already_scaled.add(id(v))
                 scaling_factor = component_scaling_factor_map[v]
                 variable_substitution_map[v] = v / scaling_factor
 
@@ -156,9 +171,13 @@ def _apply_to(self, model, **kwds):
         variable_substitution_dict = {id(k):variable_substitution_map[k]
                                       for k in variable_substitution_map}
 
+        already_scaled = set()
         for component in model.component_objects(ctype=(Constraint, Objective), descend_into=True):
             for k in component:
                 c = component[k]
+                if id(c) in already_scaled:
+                    continue
+                already_scaled.add(id(c))
                 # perform the constraint/objective scaling and variable sub
                 scaling_factor = component_scaling_factor_map[c]
                 if isinstance(c, _ConstraintData):
@@ -240,6 +259,7 @@ def propagate_solution(self, scaled_model, original_model):
         for scaled_v in scaled_model.component_objects(ctype=Var, descend_into=True):
             # get the unscaled_v from the original model
             original_v_path = scaled_component_to_original_name_map[scaled_v]
+            # This will not work if decimal indices are present:
             original_v = original_model.find_component(original_v_path)
 
             for k in scaled_v:


Test patch:
diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py
--- a/pyomo/core/tests/transform/test_scaling.py
+++ b/pyomo/core/tests/transform/test_scaling.py
@@ -128,6 +128,84 @@ def con_rule(m, i):
                 if mk in model.dual:
                     self.assertAlmostEqual(pyo.value(model.dual[mk]), pyo.value(unscaled_model.dual[umk]), 4)
 
+    def test_scaling_without_rename(self):
+        m = pyo.ConcreteModel()
+        m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT)
+        m.v1 = pyo.Var(initialize=10)
+        m.v2 = pyo.Var(initialize=20)
+        m.v3 = pyo.Var(initialize=30)
+
+        def c1_rule(m):
+            return m.v1 == 1e6
+        m.c1 = pyo.Constraint(rule=c1_rule)
+        def c2_rule(m):
+            return m.v2 == 1e-4
+        m.c2 = pyo.Constraint(rule=c2_rule)
+
+        m.scaling_factor[m.v1] = 1.0
+        m.scaling_factor[m.v2] = 0.5
+        m.scaling_factor[m.v3] = 0.25
+        m.scaling_factor[m.c1] = 1e-5
+        m.scaling_factor[m.c2] = 1e5
+
+        values = {}
+        values[id(m.v1)] = (m.v1.value, m.scaling_factor[m.v1])
+        values[id(m.v2)] = (m.v2.value, m.scaling_factor[m.v2])
+        values[id(m.v3)] = (m.v3.value, m.scaling_factor[m.v3])
+        values[id(m.c1)] = (pyo.value(m.c1.body), m.scaling_factor[m.c1])
+        values[id(m.c2)] = (pyo.value(m.c2.body), m.scaling_factor[m.c2])
+
+        m.c2_ref = pyo.Reference(m.c2)
+        m.v3_ref = pyo.Reference(m.v3)
+
+        scale = pyo.TransformationFactory('core.scale_model')
+        scale.apply_to(m, rename=False)
+
+        self.assertTrue(hasattr(m, 'v1'))
+        self.assertTrue(hasattr(m, 'v2'))
+        self.assertTrue(hasattr(m, 'c1'))
+        self.assertTrue(hasattr(m, 'c2'))
+
+        orig_val, factor = values[id(m.v1)]
+        self.assertAlmostEqual(
+                m.v1.value,
+                orig_val*factor,
+                )
+
+        orig_val, factor = values[id(m.v2)]
+        self.assertAlmostEqual(
+                m.v2.value,
+                orig_val*factor,
+                )
+
+        orig_val, factor = values[id(m.c1)]
+        self.assertAlmostEqual(
+                pyo.value(m.c1.body),
+                orig_val*factor,
+                )
+
+        orig_val, factor = values[id(m.c2)]
+        self.assertAlmostEqual(
+                pyo.value(m.c2.body),
+                orig_val*factor,
+                )   
+
+        orig_val, factor = values[id(m.v3)]
+        self.assertAlmostEqual(
+                m.v3_ref[None].value,
+                orig_val*factor,
+                )
+        # Note that because the model was not renamed,
+        # v3_ref is still intact.
+
+        lhs = m.c2.expr.arg(0)
+        monom_factor = lhs.arg(0)
+        scale_factor = (m.scaling_factor[m.c2]/
+                        m.scaling_factor[m.v2])
+        self.assertAlmostEqual(
+                monom_factor,
+                scale_factor,
+                )
 
 if __name__ == "__main__":
     unittest.main()