Repository: dwavesystems/dimod

Issue:
Allow user-specified mapping type for `BinaryQuadraticModel`s
```
bqm = dimod.BinaryQuadraticModel(linear, quadratic, offset, vartype, mapping_type=OrderedDict)
```
This would affect `.linear` and `.quadratic`, possibly `.adj` as well.

Discussion:

Patch:
diff --git a/dimod/binary_quadratic_model.py b/dimod/binary_quadratic_model.py
index 970163c3..3cbe679d 100644
--- a/dimod/binary_quadratic_model.py
+++ b/dimod/binary_quadratic_model.py
@@ -46,12 +46,12 @@ of a problem.
 """
 from __future__ import absolute_import, division
 
-from collections import Sized, Container, Iterable
+from collections import Sized, Container, Iterable, OrderedDict
 from numbers import Number
 
 import numpy as np
 
-from six import itervalues, iteritems, iterkeys, viewkeys
+from six import itervalues, iteritems, iterkeys
 
 from dimod.decorators import vartype_argument
 from dimod.response import SampleView
@@ -263,11 +263,6 @@ class BinaryQuadraticModel(Sized, Container, Iterable):
     def __iter__(self):
         return self.adj.__iter__()
 
-    @property
-    def variables(self):
-        """Returns BQM's variables as a dictionary view object."""
-        return viewkeys(self.linear)
-
 ##################################################################################################
 # vartype properties
 ##################################################################################################
@@ -2200,3 +2195,37 @@ class BinaryQuadraticModel(Sized, Container, Iterable):
             bqm.add_interaction(u, v, 0.0)
 
         return bqm
+
+
+class OrderedBinaryQuadraticModel(BinaryQuadraticModel):
+    """Consistently ordered variant of :class:`BinaryQuadraticModel`.
+
+    Uses :class:`collections.OrderedDict` to store the linear and quadratic biases. Note that
+    :attr:`~.BinaryQuadraticModel.adj` remains unordered.
+
+    Variables are ordered by insertion. This is well defined if adding the variable/interactions
+    singly, but not when constructed from unordered mappings like dicts.
+
+    Examples:
+
+        >>> bqm = dimod.OrderedBinaryQuadraticModel.empty(dimod.SPIN)
+        >>> bqm.add_variable('a', .5)
+        >>> bqm.add_interaction('a', 'b', 1.5)
+        >>> bqm.linear
+        OrderedDict([('a', 0.5), ('b', 0.0)])
+        >>> bqm.quadratic
+        OrderedDict([(('a', 'b'), 1.5)])
+
+    """
+    @vartype_argument('vartype')
+    def __init__(self, linear, quadratic, offset, vartype, **kwargs):
+        self.linear = OrderedDict()
+        self.quadratic = OrderedDict()
+        self.adj = {}
+        self.offset = offset  # we are agnostic to type, though generally should behave like a number
+        self.vartype = vartype
+        self.info = kwargs  # any additional kwargs are kept as info (metadata)
+
+        # add linear, quadratic
+        self.add_variables_from(linear)
+        self.add_interactions_from(quadratic)
diff --git a/docs/reference/binary_quadratic_model.rst b/docs/reference/binary_quadratic_model.rst
index a2d55fea..ac3d3ce4 100644
--- a/docs/reference/binary_quadratic_model.rst
+++ b/docs/reference/binary_quadratic_model.rst
@@ -108,3 +108,9 @@ Converting to other types
    BinaryQuadraticModel.to_numpy_vectors
    BinaryQuadraticModel.to_qubo
    BinaryQuadraticModel.to_pandas_dataframe
+
+
+OrderedBinaryQuadraticModel
+===========================
+
+.. autoclass:: OrderedBinaryQuadraticModel


Test patch:
diff --git a/tests/test_binary_quadratic_model.py b/tests/test_binary_quadratic_model.py
index de1211b3..958e1904 100644
--- a/tests/test_binary_quadratic_model.py
+++ b/tests/test_binary_quadratic_model.py
@@ -20,6 +20,8 @@ import itertools
 import tempfile
 import shutil
 import json
+import collections
+
 from os import path
 
 import jsonschema
@@ -135,7 +137,6 @@ class TestBinaryQuadraticModel(unittest.TestCase):
 
         self.assertEqual(dimod.BinaryQuadraticModel(linear, quadratic, offset, 'BINARY').vartype, dimod.BINARY)
 
-
     def test_construction_quadratic(self):
         linear = {v: v * .01 for v in range(10)}
         quadratic = {(u, v): u * v * .01 for u, v in itertools.combinations(linear, 2)}
@@ -226,18 +227,6 @@ class TestBinaryQuadraticModel(unittest.TestCase):
 
         self.assertEqual(set(bqm), {'a', 'b'})
 
-    def test_variables(self):
-        bqm = dimod.BinaryQuadraticModel.empty(dimod.BINARY)
-
-        self.assertEqual(set(bqm.variables), set())
-
-        bqm.add_interaction('a', 'b', -1)
-
-        self.assertEqual(set(bqm.variables), {'a', 'b'})
-        self.assertIn('a', bqm.variables)
-        self.assertEqual(bqm.variables & {'b'}, {'b'})
-        self.assertEqual(bqm.variables | {'c'}, {'a', 'b', 'c'})
-
     def test_add_variable(self):
         bqm = dimod.BinaryQuadraticModel({}, {('a', 'b'): -1}, 0.0, dimod.SPIN)
         bqm.add_variable('a', .5)
@@ -1887,3 +1876,18 @@ class TestConvert(unittest.TestCase):
         self.assertIn('tag', new_bqm.info)
         self.assertEqual(new_bqm.info['tag'], 5)
         self.assertIn(('a', "complex key"), new_bqm.linear)
+
+
+class TestOrderedBQM(unittest.TestCase):
+    def test_construction(self):
+        bqm = dimod.OrderedBinaryQuadraticModel.empty(dimod.SPIN)
+        bqm.add_variable('a', .5)
+        bqm.add_interaction('c', 'a', 1.5)
+        bqm.add_interaction('a', 'c', -1.)
+        bqm.add_interaction('a', 'b', -1.)
+        bqm.add_variables_from([('b', 1.0), ('c', 1.0)])
+        bqm.add_interaction('e', 'd', 0.0)
+
+        self.assertEqual(bqm.linear, collections.OrderedDict([('a', .5), ('c', 1.), ('b', 1.), ('e', 0.), ('d', 0.)]))
+        self.assertEqual(bqm.quadratic,
+                         collections.OrderedDict([(('c', 'a'), .5), (('a', 'b'), -1.), (('e', 'd'), 0.0)]))