Repository: Pyomo/pyomo

Issue:
Set Rewrite Integration TODO's
When integrating the set rewrite with the rest of Pyomo, we need to:

- check the implementation of `IndexedComponent.__iter__`. See the following example for details.
```
>>> import pyomo.environ as pe
>>> from pyomo.core.base.set import Set
>>> m = pe.ConcreteModel()
>>> m.a = Set(initialize=[1,2,3])
>>> list(m.a.values())
[1, 2, 3]
>>> list(m.a.cross(m.a).values())
Traceback (most recent call last):
  File "/Users/mlbynum/Documents/pyomo_big/pyomo_main/pyomo/core/base/set.py", line 902, in _to_0_based_index
    if item != int(item):
TypeError: int() argument must be a string, a bytes-like object or a number, not 'tuple'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/mlbynum/Documents/pyomo_big/pyomo_main/pyomo/core/base/indexed_component.py", line 356, in itervalues
    yield self[key]
  File "/Users/mlbynum/Documents/pyomo_big/pyomo_main/pyomo/core/base/set.py", line 2780, in __getitem__
    _idx = self._to_0_based_index(index)
  File "/Users/mlbynum/Documents/pyomo_big/pyomo_main/pyomo/core/base/set.py", line 910, in _to_0_based_index
    % (self.name, type(item).__name__,))
IndexError: SetProduct_OrderedSet indices must be integers, not tuple
```


Discussion:

Resolving patch:
diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py
--- a/pyomo/core/base/indexed_component.py
+++ b/pyomo/core/base/indexed_component.py
@@ -279,7 +279,15 @@ def __contains__(self, idx):
         """Return true if the index is in the dictionary"""
         return idx in self._data
 
+    # The default implementation is for keys() and __iter__ to be
+    # synonyms.  The logic is implemented in keys() so that
+    # keys/values/items continue to work for components that implement
+    # other definitions for __iter__ (e.g., Set)
     def __iter__(self):
+        """Return an iterator of the keys in the dictionary"""
+        return self.keys()
+
+    def keys(self):
         """Iterate over the keys in the dictionary"""
 
         if hasattr(self._index, 'isfinite') and not self._index.isfinite():
@@ -341,6 +349,14 @@ def _sparse_iter_gen(self):
                             yield idx
                 return _sparse_iter_gen(self)
 
+    def values(self):
+        """Return an iterator of the component data objects in the dictionary"""
+        return (self[s] for s in self.keys())
+
+    def items(self):
+        """Return an iterator of (index,data) tuples from the dictionary"""
+        return((s, self[s]) for s in self.keys())
+
     @deprecated('The iterkeys method is deprecated. Use dict.keys().',
                 version='6.0')
     def iterkeys(self):
@@ -359,20 +375,6 @@ def iteritems(self):
         """Return a list (index,data) tuples from the dictionary"""
         return self.items()
 
-    def keys(self):
-        """Return an iterator of the keys in the dictionary"""
-        return iter(self)
-
-    def values(self):
-        """Return an iterator of the component data objects in the dictionary"""
-        for s in self:
-            yield self[s]
-
-    def items(self):
-        """Return an iterator of (index,data) tuples from the dictionary"""
-        for s in self:
-            yield s, self[s]
-
     def __getitem__(self, index):
         """
         This method returns the data corresponding to the given index.
diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py
--- a/pyomo/core/base/param.py
+++ b/pyomo/core/base/param.py
@@ -303,7 +303,7 @@ def __contains__(self, idx):
             return idx in self._data
         return idx in self._index
 
-    def __iter__(self):
+    def keys(self):
         """
         Iterate over the keys in the dictionary.  If the default value is
         specified, then iterate over all keys in the component index.
diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py
--- a/pyomo/core/base/set.py
+++ b/pyomo/core/base/set.py
@@ -105,7 +105,7 @@
 def process_setarg(arg):
     if isinstance(arg, _SetDataBase):
         return arg
-    elif isinstance(arg, IndexedComponent):
+    elif isinstance(arg, IndexedComponent) and arg.is_indexed():
         raise TypeError("Cannot apply a Set operator to an "
                         "indexed %s component (%s)"
                         % (arg.ctype.__name__, arg.name,))
@@ -1057,49 +1057,34 @@ def cross(self, *args):
     __mul__ = cross
 
     def __ror__(self, other):
-        # See the discussion of Set vs SetOf in _processArgs below
+        # See the discussion of Set vs SetOf in process_setarg above
         #
         # return SetOf(other) | self
-        tmp = SetOf(other)
-        ans = Set(initialize=tmp, ordered=tmp.isordered())
-        ans.construct()
-        return ans | self
+        return process_setarg(other) | self
 
     def __rand__(self, other):
-        # See the discussion of Set vs SetOf in _processArgs below
+        # See the discussion of Set vs SetOf in process_setarg above
         #
         # return SetOf(other) & self
-        tmp = SetOf(other)
-        ans = Set(initialize=tmp, ordered=tmp.isordered())
-        ans.construct()
-        return ans & self
+        return process_setarg(other) & self
 
     def __rsub__(self, other):
-        # See the discussion of Set vs SetOf in _processArgs below
+        # See the discussion of Set vs SetOf in process_setarg above
         #
         # return SetOf(other) - self
-        tmp = SetOf(other)
-        ans = Set(initialize=tmp, ordered=tmp.isordered())
-        ans.construct()
-        return ans - self
+        return process_setarg(other) - self
 
     def __rxor__(self, other):
-        # See the discussion of Set vs SetOf in _processArgs below
+        # See the discussion of Set vs SetOf in process_setarg above
         #
         # return SetOf(other) ^ self
-        tmp = SetOf(other)
-        ans = Set(initialize=tmp, ordered=tmp.isordered())
-        ans.construct()
-        return ans ^ self
+        return process_setarg(other) ^ self
 
     def __rmul__(self, other):
-        # See the discussion of Set vs SetOf in _processArgs below
+        # See the discussion of Set vs SetOf in process_setarg above
         #
         # return SetOf(other) * self
-        tmp = SetOf(other)
-        ans = Set(initialize=tmp, ordered=tmp.isordered())
-        ans.construct()
-        return ans * self
+        return process_setarg(other) * self
 
     def __lt__(self,other):
         """
@@ -1379,6 +1364,23 @@ def pop(self):
         return self._values.pop()
 
 
+class _ScalarOrderedSetMixin(object):
+    # This mixin is required because scalar ordered sets implement
+    # __getitem__() as an alias of card()
+    __slots__ = ()
+
+    def values(self):
+        """Return an iterator of the component data objects in the dictionary"""
+        if list(self.keys()):
+            yield self
+
+    def items(self):
+        """Return an iterator of (index,data) tuples from the dictionary"""
+        _keys = list(self.keys())
+        if _keys:
+            yield _keys[0], self
+
+
 class _OrderedSetMixin(object):
     __slots__ = ()
 
@@ -2213,7 +2215,7 @@ class FiniteSimpleSet(metaclass=RenamedClass):
     __renamed__version__ = '6.0'
 
 
-class OrderedScalarSet(_InsertionOrderSetData, Set):
+class OrderedScalarSet(_ScalarOrderedSetMixin, _InsertionOrderSetData, Set):
     def __init__(self, **kwds):
         # In case someone inherits from us, we will provide a rational
         # default for the "ordered" flag
@@ -2228,7 +2230,7 @@ class OrderedSimpleSet(metaclass=RenamedClass):
     __renamed__version__ = '6.0'
 
 
-class SortedScalarSet(_SortedSetData, Set):
+class SortedScalarSet(_ScalarOrderedSetMixin, _SortedSetData, Set):
     def __init__(self, **kwds):
         # In case someone inherits from us, we will provide a rational
         # default for the "ordered" flag
@@ -2364,7 +2366,7 @@ def _pprint(self):
 class UnorderedSetOf(SetOf):
     pass
 
-class OrderedSetOf(_OrderedSetMixin, SetOf):
+class OrderedSetOf(_ScalarOrderedSetMixin, _OrderedSetMixin, SetOf):
     def __getitem__(self, index):
         i = self._to_0_based_index(index)
         try:
@@ -2938,7 +2940,8 @@ class InfiniteSimpleRangeSet(metaclass=RenamedClass):
     __renamed__version__ = '6.0'
 
 
-class FiniteScalarRangeSet(_FiniteRangeSetData, RangeSet):
+class FiniteScalarRangeSet(_ScalarOrderedSetMixin,
+                           _FiniteRangeSetData, RangeSet):
     def __init__(self, *args, **kwds):
         _FiniteRangeSetData.__init__(self, component=self)
         RangeSet.__init__(self, *args, **kwds)
@@ -3239,7 +3242,8 @@ def __len__(self):
         return len(set0) + sum(1 for s in set1 if s not in set0)
 
 
-class SetUnion_OrderedSet(_OrderedSetMixin, SetUnion_FiniteSet):
+class SetUnion_OrderedSet(_ScalarOrderedSetMixin, _OrderedSetMixin,
+                          SetUnion_FiniteSet):
     __slots__ = tuple()
 
     def __getitem__(self, index):
@@ -3377,7 +3381,8 @@ def __len__(self):
         return sum(1 for _ in self)
 
 
-class SetIntersection_OrderedSet(_OrderedSetMixin, SetIntersection_FiniteSet):
+class SetIntersection_OrderedSet(_ScalarOrderedSetMixin, _OrderedSetMixin,
+                                 SetIntersection_FiniteSet):
     __slots__ = tuple()
 
     def __getitem__(self, index):
@@ -3465,7 +3470,8 @@ def __len__(self):
         return sum(1 for _ in self)
 
 
-class SetDifference_OrderedSet(_OrderedSetMixin, SetDifference_FiniteSet):
+class SetDifference_OrderedSet(_ScalarOrderedSetMixin, _OrderedSetMixin,
+                               SetDifference_FiniteSet):
     __slots__ = tuple()
 
     def __getitem__(self, index):
@@ -3571,8 +3577,9 @@ def __len__(self):
         return sum(1 for _ in self)
 
 
-class SetSymmetricDifference_OrderedSet(_OrderedSetMixin,
-                                         SetSymmetricDifference_FiniteSet):
+class SetSymmetricDifference_OrderedSet(_ScalarOrderedSetMixin,
+                                        _OrderedSetMixin,
+                                        SetSymmetricDifference_FiniteSet):
     __slots__ = tuple()
 
     def __getitem__(self, index):
@@ -3849,7 +3856,8 @@ def __len__(self):
         return ans
 
 
-class SetProduct_OrderedSet(_OrderedSetMixin, SetProduct_FiniteSet):
+class SetProduct_OrderedSet(_ScalarOrderedSetMixin, _OrderedSetMixin,
+                            SetProduct_FiniteSet):
     __slots__ = tuple()
 
     def __getitem__(self, index):


Test patch:
diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py
--- a/pyomo/core/tests/unit/test_set.py
+++ b/pyomo/core/tests/unit/test_set.py
@@ -2027,11 +2027,19 @@ def test_invalid_operators(self):
                 TypeError, "Cannot apply a Set operator to an "
                 r"indexed Set component \(J\)"):
             m.I | m.J
+        with self.assertRaisesRegex(
+                TypeError, "Cannot apply a Set operator to an "
+                r"indexed Set component \(J\)"):
+            m.J | m.I
         m.x = Suffix()
         with self.assertRaisesRegex(
                 TypeError, "Cannot apply a Set operator to a "
                 r"non-Set Suffix component \(x\)"):
             m.I | m.x
+        with self.assertRaisesRegex(
+                TypeError, "Cannot apply a Set operator to a "
+                r"non-Set Suffix component \(x\)"):
+            m.x | m.I
         m.y = Var([1,2])
         with self.assertRaisesRegex(
                 TypeError, "Cannot apply a Set operator to an "
@@ -2041,6 +2049,14 @@ def test_invalid_operators(self):
                 TypeError, "Cannot apply a Set operator to a "
                 r"non-Set component data \(y\[1\]\)"):
             m.I | m.y[1]
+        with self.assertRaisesRegex(
+                TypeError, "Cannot apply a Set operator to an "
+                r"indexed Var component \(y\)"):
+            m.y | m.I
+        with self.assertRaisesRegex(
+                TypeError, "Cannot apply a Set operator to a "
+                r"non-Set component data \(y\[1\]\)"):
+            m.y[1] | m.I
 
 class TestSetIntersection(unittest.TestCase):
     def test_pickle(self):
@@ -5920,3 +5936,19 @@ def b_rule(m):
         self.assertEqual(len(m.a), 0)
         m.b = Set(initialize=b_rule, dimen=2)
         self.assertEqual(len(m.b), 0)
+
+    def test_issue_1112(self):
+        m = ConcreteModel()
+        m.a = Set(initialize=[1,2,3])
+        #
+        vals = list(m.a.values())
+        self.assertEqual(len(vals), 1)
+        self.assertIs(vals[0], m.a)
+        #
+        cross = m.a.cross(m.a)
+        self.assertIs(type(cross), SetProduct_OrderedSet)
+        #
+        vals = list(m.a.cross(m.a).values())
+        self.assertEqual(len(vals), 1)
+        self.assertIsInstance(vals[0], SetProduct_OrderedSet)
+        self.assertIsNot(vals[0], cross)
diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py
--- a/pyomo/core/tests/unit/test_sets.py
+++ b/pyomo/core/tests/unit/test_sets.py
@@ -1063,64 +1063,38 @@ def test_contains(self):
 
     def test_or(self):
         """Check that set union works"""
-        # In the set rewrite, the following now works!
-        # try:
-        #     self.instance.A | self.instance.tmpset3
-        # except TypeError:
-        #     pass
-        # else:
-        #     self.fail("fail test_or")
-        self.assertEqual(self.instance.A | self.instance.tmpset3,
-                         self.instance.A)
+        with self.assertRaisesRegex(
+                TypeError, r'Cannot apply a Set operator to an indexed Set '
+                r'component \(A\)'):
+            self.instance.A | self.instance.tmpset3
 
     def test_and(self):
         """Check that set intersection works"""
-        # In the set rewrite, the following now works!
-        # try:
-        #     self.instance.tmp = self.instance.A & self.instance.tmpset3
-        # except TypeError:
-        #     pass
-        # else:
-        #     self.fail("fail test_and")
-        self.assertEqual(self.instance.A & self.instance.tmpset3,
-                         EmptySet)
+        with self.assertRaisesRegex(
+                TypeError, r'Cannot apply a Set operator to an indexed Set '
+                r'component \(A\)'):
+            self.instance.A & self.instance.tmpset3
 
     def test_xor(self):
         """Check that set exclusive or works"""
-        # In the set rewrite, the following now works!
-        # try:
-        #     self.instance.A ^ self.instance.tmpset3
-        # except TypeError:
-        #     pass
-        # else:
-        #     self.fail("fail test_xor")
-        self.assertEqual(self.instance.A ^ self.instance.tmpset3,
-                         self.instance.A)
+        with self.assertRaisesRegex(
+                TypeError, r'Cannot apply a Set operator to an indexed Set '
+                r'component \(A\)'):
+            self.instance.A ^ self.instance.tmpset3
 
     def test_diff(self):
         """Check that set difference works"""
-        # In the set rewrite, the following now works!
-        # try:
-        #     self.instance.A - self.instance.tmpset3
-        # except TypeError:
-        #     pass
-        # else:
-        #     self.fail("fail test_diff")
-        self.assertEqual(self.instance.A - self.instance.tmpset3,
-                         self.instance.A)
+        with self.assertRaisesRegex(
+                TypeError, r'Cannot apply a Set operator to an indexed Set '
+                r'component \(A\)'):
+            self.instance.A - self.instance.tmpset3
 
     def test_mul(self):
         """Check that set cross-product works"""
-        # In the set rewrite, the following now works!
-        # try:
-        #     self.instance.A * self.instance.tmpset3
-        # except TypeError:
-        #     pass
-        # else:
-        #     self.fail("fail test_mul")
-        # Note: cross product with an empty set is an empty set
-        self.assertEqual(self.instance.A * self.instance.tmpset3,
-                         [])
+        with self.assertRaisesRegex(
+                TypeError, r'Cannot apply a Set operator to an indexed Set '
+                r'component \(A\)'):
+            self.instance.A * self.instance.tmpset3
 
     def test_override_values(self):
         m = ConcreteModel()