Repository: Pyomo/pyomo

Issue:
`_component_data_iter` I think doesn't use `sorted_robust`?
This is still related to #567, but now I'm confused. On master (after #1842), now the `pprint` statement in the following code works, but the writer still throws the `TypeError` because it cannot sort the tuples in `m.ick`:
```
from pyomo.environ import *

m = ConcreteModel()
m.ick = Set(initialize=[(1, 'str1'), ('str2', 'str3')], ordered=True,
            within=Any)
m.v = Var(m.ick, within=Binary)
m.obj = Objective(expr=m.v[1, 'str1'])
m.cons = Constraint(expr=m.v[1,'str1'] >= m.v['str2', 'str3'])

m.pprint() # this is okay
SolverFactory('gurobi').solve(m, tee=True) # this throws the error below
```
```
Traceback (most recent call last):
  File "sorting_stuff.py", line 11, in <module>
    SolverFactory('gurobi').solve(m, tee=True)
  File "/home/esjohn/src/pyomo/pyomo/opt/base/solvers.py", line 574, in solve
    self._presolve(*args, **kwds)
  File "/home/esjohn/src/pyomo/pyomo/solvers/plugins/solvers/GUROBI.py", line 230, in _presolve
    ILMLicensedSystemCallSolver._presolve(self, *args, **kwds)
  File "/home/esjohn/src/pyomo/pyomo/opt/solver/shellcmd.py", line 200, in _presolve
    OptSolver._presolve(self, *args, **kwds)
  File "/home/esjohn/src/pyomo/pyomo/opt/base/solvers.py", line 674, in _presolve
    **kwds)
  File "/home/esjohn/src/pyomo/pyomo/opt/base/solvers.py", line 740, in _convert_problem
    **kwds)
  File "/home/esjohn/src/pyomo/pyomo/opt/base/convert.py", line 100, in convert_problem
    problem_files, symbol_map = converter.apply(*tmp, **tmpkw)
  File "/home/esjohn/src/pyomo/pyomo/solvers/plugins/converter/model.py", line 82, in apply
    io_options=io_options)
  File "/home/esjohn/src/pyomo/pyomo/core/base/block.py", line 1812, in write
    io_options)
  File "/home/esjohn/src/pyomo/pyomo/repn/plugins/cpxlp.py", line 172, in __call__
    include_all_variable_bounds=include_all_variable_bounds)
  File "/home/esjohn/src/pyomo/pyomo/repn/plugins/cpxlp.py", line 451, in _print_model_LP
    Var, sort=sortOrder) )
  File "/home/esjohn/src/pyomo/pyomo/core/base/block.py", line 1435, in component_data_objects
    sort=sort):
  File "/home/esjohn/src/pyomo/pyomo/core/base/block.py", line 1361, in _component_data_iter
    _items = sorted(_items, key=itemgetter(0))
TypeError: '<' not supported between instances of 'str' and 'int'
```
Should `_component_data_iter` be using `sorted_robust`?


Discussion:
I think the LP writer can now be switched to not sort by default since sets are now ordered by default?
Although it probably still makes sense to fix Block._component_data_iter
Yeah, this also means you can't call `m.component_data_objects(Var, sort=SortComponents.deterministic)` which is a thing that several transformations need to do, at least.

Resolving patch:
diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py
--- a/pyomo/core/base/block.py
+++ b/pyomo/core/base/block.py
@@ -38,6 +38,7 @@
 from pyomo.core.base.indexed_component import (
     ActiveIndexedComponent, UnindexedComponent_set,
 )
+from pyomo.core.base.misc import sorted_robust
 
 from pyomo.opt.base import ProblemFormat, guess_format
 from pyomo.opt import WriterFactory
@@ -1358,7 +1359,7 @@ def _component_data_iter(self, ctype=None, active=None, sort=False):
                 _items = ((None, comp),)
 
             if _sort_indices:
-                _items = sorted(_items, key=itemgetter(0))
+                _items = sorted_robust(_items, key=itemgetter(0))
             if active is None or not isinstance(comp, ActiveIndexedComponent):
                 for idx, compData in _items:
                     yield (name, idx), compData
diff --git a/pyomo/core/base/misc.py b/pyomo/core/base/misc.py
--- a/pyomo/core/base/misc.py
+++ b/pyomo/core/base/misc.py
@@ -14,6 +14,8 @@
 import sys
 import types
 
+from pyomo.core.expr import native_numeric_types
+
 logger = logging.getLogger('pyomo.core')
 
 
@@ -110,8 +112,11 @@ class _robust_sort_keyfcn(object):
     _typemap without resorting to global variables.
 
     """
-    def __init__(self):
-        self._typemap = {tuple: (3, tuple.__name__)}
+    def __init__(self, key=None):
+        # sort all native numeric types as if they were floats
+        self._typemap = {t: (1, float.__name__) for t in native_numeric_types}
+        self._typemap[tuple] =(3, tuple.__name__)
+        self._key = key
 
     def __call__(self, val):
         """Generate a tuple ( str(type_name), val ) for sorting the value.
@@ -122,6 +127,9 @@ def __call__(self, val):
         argument of the sort key.
 
         """
+        if self._key is not None:
+            val = self._key(val)
+
         try:
             i, _typename = self._typemap[val.__class__]
         except KeyError:
@@ -155,7 +163,7 @@ def __call__(self, val):
             return _typename, id(val)
 
 
-def sorted_robust(arg):
+def sorted_robust(arg, key=None, reverse=False):
     """Utility to sort an arbitrary iterable.
 
     This returns the sorted(arg) in a consistent order by first tring
@@ -166,16 +174,16 @@ def sorted_robust(arg):
     """
     # It is possible that arg is a generator.  We need to cache the
     # elements returned by the generator in case 'sorted' raises an
-    # exception (this ensures we don't loose any elements).  Howevver,
+    # exception (this ensures we don't lose any elements).  However,
     # if we were passed a list, we do not want to make an unnecessary
     # copy.  Tuples are OK because tuple(a) will not copy a if it is
     # already a tuple.
     if type(arg) is not list:
         arg = tuple(arg)
     try:
-        return sorted(arg)
+        return sorted(arg, key=key, reverse=reverse)
     except:
-        return sorted(arg, key=_robust_sort_keyfcn())
+        return sorted(arg, key=_robust_sort_keyfcn(key), reverse=reverse)
 
 
 def _to_ustr(obj):


Test patch:
diff --git a/pyomo/core/tests/unit/test_base_misc.py b/pyomo/core/tests/unit/test_base_misc.py
--- a/pyomo/core/tests/unit/test_base_misc.py
+++ b/pyomo/core/tests/unit/test_base_misc.py
@@ -22,8 +22,8 @@ def test_unicode_table(self):
         # Test that an embedded unicode character does not foul up the
         # table alignment
         os = StringIO()
-        data = {1: ("a", 1), (2,3): ("∧", 2)}
-        tabular_writer(os, "", data.items(), ["s", "val"], lambda k,v: v)
+        data = {1: ("a", 1), (2, 3): ("∧", 2)}
+        tabular_writer(os, "", data.items(), ["s", "val"], lambda k, v: v)
         ref = u"""
 Key    : s : val
      1 : a :   1
@@ -35,21 +35,26 @@ def test_unicode_table(self):
 class TestSortedRobust(unittest.TestCase):
     def test_sorted_robust(self):
         # Note: as types are sorted by name, int < str < tuple
-        a = sorted_robust([3,2,1])
-        self.assertEqual(a, [1,2,3])
+        a = sorted_robust([3, 2, 1])
+        self.assertEqual(a, [1, 2, 3])
 
-        a = sorted_robust([3,'2',1])
-        self.assertEqual(a, [1,3,'2'])
+        # Testthat ints and floats are sorted as "numbers"
+        a = sorted_robust([3, 2.1, 1])
+        self.assertEqual(a, [1, 2.1, 3])
 
-        a = sorted_robust([('str1','str1'), (1, 'str2')])
-        self.assertEqual(a, [(1, 'str2'), ('str1','str1')])
+        a = sorted_robust([3, '2', 1])
+        self.assertEqual(a, [1, 3, '2'])
 
-        a = sorted_robust([((1,), 'str2'), ('str1','str1')])
-        self.assertEqual(a, [('str1','str1'), ((1,), 'str2')])
+        a = sorted_robust([('str1', 'str1'), (1, 'str2')])
+        self.assertEqual(a, [(1, 'str2'), ('str1', 'str1')])
+
+        a = sorted_robust([((1,), 'str2'), ('str1', 'str1')])
+        self.assertEqual(a, [('str1', 'str1'), ((1,), 'str2')])
+
+        a = sorted_robust([('str1', 'str1'), ((1,), 'str2')])
+        self.assertEqual(a, [('str1', 'str1'), ((1,), 'str2')])
 
-        a = sorted_robust([('str1','str1'), ((1,), 'str2')])
-        self.assertEqual(a, [('str1','str1'), ((1,), 'str2')])
 
 if __name__ == "__main__":
     unittest.main()
-    
+
diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py
--- a/pyomo/core/tests/unit/test_block.py
+++ b/pyomo/core/tests/unit/test_block.py
@@ -342,6 +342,15 @@ def test_Block(self):
         self.assertEqual(sorted([id(comp) for comp in model.block_data_objects(sort=False)]),
                          sorted([id(comp) for comp in [model,]+model.component_data_lists[Block]]))
 
+    def test_mixed_index_type(self):
+        m = ConcreteModel()
+        m.I = Set(initialize=[1,'1',3.5,4])
+        m.x = Var(m.I)
+        v = list(m.component_data_objects(Var, sort=True))
+        self.assertEqual(len(v), 4)
+        for a,b in zip([m.x[1], m.x[3.5], m.x[4], m.x['1']], v):
+            self.assertIs(a, b)
+
 
 class HierarchicalModel(object):
     def __init__(self):