Skip to content

Commit

Permalink
Add ExactDQMSampler
Browse files Browse the repository at this point in the history
See #820
  • Loading branch information
necaisej authored Jun 8, 2021
1 parent 8adc0ed commit a4bca10
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 3 deletions.
57 changes: 55 additions & 2 deletions dimod/reference/samplers/exact_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@
import numpy as np

from dimod.core.sampler import Sampler
from dimod.sampleset import SampleSet
from dimod.sampleset import SampleSet, as_samples
from dimod.core.polysampler import PolySampler
from dimod.vartypes import Vartype
from dimod.exceptions import SamplerUnknownArgWarning

__all__ = ['ExactSolver', 'ExactPolySolver']
__all__ = ['ExactSolver', 'ExactPolySolver', 'ExactDQMSolver']


class ExactSolver(Sampler):
Expand Down Expand Up @@ -159,6 +160,47 @@ def sample_poly(self, polynomial, **kwargs):
"""
return ExactSolver().sample(polynomial, **kwargs)

class ExactDQMSolver():
"""A simple exact solver for testing and debugging code using your local CPU.
Notes:
This solver calculates the energy for every possible
combination of variable cases. If variable i has
k_i many cases, this will be k_1 * k_2 * ... * k_n
which grows exponentially for constant k_i in the
number of variables.
"""
properties = None
parameters = None

def __init__(self):
self.properties = {}
self.parameters = {}

def sample_dqm(self, dqm, **kwargs):
"""Sample from a discrete quadratic model.
Args:
dqm (:obj:`~dimod.DiscreteQuadraticModel`):
Discrete quadratic model to be sampled from.
Returns:
:obj:`~dimod.SampleSet`
"""
Sampler.remove_unknown_kwargs(self, **kwargs)

n = dqm.num_variables()
if n == 0:
return SampleSet.from_samples([], 'DISCRETE', energy=[])

possible_samples = as_samples((_all_cases_dqm(dqm), list(dqm.variables)))
energies = dqm.energies(possible_samples)

response = SampleSet.from_samples(possible_samples, 'DISCRETE', energies)
return response


def _graycode(bqm):
"""Get a numpy array containing all possible samples in a gray-code order"""
Expand All @@ -177,3 +219,14 @@ def _graycode(bqm):
samples[i, v] = not samples[i - 1, v]

return samples


def _all_cases_dqm(dqm):
"""Get a numpy array containing all possible samples as lists of integers"""
# developer note: there may be better ways to do this, but because we're
# limited in performance by the energy calculation, this is probably fine

cases = [range(dqm.num_cases(v)) for v in dqm.variables]
combinations = np.array(np.meshgrid(*cases)).T.reshape(-1,dqm.num_variables())

return combinations
36 changes: 36 additions & 0 deletions dimod/testing/asserts.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,42 @@ def assert_sampleset_energies(sampleset, bqm, precision=7):
assert round(bqm.energy(sample) - energy, precision) == 0


def assert_sampleset_energies_dqm(sampleset, dqm, precision=7):
"""Assert that each sample in the given sample set has the correct energy.
Args:
sampleset (:obj:`.SampleSet`):
Sample set as returned by a dimod sampler.
dqm (:obj:`.DiscreteQuadraticModel`):
The discrete quadratic model (DQM) used to generate the samples.
precision (int, optional, default=7):
Equality of energy is tested by calculating the difference between
the `response`'s sample energy and that returned by BQM's
:meth:`~.BinaryQuadraticModel.energy`, rounding to the closest
multiple of 10 to the power of minus `precision`.
Raises:
AssertionError: If any of the samples in the sample set do not match
their associated energy.
"""

assert isinstance(sampleset, dimod.SampleSet), "expected sampleset to be a dimod SampleSet object"

for sample, energy in sampleset.data(['sample', 'energy']):
assert isinstance(sample, abc.Mapping), "'for sample in sampleset', each sample should be a Mapping"

for v, value in sample.items():
assert v in dqm.variables, 'sample contains a variable not in the given dqm'

for v in dqm.variables:
assert v in sample, "dqm contains a variable not in sample"

assert round(dqm.energy(sample) - energy, precision) == 0


def assert_bqm_almost_equal(actual, desired, places=7,
ignore_zero_interactions=False):
"""Test if two binary quadratic models have almost equal biases.
Expand Down
23 changes: 23 additions & 0 deletions docs/reference/sampler_composites/samplers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@ Methods
ExactSolver.sample_ising
ExactSolver.sample_qubo

Exact DQM Solver
------------

A simple DQM exact solver for testing and debugging code using your local CPU.

Note:
This sampler is designed for use in testing. Because it calculates the
energy for every possible sample, it is very slow.


Class
~~~~~

.. autoclass:: ExactDQMSolver

Methods
~~~~~~~

.. autosummary::
:toctree: generated/

ExactDQMSolver.sample_dqm

Identity Sampler
----------------

Expand Down
34 changes: 33 additions & 1 deletion tests/test_exact_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ def test_sample_BINARY_empty(self):
self.assertEqual(response.record.sample.shape, (0, 0))
self.assertIs(response.vartype, bqm.vartype)

def test_sample_DISCRETE_empty(self):
dqm = dimod.DiscreteQuadraticModel()
response = dimod.ExactDQMSolver().sample_dqm(dqm)

self.assertEqual(response.record.sample.shape, (0, 0))
self.assertIs(response.vartype, dimod.DISCRETE)

def test_sample_SPIN(self):
bqm = dimod.BinaryQuadraticModel({0: 0.0, 1: 0.0, 2: 0.0},
{(0, 1): -1.0, (1, 2): 1.0, (0, 2): 1.0},
Expand All @@ -74,7 +81,7 @@ def test_sample_BINARY(self):

response = dimod.ExactSolver().sample(bqm)

# every possible conbination should be present
# every possible combination should be present
self.assertEqual(len(response), 2**len(bqm))
self.assertEqual(response.record.sample.shape, (2**len(bqm), len(bqm)))

Expand All @@ -83,6 +90,23 @@ def test_sample_BINARY(self):

dimod.testing.assert_response_energies(response, bqm)

def test_sample_DISCRETE(self):
dqm = dimod.DiscreteQuadraticModel.from_numpy_vectors(
case_starts = [0, 3],
linear_biases = [0, 1, 2, 0, 1, 2, 3, 4, 5],
quadratic = ([5, 5, 5, 7, 7, 7], [0, 1, 2, 0, 1, 2], [0, 1, 2, 1, 2, 3])
)
response = dimod.ExactDQMSolver().sample_dqm(dqm)

# every possible combination should be present
self.assertEqual(len(response), 18)
self.assertEqual(response.record.sample.shape, (18, dqm.num_variables()))

#confirm vartype
self.assertIs(response.vartype, dimod.DISCRETE)

dimod.testing.assert_sampleset_energies_dqm(response, dqm)

def test_sample_ising(self):
h = {0: 0.0, 1: 0.0, 2: 0.0}
J = {(0, 1): -1.0, (1, 2): 1.0, (0, 2): 1.0}
Expand Down Expand Up @@ -143,11 +167,19 @@ def test_arbitrary_labels(self):
sampleset = dimod.ExactSolver().sample(bqm)
self.assertEqual(set(sampleset.variables), set(bqm.variables))

dqm = dimod.DQM.from_numpy_vectors([0], [-1], ([], [], []), labels={'ab'})
sampleset = dimod.ExactDQMSolver().sample_dqm(dqm)
self.assertEqual(set(sampleset.variables), set(dqm.variables))

def test_kwargs(self):
bqm = dimod.BinaryQuadraticModel({}, {}, 0.0, dimod.SPIN)
with self.assertWarns(SamplerUnknownArgWarning):
sampleset = dimod.ExactSolver().sample(bqm, a=1, b="abc")

dqm = dimod.DiscreteQuadraticModel()
with self.assertWarns(SamplerUnknownArgWarning):
sampleset = dimod.ExactDQMSolver().sample_dqm(dqm, a=1, b="abc")

class TestExactPolySolver(unittest.TestCase):
def test_instantiation(self):
sampler = dimod.ExactPolySolver()
Expand Down

0 comments on commit a4bca10

Please sign in to comment.