Repository: dwavesystems/dimod
Pull request #28 (2017-11-30 01:07:09)

WIP: 0.4.0


Patch:
diff --git a/dimod/__init__.py b/dimod/__init__.py
index 8249f1ec..4bed3cdb 100644
--- a/dimod/__init__.py
+++ b/dimod/__init__.py
@@ -24,4 +24,7 @@ import dimod.responses
 from dimod.utilities import *
 import dimod.utilities
 
+from dimod.keyword_arguments import *
+import dimod.keyword_arguments
+
 import dimod.decorators
diff --git a/dimod/composites/spin_transform.py b/dimod/composites/spin_transform.py
index 9f3df4c3..379df636 100644
--- a/dimod/composites/spin_transform.py
+++ b/dimod/composites/spin_transform.py
@@ -11,11 +11,14 @@ from dimod.utilities import ising_to_qubo, qubo_to_ising
 __all__ = ['SpinReversalTransform']
 
 if _PY2:
-    iteritems = lambda d: d.iteritems()
     range = xrange
     zip = itertools.izip
+
+    def iteritems(d):
+        return d.iteritems()
 else:
-    iteritems = lambda d: d.items()
+    def iteritems(d):
+        return d.items()
 
 
 class SpinReversalTransform(TemplateComposite):
diff --git a/dimod/decorators.py b/dimod/decorators.py
index 47f35de0..77ae5a04 100644
--- a/dimod/decorators.py
+++ b/dimod/decorators.py
@@ -10,9 +10,13 @@ __all__ = ['qubo', 'ising', 'qubo_index_labels', 'ising_index_labels']
 
 if _PY2:
     range = xrange
-    iteritems = lambda d: d.iteritems()
+
+    def iteritems(d):
+        return d.iteritems()
+
 else:
-    iteritems = lambda d: d.items()
+    def iteritems(d):
+        return d.items()
 
 
 def qubo(Q_arg):
diff --git a/dimod/exceptions.py b/dimod/exceptions.py
new file mode 100644
index 00000000..4dbdb770
--- /dev/null
+++ b/dimod/exceptions.py
@@ -0,0 +1,2 @@
+class InvalidComposition(Exception):
+    """Used for compositions of samplers that are invalid"""
diff --git a/dimod/keyword_arguments.py b/dimod/keyword_arguments.py
new file mode 100644
index 00000000..358a0166
--- /dev/null
+++ b/dimod/keyword_arguments.py
@@ -0,0 +1,37 @@
+class SamplerKeywordArg(object):
+    """Allows for passing information about keyword parameters accepted
+    by dimod samplers.
+
+    Args:
+        name (str): The keyword argument's name.
+        type_annotation (str): The keyword type as a string.
+        classinfo (type): The expected type of the parameter.
+
+    Attributes:
+        name (str): The keyword argument's name.
+        type_annotation (str): The keyword type as a string.
+        classinfo (type): The expected type of the parameter.
+
+    Examples:
+        >>> num_samples_kwarg = SamplerKeywordArg('num_samples', 'int', int)
+        >>> embedding_kwarg = SamplerKeywordArg('embedding', 'dict[hashable, iterable]', dict)
+
+    """
+    # it would be cool if isinstance(6, SamplerKeywordArg('num_samples', 'int', int)) => True
+    # if you can get it to work then please do!
+
+    def __init__(self, name, type_annotation='', classinfo=object):
+        if not isinstance(name, str):
+            raise TypeError("expected input 'name' to be a str.")
+        self.name = name
+
+        if not isinstance(type_annotation, str):
+            raise TypeError("expected input 'type_annotation' to be a str.")
+        self.type_annotation = type_annotation
+
+        if not isinstance(classinfo, type):
+            raise TypeError("expected input 'classinfo' to be a type.")
+        self.classinfo = classinfo
+
+    def __str__(self):
+        return "SamplerKeywordArg('{}', '{}', {})".format(self.name, self.type_annotation, self.classinfo)
diff --git a/dimod/samplers/brute_force.py b/dimod/samplers/brute_force.py
index 07fa8556..42676cc1 100644
--- a/dimod/samplers/brute_force.py
+++ b/dimod/samplers/brute_force.py
@@ -1,8 +1,6 @@
-"""A exact solver that calculates the energy of all possible samples.
+"""An exact solver that calculates the energy of all possible samples.
 """
-
-import sys
-
+from dimod import _PY2
 from dimod.template_sampler import TemplateSampler
 from dimod.decorators import ising, ising_index_labels
 from dimod.responses import SpinResponse
@@ -10,11 +8,15 @@ from dimod.utilities import ising_energy
 
 __all__ = ['ExactSolver']
 
-if sys.version_info[0] == 2:
+if _PY2:
     range = xrange
-    iteritems = lambda d: d.iteritems()
+
+    def iteritems(d):
+        return d.iteritems()
+
 else:
-    iteritems = lambda d: d.items()
+    def iteritems(d):
+        return d.items()
 
 
 class ExactSolver(TemplateSampler):
@@ -76,7 +78,7 @@ class ExactSolver(TemplateSampler):
         response.add_sample(sample.copy(), energy)
 
         # now we iterate, flipping one bit at a time until we have
-        # traversed all samples. This is a gray code.
+        # traversed all samples. This is a Gray code.
         # https://en.wikipedia.org/wiki/Gray_code
         for i in range(1, 1 << len(h)):
             v = _ffs(i)
diff --git a/dimod/samplers/random_sampler.py b/dimod/samplers/random_sampler.py
index e39637c6..42bb23b2 100644
--- a/dimod/samplers/random_sampler.py
+++ b/dimod/samplers/random_sampler.py
@@ -1,21 +1,24 @@
 """
-A random sampler that can be used to unit testing and debugging.
+A random sampler that can be used for unit testing and debugging.
 """
-
-import sys
 import random
 
+from dimod import _PY2
 from dimod.template_sampler import TemplateSampler
 from dimod.decorators import ising, qubo, ising_index_labels
 from dimod.responses import BinaryResponse
 
 __all__ = ['RandomSampler']
 
-if sys.version_info[0] == 2:
+if _PY2:
     range = xrange
-    iteritems = lambda d: d.iteritems()
+
+    def iteritems(d):
+        return d.iteritems()
+
 else:
-    iteritems = lambda d: d.items()
+    def iteritems(d):
+        return d.items()
 
 
 class RandomSampler(TemplateSampler):
diff --git a/dimod/samplers/simulated_annealing.py b/dimod/samplers/simulated_annealing.py
index 0be43cbd..5ae09187 100644
--- a/dimod/samplers/simulated_annealing.py
+++ b/dimod/samplers/simulated_annealing.py
@@ -17,9 +17,13 @@ __all__ = ['SimulatedAnnealingSampler']
 
 if _PY2:
     range = xrange
-    itervalues = lambda d: d.itervalues()
+
+    def itervalues(d):
+        return d.itervalues()
+
 else:
-    itervalues = lambda d: d.values()
+    def itervalues(d):
+        return d.values()
 
 
 class SimulatedAnnealingSampler(TemplateSampler):
@@ -61,8 +65,8 @@ class SimulatedAnnealingSampler(TemplateSampler):
             [{0: 1, 1: 1}]
 
         Note:
-            This is a reference implementation and not optimized for speed
-            and therefore is not an appropriate sampler for benchmarking.
+            This is a reference implementation, not optimized for speed
+            and therefore not an appropriate sampler for benchmarking.
 
         """
 
@@ -158,7 +162,7 @@ def ising_simulated_annealing(h, J, beta_range=None, num_sweeps=1000):
         adj[n0].add(n1)
         adj[n1].add(n0)
 
-    # we will use a vertex coloring the the graph and update the nodes by color. A quick
+    # we will use a vertex coloring for the graph and update the nodes by color. A quick
     # greedy coloring will be sufficient.
     __, colors = greedy_coloring(adj)
 
@@ -168,8 +172,8 @@ def ising_simulated_annealing(h, J, beta_range=None, num_sweeps=1000):
     # there are exactly as many betas as sweeps
     for beta in betas:
 
-        # we want to know the gain in energy for flipping each of the spins
-        # we can calculate all of the linear terms simultaneously
+        # we want to know the gain in energy for flipping each of the spins.
+        # We can calculate all of the linear terms simultaneously
         energy_diff_h = {v: -2 * spins[v] * h[v] for v in h}
 
         # for each color, do updates
@@ -216,7 +220,7 @@ def greedy_coloring(adj):
         dict: the colors {color: [node, ...], ...}
 
     Note:
-        This is a greedy heuristic, the resulting coloring is not
+        This is a greedy heuristic: the resulting coloring is not
         necessarily minimal.
 
     """
diff --git a/dimod/template_composite.py b/dimod/template_composite.py
index 5b242cf0..5f59a5fc 100644
--- a/dimod/template_composite.py
+++ b/dimod/template_composite.py
@@ -57,3 +57,15 @@ class TemplateComposite(TemplateSampler):
     def __init__(self, *samplers):
         TemplateSampler.__init__(self)
         self.children = list(samplers)
+
+    @property
+    def accepted_kwargs(self):
+        """dict[str: :class:`.SamplerKeywordArg`]: The keyword arguments
+        accepted by the `sample_ising` and `sample_qubo` methods for this
+        sampler.
+        """
+        kwargs = {}
+        for child in self.children:
+            kwargs.update(child.accepted_kwargs)
+        kwargs.update(self.my_kwargs())
+        return kwargs
diff --git a/dimod/template_response.py b/dimod/template_response.py
index 6fa5b68e..a6f7e4c5 100644
--- a/dimod/template_response.py
+++ b/dimod/template_response.py
@@ -113,11 +113,18 @@ __all__ = ['TemplateResponse']
 if _PY2:
     range = xrange
     zip = itertools.izip
-    iteritems = lambda d: d.iteritems()
-    itervalues = lambda d: d.itervalues()
+
+    def iteritems(d):
+        return d.iteritems()
+
+    def itervalues(d):
+        return d.itervalues()
 else:
-    iteritems = lambda d: d.items()
-    itervalues = lambda d: d.values()
+    def iteritems(d):
+        return d.items()
+
+    def itervalues(d):
+        return d.values()
 
 
 class TemplateResponse(object):
diff --git a/dimod/template_sampler.py b/dimod/template_sampler.py
index b5fb28c1..fe3b0e0c 100644
--- a/dimod/template_sampler.py
+++ b/dimod/template_sampler.py
@@ -69,9 +69,11 @@ In this case, the `structure` property will not be None.
 
 """
 # we could do a solver that requires a complete graph as an example above...
+import inspect
 
-
+from dimod import _PY2
 from dimod.decorators import ising, qubo
+from dimod.keyword_arguments import SamplerKeywordArg
 from dimod.utilities import qubo_to_ising, ising_to_qubo
 
 __all__ = ['TemplateSampler']
@@ -158,3 +160,69 @@ class TemplateSampler(object):
     @structure.setter
     def structure(self, struct):
         self._structure = struct
+
+    @property
+    def accepted_kwargs(self):
+        """dict[str: :class:`.SamplerKeywordArg`]: The keyword arguments
+        accepted by the `sample_ising` and `sample_qubo` methods for this
+        sampler.
+        """
+        return self.my_kwargs()
+
+    def my_kwargs(self):
+        """The keyword arguments accepted by the sampler or the highest
+        composite layer.
+
+        Returns:
+            dict[str: :class:`.SamplerKeywordArg`]: The keyword arguments
+            accepted by the `sample_ising` and `sample_qubo` methods for this
+            sampler or the top-level composite layer. For all accepted keyword
+            arguments see `accepted_kwargs`.
+
+        Note:
+            This method is inherited from the :obj:`.TemplateSampler` base class.
+
+        """
+        if _PY2:
+            # we need to use getargspec in python 2
+            ising_spec = inspect.getargspec(self.sample_ising)
+            qubo_spec = inspect.getargspec(self.sample_qubo)
+
+            kwargs = {}
+            for name in ising_spec.args:
+                if name == 'self':
+                    continue
+                elif name == 'h':
+                    kwargs[name] = SamplerKeywordArg(name, 'dict[hashable, numeric]', dict)
+                elif name == 'J':
+                    kwargs[name] = SamplerKeywordArg(name, 'dict[(hashable, hashable), numeric]', dict)
+                else:
+                    kwargs[name] = SamplerKeywordArg(name)
+            for name in qubo_spec.args:
+                if name == 'self':
+                    continue
+                elif name == 'Q':
+                    kwargs[name] = SamplerKeywordArg(name, 'dict[(hashable, hashable), numeric]', dict)
+                else:
+                    kwargs[name] = SamplerKeywordArg(name)
+        else:
+            # in python 3 we can use signature
+            ising_sig = inspect.signature(self.sample_ising)
+            qubo_sig = inspect.signature(self.sample_qubo)
+
+            kwargs = {}
+            for name, param in qubo_sig.parameters.items():
+                if param.kind == param.POSITIONAL_OR_KEYWORD:
+                    if name == 'Q':
+                        kwargs[name] = SamplerKeywordArg(name, 'dict[(hashable, hashable), numeric]', dict)
+                    else:
+                        kwargs[name] = SamplerKeywordArg(name)
+            for name, param in ising_sig.parameters.items():
+                if param.kind == param.POSITIONAL_OR_KEYWORD:
+                    if name == 'h':
+                        kwargs[name] = SamplerKeywordArg(name, 'dict[hashable, numeric]', dict)
+                    elif name == 'J':
+                        kwargs[name] = SamplerKeywordArg(name, 'dict[(hashable, hashable), numeric]', dict)
+                    else:
+                        kwargs[name] = SamplerKeywordArg(name)
+        return kwargs
diff --git a/dimod/utilities.py b/dimod/utilities.py
index aadf66f7..bb85b7b2 100644
--- a/dimod/utilities.py
+++ b/dimod/utilities.py
@@ -2,17 +2,22 @@
 A collection of utility functions useful for Samplers.
 """
 
-import sys
+from dimod import _PY2
 
 __all__ = ['ising_energy', 'qubo_energy', 'ising_to_qubo', 'qubo_to_ising']
 
-PY2 = sys.version_info[0] == 2
-if PY2:
-    iteritems = lambda d: d.iteritems()
-    itervalues = lambda d: d.itervalues()
+if _PY2:
+    def iteritems(d):
+        return d.iteritems()
+
+    def itervalues(d):
+        return d.itervalues()
 else:
-    iteritems = lambda d: d.items()
-    itervalues = lambda d: d.values()
+    def iteritems(d):
+        return d.items()
+
+    def itervalues(d):
+        return d.values()
 
 
 def ising_energy(h, J, sample):
diff --git a/docs/template_sampler.rst b/docs/template_sampler.rst
index ab7ab356..57f317e8 100644
--- a/docs/template_sampler.rst
+++ b/docs/template_sampler.rst
@@ -17,6 +17,7 @@ TemplateSampler methods
 
    TemplateSampler.sample_ising
    TemplateSampler.sample_qubo
+   TemplateSampler.my_kwargs
 
 TemplateSampler properties
 --------------------------
@@ -25,4 +26,5 @@ TemplateSampler properties
    :toctree: generated/
 
    TemplateSampler.structure
+   TemplateSampler.accepted_kwargs
    
\ No newline at end of file


Test patch:
diff --git a/dimod/composites/tests/test_spin_transform.py b/dimod/composites/tests/test_spin_transform.py
index f49fdacf..bc0791b7 100644
--- a/dimod/composites/tests/test_spin_transform.py
+++ b/dimod/composites/tests/test_spin_transform.py
@@ -105,6 +105,12 @@ class TestSpinTransformComposition(unittest.TestCase, SamplerAPITest):
         # should be twice as many samples
         self.assertEqual(len(response2), 2 * len(response))
 
+    def test_kwarg_propogation_composite(self):
+        sampler = dimod.SpinReversalTransform(self.sampler)
+
+        # kwargs should have propogated through, correctness is tested elsewhere
+        self.assertIn('orig_h', sampler.accepted_kwargs)
+
 
 class TestSpinTransform(unittest.TestCase):
     def test_function_typical(self):
diff --git a/dimod/samplers/tests/generic_sampler_tests.py b/dimod/samplers/tests/generic_sampler_tests.py
index ed6c2f8e..daa65366 100644
--- a/dimod/samplers/tests/generic_sampler_tests.py
+++ b/dimod/samplers/tests/generic_sampler_tests.py
@@ -1,4 +1,4 @@
-"""These are generic tests that can be applied to any sampler, this should
+"""These are generic tests that can be applied to any sampler. This should
 not be run as a unittest.
 
 Example:
@@ -9,6 +9,7 @@ Example:
     This will run all of the tests in TestSamplerAPI
 
 """
+import dimod
 
 
 class SamplerAPITest:
@@ -60,3 +61,8 @@ class SamplerAPITest:
         for var in variables:
             for soln in response:
                 self.assertIn(var, soln)
+
+    def test_arg_propogation(self):
+        self.assertIsInstance(self.sampler.accepted_kwargs, dict)
+        for name, param in self.sampler.accepted_kwargs.items():
+            self.assertIsInstance(param, dimod.SamplerKeywordArg)
diff --git a/dimod/samplers/tests/test_brute_force.py b/dimod/samplers/tests/test_brute_force.py
index d5c126ef..07b47a4d 100644
--- a/dimod/samplers/tests/test_brute_force.py
+++ b/dimod/samplers/tests/test_brute_force.py
@@ -11,7 +11,7 @@ class TestExactSolver(unittest.TestCase, SamplerAPITest):
         self.sampler = ExactSolver()
 
     def test_all_samples(self):
-        """Check that every sample is included and that they all have the correct energy."""
+        """Check that every sample is included and has the correct energy."""
 
         n = 10
 
@@ -41,3 +41,9 @@ class TestExactSolver(unittest.TestCase, SamplerAPITest):
 
         for energy in response.energies():
             self.assertEqual(energy, 0.0)
+
+    def test_keyword_propogation_brute_force(self):
+        sampler = self.sampler
+
+        # no extra args
+        self.assertEqual(set(sampler.accepted_kwargs), {'h', 'J', 'Q'})
diff --git a/dimod/samplers/tests/test_random_sampler.py b/dimod/samplers/tests/test_random_sampler.py
index 3e4093be..54b3cee0 100644
--- a/dimod/samplers/tests/test_random_sampler.py
+++ b/dimod/samplers/tests/test_random_sampler.py
@@ -9,3 +9,9 @@ class TestRandomSampler(unittest.TestCase, SamplerAPITest):
 
     def setUp(self):
         self.sampler = RandomSampler()
+
+    def test_keyword_propogation_random_sampler(self):
+        sampler = self.sampler
+
+        # no extra args
+        self.assertEqual(set(sampler.accepted_kwargs), {'h', 'J', 'Q', 'num_samples'})
diff --git a/dimod/samplers/tests/test_simulated_annealing.py b/dimod/samplers/tests/test_simulated_annealing.py
index 1e4df53e..8a6af3a5 100644
--- a/dimod/samplers/tests/test_simulated_annealing.py
+++ b/dimod/samplers/tests/test_simulated_annealing.py
@@ -81,6 +81,12 @@ class TestSASampler(unittest.TestCase, SamplerAPITest):
         with self.assertRaises(ValueError):
             sampler.sample_ising({}, {}, beta_range=[7, 1, 6])
 
+    def test_keyword_propogation_random_sampler(self):
+        sampler = self.sampler
+
+        # no extra args
+        self.assertEqual(set(sampler.accepted_kwargs), {'h', 'J', 'Q', 'num_samples', 'beta_range', 'num_sweeps'})
+
 
 class TestSimulatedAnnealingAlgorithm(unittest.TestCase):
     def test_ising_simulated_annealing_basic(self):
@@ -111,8 +117,8 @@ class TestSimulatedAnnealingAlgorithm(unittest.TestCase):
 
     def test_ising_simulated_annealing_sample_quality(self):
         # because simulated annealing has randomness, we cannot
-        # really test that it finds the solution. So instead we
-        # note that it should return better than average solutions
+        # really test that it finds the solution. Instead we
+        # note that it should return better-than-average solutions,
         # so if we test the returned energy against the energy of
         # 100 random samples, it should do better than the average
         nV = 100  # number of variables in h,J
@@ -140,7 +146,7 @@ class TestSimulatedAnnealingAlgorithm(unittest.TestCase):
 
         adj = {node: set() for node in range(N)}
 
-        # add randomly approximately 5% of the edges
+        # randomly add approximately 5% of the edges
         for u, v in itertools.combinations(range(N), 2):
             if random.random() < .05:
                 adj[u].add(v)
diff --git a/dimod/tests/test_keyword_arguments.py b/dimod/tests/test_keyword_arguments.py
new file mode 100644
index 00000000..cce49e82
--- /dev/null
+++ b/dimod/tests/test_keyword_arguments.py
@@ -0,0 +1,28 @@
+import unittest
+
+from dimod import SamplerKeywordArg
+
+
+class TestSamplerKeywordArg(unittest.TestCase):
+    def test_docstring(self):
+        num_samples_kwarg = SamplerKeywordArg('num_samples', 'int', int)
+        embedding_kwarg = SamplerKeywordArg('embedding', 'dict[hashable, iterable]', dict)
+
+    def test_isinstancec(self):
+        num_samples_kwarg = SamplerKeywordArg('num_samples', 'int', int)
+
+        self.assertTrue(isinstance(6, num_samples_kwarg.classinfo))
+
+    def test_string(self):
+        num_samples_kwarg = SamplerKeywordArg('num_samples', 'int', int)
+        self.assertIsInstance(num_samples_kwarg.__str__(), str)
+
+    def test_exceptions(self):
+        with self.assertRaises(TypeError):
+            num_samples_kwarg = SamplerKeywordArg('num_samples', 'int', 'int')
+
+        with self.assertRaises(TypeError):
+            num_samples_kwarg = SamplerKeywordArg('num_samples', int, int)
+
+        with self.assertRaises(TypeError):
+            num_samples_kwarg = SamplerKeywordArg(100, 'int', int)