Repository: dwavesystems/dimod
Issue: sampleset.to_serializable to check variable indices

when using to_serializable, seems like it doesn't check the type of variable indices.
if np.int64 is sent it as variable indices, sampleset.to_serializable() remains not bson compatible. 

would be nice if we can check that and convert during the process

Changed files:
dimod/variables.py, tests/test_binary_quadratic_model.py, tests/test_sampleset.py, dimod/sampleset.py, dimod/binary_quadratic_model.py

diff --git a/dimod/binary_quadratic_model.py b/dimod/binary_quadratic_model.py
index be17433b..1288c6ef 100644
--- a/dimod/binary_quadratic_model.py
+++ b/dimod/binary_quadratic_model.py
@@ -53,7 +53,7 @@ try:
 except ImportError:
     import collections as abc
 
-from numbers import Number
+from numbers import Integral, Number
 
 import numpy as np
 
@@ -65,6 +65,7 @@ from dimod.sampleset import as_samples
 from dimod.utilities import resolve_label_conflict, LockableDict
 from dimod.views.bqm import LinearView, QuadraticView, AdjacencyView
 from dimod.views.samples import SampleView
+from dimod.variables import iter_serialize_variables
 from dimod.vartypes import Vartype
 
 __all__ = ['BinaryQuadraticModel', 'BQM']
@@ -1739,11 +1740,13 @@ class BinaryQuadraticModel(abc.Sized, abc.Container, abc.Iterable):
         from dimod.package_info import __version__
         schema_version = "2.0.0"
 
+        variables = list(iter_serialize_variables(self.variables))
+
         try:
-            variables = sorted(self.variables)
+            variables.sort()
         except TypeError:
-            # sorting unlike types in py3
-            variables = list(self.variables)
+            # cannot unlike types in py3
+            pass
 
         num_variables = len(variables)
 
diff --git a/dimod/sampleset.py b/dimod/sampleset.py
index b257fb87..c2efd78f 100644
--- a/dimod/sampleset.py
+++ b/dimod/sampleset.py
@@ -1359,7 +1359,7 @@ class SampleSet(abc.Iterable, abc.Sized):
                 "info": self.info,
                 "version": {"dimod": __version__,
                             "sampleset_schema": schema_version},
-                "variable_labels": list(self.variables),
+                "variable_labels": self.variables.to_serializable(),
                 "use_bytes": bool(use_bytes)}
 
     def _asdict(self):
diff --git a/dimod/variables.py b/dimod/variables.py
index b40060ce..b7b054bd 100644
--- a/dimod/variables.py
+++ b/dimod/variables.py
@@ -13,6 +13,8 @@
 #    limitations under the License.
 #
 # =============================================================================
+from numbers import Integral, Number
+
 try:
     import collections.abc as abc
 except ImportError:
@@ -26,6 +28,8 @@ from dimod.utilities import resolve_label_conflict
 from six import PY3
 from six.moves import map
 
+__all__ = ['Variables']
+
 
 class CallableDict(abc.Callable, dict):
     """Dict that can be accessed like a function."""
@@ -37,6 +41,23 @@ class CallableDict(abc.Callable, dict):
         return self[v]
 
 
+def iter_serialize_variables(variables):
+    # want to handle things like numpy numbers and fractions that do not
+    # serialize so easy
+    for v in variables:
+        if isinstance(v, Integral):
+            yield int(v)
+        elif isinstance(v, Number):
+            yield float(v)
+        elif isinstance(v, str):
+            yield v
+        # we want Collection, but that's not available in py2.7
+        elif isinstance(v, (abc.Sequence, abc.Set)):
+            yield tuple(iter_serialize_variables(v))
+        else:
+            yield v
+
+
 class Variables(abc.Sequence, abc.Set):
     """set-like and list-like variable tracking.
 
@@ -116,6 +137,9 @@ class Variables(abc.Sequence, abc.Set):
         # everything is unique
         return int(v in self)
 
+    def to_serializable(self):
+        return list(iter_serialize_variables(self))
+
     @lockable_method
     def relabel(self, mapping):
 
diff --git a/tests/test_binary_quadratic_model.py b/tests/test_binary_quadratic_model.py
index 37a69a69..a761cf5c 100644
--- a/tests/test_binary_quadratic_model.py
+++ b/tests/test_binary_quadratic_model.py
@@ -14,14 +14,15 @@
 #
 # ================================================================================================
 
-import unittest
-import random
+import collections
+import fractions
 import itertools
-import tempfile
-import shutil
 import json
-import collections
 import pickle
+import random
+import shutil
+import tempfile
+import unittest
 
 from os import path
 
@@ -2221,6 +2222,16 @@ class TestSerialization(unittest.TestCase):
         self.assertEqual(bqm, new)
         self.assertEqual(new.info, {"tag": 5})
 
+    def test_variable_labels(self):
+        h = {0: 0, 1: 1, np.int64(2): 2, np.float(3): 3,
+             fractions.Fraction(4, 1): 4, fractions.Fraction(5, 2): 5,
+             '6': 6}
+        J = {}
+
+        bqm = dimod.BinaryQuadraticModel.from_ising(h, J)
+
+        json.dumps(bqm.to_serializable())
+
 
 class TestZeroField(unittest.TestCase):
     # test when there are only quadratic biases
diff --git a/tests/test_sampleset.py b/tests/test_sampleset.py
index 22f3be24..2f03c9c6 100644
--- a/tests/test_sampleset.py
+++ b/tests/test_sampleset.py
@@ -14,6 +14,7 @@
 #
 # ================================================================================================
 import unittest
+import fractions
 import json
 import pickle
 
@@ -648,6 +649,15 @@ class TestSerialization(unittest.TestCase):
 
         self.assertEqual(sampleset, new)
 
+    def test_numpy_variable_labels(self):
+        h = {0: 0, 1: 1, np.int64(2): 2, np.float(3): 3,
+             fractions.Fraction(4, 1): 4, fractions.Fraction(5, 2): 5,
+             '6': 6}
+
+        sampleset = dimod.NullSampler().sample_ising(h, {})
+
+        json.dumps(sampleset.to_serializable())
+
 
 @unittest.skipUnless(_pandas, "no pandas present")
 class TestPandas(unittest.TestCase):