Repository: Pyomo/pyomo

Issue:
Bug in Sequential Decomposition `tear_diff_direct`
Working with @jsiirola, we have found a bug in the sequential decomposition where an mismatch of indices is occurring.

In this case, I have two blocks with Ports with the following structure:

```
Size=1, Index=None
Key  : Name                : Size : Variable
None : flow_mol_phase_comp :    8 : fs.H101._inlet_flow_mol_phase_comp_ref
     :            pressure :    1 :            fs.H101._inlet_pressure_ref
     :         temperature :    1 :         fs.H101._inlet_temperature_ref
```

These Ports are connected via an Arc, which is the tear stream in my model. However, `flow_mol_phase_comp_ref` is a Pyomo `Reference` with three indices (time, phase, component), which references a set of variables in an indexed block (form: `block[time].flow_mol_phase_comp[phase, component]`).

When running the sequential decomposition, I get the following traceback:

Traceback (most recent call last):
  File "module_2.py", line 196, in <module>
    seq.run(m, function)
  File "/home/alee/pyomo/pyomo/pyomo/network/decomposition.py", line 336, in run
    self.solve_tear_direct(**kwds)
  File "/home/alee/pyomo/pyomo/pyomo/network/foqus_graph.py", line 123, in solve_tear_direct
    svals, dvals = self.tear_diff_direct(G, tears)
  File "/home/alee/pyomo/pyomo/pyomo/network/decomposition.py", line 844, in tear_diff_direct
    dvals.append(value(self.source_dest_peer(arc, name, index)))
  File "/home/alee/pyomo/pyomo/pyomo/network/decomposition.py", line 686, in source_dest_peer
    return mem[index]
  File "/home/alee/pyomo/pyomo/pyomo/core/base/indexed_component.py", line 334, in __getitem__
    validated_index = self._validate_index(index)
  File "/home/alee/pyomo/pyomo/pyomo/core/base/indexed_component.py", line 494, in _validate_index
    % ( idx, self.name, ))
KeyError: "Index '('Liq', 'benzene')' is not valid for indexed component 'fs.H101._inlet_flow_mol_phase_comp_ref'"

It appears that the sequential decomposition is taking the indexes from the original `flow_mol_phase_comp` variable (some instance), and is trying to use these as the indices for the `flow_mol_phase_comp_ref` reference.


Discussion:

Resolving patch:
diff --git a/pyomo/network/decomposition.py b/pyomo/network/decomposition.py
--- a/pyomo/network/decomposition.py
+++ b/pyomo/network/decomposition.py
@@ -598,7 +598,7 @@ def load_guesses(self, guesses, port, fixed):
 
     def load_values(self, port, default, fixed, use_guesses):
         sources = port.sources()
-        for name, obj in port.iter_vars(fixed=False, names=True):
+        for name, index, obj in port.iter_vars(fixed=False, names=True):
             evars = None
             if port.is_extensive(name):
                 # collect evars if there are any
@@ -610,9 +610,8 @@ def load_values(self, port, default, fixed, use_guesses):
                     try:
                         # index into them if necessary, now that
                         # we know they are not None
-                        i = obj.index()
                         for j in range(len(evars)):
-                            evars[j] = evars[j][i]
+                            evars[j] = evars[j][index]
                     except AttributeError:
                         pass
             if evars is not None:
@@ -827,16 +826,12 @@ def tear_diff_direct(self, G, tears):
             arc = G.edges[edge_list[tear]]["arc"]
             src, dest = arc.src, arc.dest
             sf = arc.expanded_block.component("splitfrac")
-            for name, mem in src.iter_vars(names=True):
+            for name, index, mem in src.iter_vars(names=True):
                 if src.is_extensive(name) and sf is not None:
                     # TODO: same as above, what if there's no splitfrac
                     svals.append(value(mem * sf))
                 else:
                     svals.append(value(mem))
-                try:
-                    index = mem.index()
-                except AttributeError:
-                    index = None
                 dvals.append(value(self.source_dest_peer(arc, name, index)))
         svals = numpy.array(svals)
         dvals = numpy.array(dvals)
@@ -889,11 +884,7 @@ def pass_tear_wegstein(self, G, tears, x):
             if dest_unit not in fixed_inputs:
                 fixed_inputs[dest_unit] = ComponentSet()
 
-            for name, mem in src.iter_vars(names=True):
-                try:
-                    index = mem.index()
-                except AttributeError:
-                    index = None
+            for name, index, mem in src.iter_vars(names=True):
                 peer = self.source_dest_peer(arc, name, index)
                 self.pass_single_value(dest, name, peer, x[i],
                     fixed_inputs[dest_unit])
@@ -906,7 +897,7 @@ def generate_gofx(self, G, tears):
             arc = G.edges[edge_list[tear]]["arc"]
             src = arc.src
             sf = arc.expanded_block.component("splitfrac")
-            for name, mem in src.iter_vars(names=True):
+            for name, index, mem in src.iter_vars(names=True):
                 if src.is_extensive(name) and sf is not None:
                     # TODO: same as above, what if there's no splitfrac
                     gofx.append(value(mem * sf))
@@ -920,11 +911,7 @@ def generate_first_x(self, G, tears):
         x = []
         for tear in tears:
             arc = G.edges[edge_list[tear]]["arc"]
-            for name, mem in arc.src.iter_vars(names=True):
-                try:
-                    index = mem.index()
-                except AttributeError:
-                    index = None
+            for name, index, mem in arc.src.iter_vars(names=True):
                 peer = self.source_dest_peer(arc, name, index)
                 x.append(value(peer))
         x = numpy.array(x)
diff --git a/pyomo/network/port.py b/pyomo/network/port.py
--- a/pyomo/network/port.py
+++ b/pyomo/network/port.py
@@ -235,14 +235,14 @@ def iter_vars(self, expr_vars=False, fixed=None, names=False):
             fixed: `bool`
                 Only include variables/expressions with this type of fixed
             names: `bool`
-                If True, yield (name, var/expr) pairs
+                If True, yield (name, index, var/expr) tuples
         """
         for name, mem in iteritems(self.vars):
             if not mem.is_indexed():
-                itr = (mem,)
+                itr = {None: mem}
             else:
-                itr = itervalues(mem)
-            for v in itr:
+                itr = mem
+            for idx, v in iteritems(itr):
                 if fixed is not None and v.is_fixed() != fixed:
                     continue
                 if expr_vars and v.is_expression_type():
@@ -250,12 +250,12 @@ def iter_vars(self, expr_vars=False, fixed=None, names=False):
                         if fixed is not None and var.is_fixed() != fixed:
                             continue
                         if names:
-                            yield name, var
+                            yield name, idx, var
                         else:
                             yield var
                 else:
                     if names:
-                        yield name, v
+                        yield name, idx, v
                     else:
                         yield v
 


Test patch:
diff --git a/pyomo/network/tests/test_decomposition.py b/pyomo/network/tests/test_decomposition.py
--- a/pyomo/network/tests/test_decomposition.py
+++ b/pyomo/network/tests/test_decomposition.py
@@ -250,7 +250,9 @@ def initialize_unit(self):
         # Splitter
         m.splitter = Block()
 
-        m.splitter.flow_in = Var(m.comps)
+        @m.splitter.Block(m.comps)
+        def flow_in(b, i):
+            b.flow = Var()
         m.splitter.temperature_in = Var()
         m.splitter.pressure_in = Var()
 
@@ -288,7 +290,7 @@ def expr_idx_out_side_2(b, i):
 
         @m.splitter.Port()
         def inlet(b):
-            return dict(flow=b.flow_in,
+            return dict(flow=Reference(b.flow_in[:].flow),
                 temperature=b.temperature_in,
                 pressure=b.pressure_in,
                 expr_idx=b.expr_idx_in,
@@ -314,8 +316,10 @@ def initialize_splitter(self):
             recycle = 0.1
             prod = 1 - recycle
             for i in self.flow_in:
-                self.flow_out_side_1[i].value = prod * value(self.flow_in[i])
-                self.flow_out_side_2[i].value = recycle * value(self.flow_in[i])
+                self.flow_out_side_1[i].value \
+                    = prod * value(self.flow_in[i].flow)
+                self.flow_out_side_2[i].value \
+                    = recycle * value(self.flow_in[i].flow)
             for i in self.expr_var_idx_in:
                 self.expr_var_idx_out_side_1[i].value = \
                     prod * value(self.expr_var_idx_in[i])
diff --git a/pyomo/network/tests/test_port.py b/pyomo/network/tests/test_port.py
--- a/pyomo/network/tests/test_port.py
+++ b/pyomo/network/tests/test_port.py
@@ -446,10 +446,10 @@ def contains(item, container):
 
         v = list(p.iter_vars(expr_vars=True, names=True))
         self.assertEqual(len(v), 10)
-        self.assertEqual(len(v[0]), 2)
+        self.assertEqual(len(v[0]), 3)
         for t in v:
             if t[0] == 'x':
-                self.assertIs(t[1], m.x)
+                self.assertIs(t[2], m.x)
                 break
 
     def test_pprint(self):