Repository: Pyomo/pyomo

Issue:
SequentialDecomposition inserts numpy float64 values during solution
An unusual bug was brought to my attention today where a scalar component appeared to be treated as an indexed component.

This comes from IDAES, and a Jupyter notebook with an example can be found here: https://github.com/IDAES/workspace/pull/106/files

It took a bit of digging, but it seems something unexpected is happening when Pyomo is generating an expression. The expression being generated is:

`x = 1 - T/temperature_crit`

In this particular case, `T` is a float and `temeprature_crit` is a scalar `Var` which happens to be located on another `Block`. When trying to generate this expression, the following exception is raised (shortened traceback at bottom of page).

```
KeyError: "Cannot treat the scalar component 'fs.props.benzene.temperature_crit' as an indexed component"
```

Interestingly, if I rewrite the expression as:

`x = 1 - T*temperature_crit**-1`

everything works.

**Traceback**

~/idaes/idaes-dev/idaes/generic_models/properties/core/pure/RPP.py in return_expression(b, cobj, T, dT)
    100             return pressure_sat_comp.dT_expression(b, cobj, T)
    101 
--> 102         x = 1 - T/cobj.temperature_crit
    103 
    104         return (exp((1-x)**-1 * (cobj.pressure_sat_comp_coeff['A']*x +

~/pyomo/pyomo/pyomo/core/base/indexed_component.py in __getitem__(self, index)
    396             if index.__class__ is EXPR.GetItemExpression:
    397                 return index
--> 398             validated_index = self._validate_index(index)
    399             if validated_index is not index:
    400                 index = validated_index

~/pyomo/pyomo/pyomo/core/base/indexed_component.py in _validate_index(self, idx)
    551             raise KeyError(
    552                 "Cannot treat the scalar component '%s' "
--> 553                 "as an indexed component" % ( self.name, ))
    554         #
    555         # Raise an exception

KeyError: "Cannot treat the scalar component 'fs.props.benzene.temperature_crit' as an indexed component"


Discussion:
The problem is that `T` is a `numpy.float64` and not a `float`.  There is a known incompatibility between numpy and pyomo due to the logic that numpy uses to guess when an object is an array.  See #31. 

The current workaround is to cast `T` to a float.
@jsiirola That appears to be the case here. However, I am trying to work out *how* a `numpy.float64` ended up in there, as the Jupyter notebook and IDAES code do not use `numpy`. Trying to trace things, it is starting to look like the `numpy.float64` is coming out of `pyomo.network` and the `SequentialDecomposition` tool (the places I start seeing `numpy.float64` start appearing are in the inputs being set bu the SD tool).

So, I think there might still be a bug here, but maybe buried deeper. If the SD tool is going to return  values as `numpy.float64`, then we are going to run into this more frequently, and users are going to have no idea why.
Adding some more detail, I suspect the issue lies in the tear convergence code.

The first tear iteration works fine, so values are being properly propagated along Arcs, and the issue only appears on the first step of the second tear iteration. It looks like the tear stream guesses are being set with `numpy.float64`'s, which results in the observed issue.
@andrewlee94, interesting.  The problem is arising because we are generating expressions.  Do you have an intuition as to why generic_property is generating new expressions the second time around?
@jsiirola This is happening as part of the initialisation process, not model construction. We need to estimate bubble and dew points, which in turn requires saturation pressures. I am using the same code that generates the expression for saturation pressure in the actual model to get estimated values for the saturation pressure, hence the expression getting built again.

Resolving patch:
diff --git a/pyomo/network/decomposition.py b/pyomo/network/decomposition.py
--- a/pyomo/network/decomposition.py
+++ b/pyomo/network/decomposition.py
@@ -31,7 +31,6 @@
 
 logger = logging.getLogger('pyomo.network')
 
-
 class SequentialDecomposition(FOQUSGraph):
     """
     A sequential decomposition tool for Pyomo Network models
@@ -470,7 +469,9 @@ def pass_values(self, arc, fixed_inputs):
                     evars = [(evar, None)]
                 for evar, idx in evars:
                     fixed_inputs[dest_unit].add(evar)
-                    evar.fix(value(mem[idx] if mem.is_indexed() else mem))
+                    val = value(mem[idx] if mem.is_indexed() else mem)
+                    # val are numpy.float64; coerce val back to float
+                    evar.fix(float(val))
 
         for con in eblock.component_data_objects(Constraint, active=True):
             # we expect to find equality constraints with one linear variable
@@ -501,7 +502,8 @@ def pass_values(self, arc, fixed_inputs):
             val = (value(con.lower) - repn.constant) / repn.linear_coefs[0]
             var = repn.linear_vars[0]
             fixed_inputs[dest_unit].add(var)
-            var.fix(val)
+            # val are numpy.float64; coerce val back to float
+            var.fix(float(val))
 
     def pass_single_value(self, port, name, member, val, fixed):
         """
@@ -525,7 +527,8 @@ def pass_single_value(self, port, name, member, val, fixed):
                 fval = (0 - repn.constant) / repn.linear_coefs[0]
                 var = repn.linear_vars[0]
                 fixed.add(var)
-                var.fix(fval)
+                # val are numpy.float64; coerce val back to float
+                var.fix(float(fval))
             else:
                 raise RuntimeError(
                     "Member '%s' of port '%s' had more than "
@@ -534,7 +537,8 @@ def pass_single_value(self, port, name, member, val, fixed):
                     "to this port." % (name, port.name))
         else:
             fixed.add(member)
-            member.fix(val)
+            # val are numpy.float64; coerce val back to float
+            member.fix(float(val))
 
     def load_guesses(self, guesses, port, fixed):
         srcs = port.sources()
@@ -577,7 +581,7 @@ def load_guesses(self, guesses, port, fixed):
                             # silently ignore vars already fixed
                             continue
                         fixed.add(evar)
-                        evar.fix(val)
+                        evar.fix(float(val))
                 if not has_evars:
                     # the only NumericValues in Pyomo that return True
                     # for is_fixed are expressions and variables
@@ -591,7 +595,7 @@ def load_guesses(self, guesses, port, fixed):
                                 port.name))
                     else:
                         fixed.add(var)
-                        var.fix(entry)
+                        var.fix(float(entry))
 
     def load_values(self, port, default, fixed, use_guesses):
         sources = port.sources()
@@ -652,7 +656,7 @@ def check_value_fix(self, port, var, default, fixed, use_guesses,
                     "guess, " if use_guesses else ""))
 
         fixed.add(var)
-        var.fix(val)
+        var.fix(float(val))
 
     def combine_and_fix(self, port, name, obj, evars, fixed):
         """


Test patch: