Repository: Pyomo/pyomo

Issue:
Var bound defined by a Param doesn't update in 6.0.1
## Summary

I have a Var whose bound is defined by a mutable Param. The Param has a default value upon construction, and later gets updated. In Pyomo version 5.7.3, the bound's value is updated correctly; however in version 6.0.1, the bound's value doesn't get updated and remains at the default value. The thing that seems to be breaking version 6.0.1 is whether or not units are included in the Var and the Param.

### Steps to reproduce the issue
```
    import pyomo.environ as pyomo
    from pyomo.environ import units as u

    model = pyomo.ConcreteModel()

    model.avail_gen = pyomo.Param(within=pyomo.NonNegativeReals,
                                  default=0,
                                  mutable=True,
                                  units=u.MW
                                  )
    model.generation = pyomo.Var(domain=pyomo.NonNegativeReals,
                                 bounds=(0, model.avail_gen),
                                 units=u.MW
                                 )
    model.avail_gen.set_value(10)
    model.obj = pyomo.Objective(expr=model.generation, sense=pyomo.maximize)

    solver = pyomo.SolverFactory('glpk')
    results = solver.solve(model)

    print(pyomo.value(model.obj))
```

### Error Message

The obj value is incorrect at 0, due to the upper bounds of model.generation = 0. However, if the units=u.MW lines are commented out, the obj value is correct at 10. This doesn't happen with 5.7.3.

### Information on your system

Pyomo version: 6.0.1
Python version: 3.7
Operating system: OSX
How Pyomo was installed (PyPI, conda, source): PyPi
Solver (if applicable): GLPK, but others also




Discussion:

Resolving patch:
diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py
--- a/pyomo/core/base/var.py
+++ b/pyomo/core/base/var.py
@@ -462,50 +462,14 @@ def setlb(self, val):
         Set the lower bound for this variable after validating that
         the value is fixed (or None).
         """
-        # Note: is_fixed(None) returns True
-        if not is_fixed(val):
-            raise ValueError(
-                "Non-fixed input of type '%s' supplied as variable lower "
-                "bound - legal types must be fixed expressions or variables."
-                % (type(val),))
-        if type(val) in native_numeric_types or val is None:
-            # TODO: warn/error: check if this Var has units: assigning
-            # a dimensionless value to a united variable should be an error
-            pass
-        else:
-            if self.parent_component()._units is not None:
-                _src_magnitude = value(val)
-                _src_units = units.get_units(val)
-                val = units.convert_value(
-                    num_value=_src_magnitude, from_units=_src_units,
-                    to_units=self.parent_component()._units)
-        self._lb = val
-
+        self._lb = self._process_bound(val, 'lower')
 
     def setub(self, val):
         """
         Set the upper bound for this variable after validating that
         the value is fixed (or None).
         """
-        # Note: is_fixed(None) returns True
-        if not is_fixed(val):
-            raise ValueError(
-                "Non-fixed input of type '%s' supplied as variable upper "
-                "bound - legal types are fixed expressions or variables."
-                "parameters"
-                % (type(val),))
-        if type(val) in native_numeric_types or val is None:
-            # TODO: warn/error: check if this Var has units: assigning
-            # a dimensionless value to a united variable should be an error
-            pass
-        else:
-            if self.parent_component()._units is not None:
-                _src_magnitude = value(val)
-                _src_units = units.get_units(val)
-                val = units.convert_value(
-                    num_value=_src_magnitude, from_units=_src_units,
-                    to_units=self.parent_component()._units)
-        self._ub = val
+        self._ub = self._process_bound(val, 'upper')
 
     def fix(self, value=NoArgumentGiven):
         """
@@ -522,6 +486,26 @@ def unfix(self):
 
     free = unfix
 
+    def _process_bound(self, val, bound_type):
+        # Note: is_fixed(None) returns True
+        if not is_fixed(val):
+            raise ValueError(
+                "Non-fixed input of type '%s' supplied as variable %s "
+                "bound - legal types must be constants or fixed expressions."
+                % (type(val), bound_type))
+        if type(val) in native_numeric_types or val is None:
+            # TODO: warn/error: check if this Var has units: assigning
+            # a dimensionless value to a united variable should be an error
+            pass
+        else:
+            # We want to create an expression and not just convert the
+            # current value so that things like mutable Params behave as
+            # expected.
+            if self.parent_component()._units is not None:
+                val = units.convert(
+                    val, to_units=self.parent_component()._units)
+        return val
+
 
 @ModelComponentFactory.register("Decision variables.")
 class Var(IndexedComponent, IndexedComponent_NDArrayMixin):


Test patch:
diff --git a/pyomo/core/tests/unit/test_var.py b/pyomo/core/tests/unit/test_var.py
--- a/pyomo/core/tests/unit/test_var.py
+++ b/pyomo/core/tests/unit/test_var.py
@@ -1405,6 +1405,7 @@ def test_set_value_units(self):
     def test_set_bounds_units(self):
         m = ConcreteModel()
         m.x = Var(units=units.g)
+        m.p = Param(mutable=True, initialize=1, units=units.kg)
         m.x.setlb(5)
         self.assertEqual(m.x.lb, 5)
         m.x.setlb(6*units.g)
@@ -1413,6 +1414,10 @@ def test_set_bounds_units(self):
         self.assertEqual(m.x.lb, 7000)
         with self.assertRaises(UnitsError):
             m.x.setlb(1*units.s)
+        m.x.setlb(m.p)
+        self.assertEqual(m.x.lb, 1000)
+        m.p = 2 * units.kg
+        self.assertEqual(m.x.lb, 2000)
 
         m.x.setub(2)
         self.assertEqual(m.x.ub, 2)
@@ -1421,7 +1426,11 @@ def test_set_bounds_units(self):
         m.x.setub(4*units.kg)
         self.assertEqual(m.x.ub, 4000)
         with self.assertRaises(UnitsError):
-            m.x.setlb(1*units.s)
+            m.x.setub(1*units.s)
+        m.x.setub(m.p)
+        self.assertEqual(m.x.ub, 2000)
+        m.p = 3 * units.kg
+        self.assertEqual(m.x.ub, 3000)
 
 
 if __name__ == "__main__":