diff --git a/docs/reference/samplers.rst b/docs/reference/samplers.rst index 9360ecf8..11cab782 100644 --- a/docs/reference/samplers.rst +++ b/docs/reference/samplers.rst @@ -42,6 +42,34 @@ Methods DWaveSampler.sample_qubo DWaveSampler.validate_anneal_schedule +DWaveCliqueSampler +================== + +.. autoclass:: DWaveCliqueSampler + +Properties +---------- + +.. autosummary:: + :toctree: generated/ + + DWaveCliqueSampler.largest_clique_size + DWaveCliqueSampler.properties + DWaveCliqueSampler.parameters + + +Methods +------- + +.. autosummary:: + :toctree: generated/ + + DWaveCliqueSampler.largest_clique + DWaveCliqueSampler.sample + DWaveCliqueSampler.sample_ising + DWaveCliqueSampler.sample_qubo + + LeapHybridSampler ================= diff --git a/dwave/system/samplers/__init__.py b/dwave/system/samplers/__init__.py index d505e9ea..a2282358 100644 --- a/dwave/system/samplers/__init__.py +++ b/dwave/system/samplers/__init__.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# -# ================================================================================================ + +from dwave.system.samplers.clique import * from dwave.system.samplers.dwave_sampler import * from dwave.system.samplers.leap_hybrid_sampler import * diff --git a/dwave/system/samplers/clique.py b/dwave/system/samplers/clique.py new file mode 100644 index 00000000..b4f2768e --- /dev/null +++ b/dwave/system/samplers/clique.py @@ -0,0 +1,197 @@ +# Copyright 2020 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + +import dimod +import dwave_networkx as dnx + +from minorminer.busclique import find_clique_embedding, busgraph_cache + +from dwave.system.samplers.dwave_sampler import DWaveSampler + +__all__ = ['DWaveCliqueSampler'] + + +class DWaveCliqueSampler(dimod.Sampler): + """A sampler for solving clique problems on the D-Wave system. + + The `DWaveCliqueSampler` wraps + :func:`minorminer.busclique.find_clique_embedding` to generate embeddings + with even chain length. These embeddings will work well for dense + binary quadratic models. For sparse models, using + :class:`.EmbeddingComposite` with :class:`.DWaveSampler` is preferred. + + Args: + **config: + Keyword arguments, as accepted by :class:`.DWaveSampler` + + """ + def __init__(self, **config): + + # get the QPU with the most qubits available, subject to other + # constraints specified in **config + self.child = child = DWaveSampler(order_by='-num_active_qubits', + **config) + + # do some topology checking + try: + topology_type = child.properties['topology']['type'] + shape = child.properties['topology']['shape'] + except KeyError: + raise ValueError("given sampler has unknown topology format") + + # We need a networkx graph with certain properties. In the + # future it would be good for DWaveSampler to handle this. + # See https://github.com/dwavesystems/dimod/issues/647 + if topology_type == 'chimera': + G = dnx.chimera_graph(*shape, + node_list=child.nodelist, + edge_list=child.edgelist, + ) + elif topology_type == 'pegasus': + G = dnx.pegasus_graph(shape[0], + node_list=child.nodelist, + edge_list=child.edgelist, + ) + else: + raise ValueError("unknown topology type") + + self.target_graph = G + + # get the energy range + self.qpu_linear_range = child.properties['h_range'] + self.qpu_quadratic_range = child.properties.get( + 'extended_j_range', child.properties['j_range']) + + @property + def parameters(self): + try: + return self._parameters + except AttributeError: + pass + + self._parameters = parameters = self.child.parameters.copy() + + # this sampler handles scaling + parameters.pop('auto_scale', None) + parameters.pop('bias_range', None) + parameters.pop('quadratic_range', None) + + return parameters + + @property + def properties(self): + try: + return self._properties + except AttributeError: + pass + + self._properties = dict(qpu_properties=self.child.properties) + return self.properties + + @property + def largest_clique_size(self): + """The maximum number of variables.""" + return len(self.largest_clique()) + + def largest_clique(self): + """Return a largest-size clique embedding.""" + return busgraph_cache(self.target_graph).largest_clique() + + def sample(self, bqm, chain_strength=None, **kwargs): + """Sample from the specified binary quadratic model. + + Args: + bqm (:class:`~dimod.BinaryQuadraticModel`): + Any binary quadratic model with up to + :attr:`.largest_clique_size` variables. This BQM is embedded + using a dense clique embedding. + + chain_strength (float, optional): + The (relative) chain strength to use in the embedding. By + default a chain strength of `1.5sqrt(N)` where `N` is the size + of the largest clique, as returned by + :attr:`.largest_clique_size`. + + **kwargs: + Optional keyword arguments for the sampling method, specified + per solver in :attr:`.DWaveCliqueSampler.parameters`. + D-Wave System Documentation's + `solver guide `_ + describes the parameters and properties supported on the D-Wave + system. Note that `auto_scale` is not supported by this + sampler, because it scales the problem as part of the embedding + process. + + """ + + # some arguments should not be overwritten + if 'auto_scale' in kwargs: + raise TypeError("sample() got an unexpected keyword argument " + "'auto_scale'") + if 'bias_range' in kwargs: + raise TypeError("sample() got an unexpected keyword argument " + "'bias_range'") + if 'quadratic_range' in kwargs: + raise TypeError("sample() got an unexpected keyword argument " + "'quadratic_range'") + + # handle circular import. todo: fix + from dwave.system.composites.embedding import FixedEmbeddingComposite + + # get the embedding + embedding = find_clique_embedding(bqm.variables, self.target_graph, + use_cache=True) + + # returns an empty embedding when the BQM is too large + if not embedding and bqm.num_variables: + raise ValueError("Cannot embed given BQM (size {}), sampler can " + "only handle problems of size {}".format( + len(bqm.variables), self.largest_clique_size)) + + assert bqm.num_variables == len(embedding) # sanity check + + if chain_strength is None: + # chain length determines chain strength + if embedding: + chain_strength = 1.5 * math.sqrt(len(embedding)) + else: + chain_strength = 1 # doesn't matter + + # scale chain strength by the problem scale + scalar = max(max(map(abs, bqm.linear.values()), default=0), + max(map(abs, bqm.quadratic.values()), default=0)) + chain_strength *= scalar + + # # scaling only make sense in Ising space + original_bqm = bqm + + if bqm.vartype is not dimod.SPIN: + bqm = bqm.change_vartype(dimod.SPIN, inplace=False) + + sampler = FixedEmbeddingComposite( + dimod.ScaleComposite(self.child), + embedding) + + sampleset = sampler.sample(bqm, + bias_range=self.qpu_linear_range, + quadratic_range=self.qpu_quadratic_range, + auto_scale=False, + chain_strength=chain_strength, + **kwargs + ) + + # change_vartype is non-blocking + return sampleset.change_vartype(original_bqm.vartype) diff --git a/dwave/system/samplers/dwave_sampler.py b/dwave/system/samplers/dwave_sampler.py index a50915d6..5b921994 100644 --- a/dwave/system/samplers/dwave_sampler.py +++ b/dwave/system/samplers/dwave_sampler.py @@ -105,6 +105,12 @@ class DWaveSampler(dimod.Sampler, dimod.Structured): then it will instead propogate the `SolverNotFoundError` to the user. + order_by (callable/str/None): + Solver sorting key function or (or :class:`~dwave.cloud.Solver` + attribute/item dot-separated path). + See :class:`~dwave.cloud.Client.get_solvers` for a more detailed + description of the parameter. + config_file (str, optional): Path to a configuration file that identifies a D-Wave system and provides connection information. @@ -151,7 +157,7 @@ class DWaveSampler(dimod.Sampler, dimod.Structured): for explanations of technical terms in descriptions of Ocean tools. """ - def __init__(self, failover=False, retry_interval=-1, **config): + def __init__(self, failover=False, retry_interval=-1, order_by=None, **config): if config.get('solver_features') is not None: warn("'solver_features' argument has been renamed to 'solver'.", DeprecationWarning) @@ -162,7 +168,12 @@ def __init__(self, failover=False, retry_interval=-1, **config): config['solver'] = config.pop('solver_features') self.client = Client.from_config(**config) - self.solver = self.client.get_solver() + + if order_by is None: + # use the default from the cloud-client + self.solver = self.client.get_solver() + else: + self.solver = self.client.get_solver(order_by=order_by) self.failover = failover self.retry_interval = retry_interval diff --git a/requirements.txt b/requirements.txt index 0b6b90ff..7eeee5b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ --extra-index-url https://pypi.dwavesys.com/simple -dimod==0.9.1 -dwave-cloud-client==0.7.2 +dimod==0.9.5 +dwave-cloud-client==0.7.5 dwave-networkx==0.8.4 dwave-drivers==0.4.4 dwave-tabu==0.2.2 homebase==1.0.1 -minorminer==0.1.9 +minorminer==0.2.0 six==1.11.0 mock==2.0.0 numpy==1.18.0 diff --git a/setup.py b/setup.py index 25661d0a..9aec4157 100644 --- a/setup.py +++ b/setup.py @@ -25,12 +25,12 @@ exec(open(os.path.join(".", "dwave", "system", "package_info.py")).read()) -install_requires = ['dimod>=0.9.0,<0.10.0', - 'dwave-cloud-client>=0.7.2,<0.8.0', +install_requires = ['dimod>=0.9.5,<0.10.0', + 'dwave-cloud-client>=0.7.5,<0.8.0', 'dwave-networkx>=0.8.4', 'networkx>=2.0,<3.0', 'homebase>=1.0.0,<2.0.0', - 'minorminer>=0.1.3,<0.2.0', + 'minorminer>=0.2.0,<0.3.0', 'six>=1.11.0,<2.0.0', 'numpy>=1.14.0,<2.0.0', 'dwave-tabu>=0.2.0', diff --git a/tests/qpu/test_dwavecliquesampler.py b/tests/qpu/test_dwavecliquesampler.py new file mode 100644 index 00000000..ac8dcb06 --- /dev/null +++ b/tests/qpu/test_dwavecliquesampler.py @@ -0,0 +1,55 @@ +# Copyright 2020 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +import unittest + +import dimod + +from dwave.cloud.exceptions import ConfigFileError, SolverNotFoundError +from dwave.system import DWaveCliqueSampler + + +class TestDWaveCliqueSampler(unittest.TestCase): + def test_chimera(self): + try: + sampler = DWaveCliqueSampler( + solver=dict(topology__type='chimera', qpu=True)) + except (ValueError, ConfigFileError, SolverNotFoundError): + raise unittest.SkipTest("no Chimera-structured QPU available") + + dimod.testing.assert_sampler_api(sampler) + + # submit a maximum ferromagnet + bqm = dimod.AdjVectorBQM('SPIN') + for u, v in itertools.combinations(sampler.largest_clique(), 2): + bqm.quadratic[u, v] = -1 + + sampler.sample(bqm).resolve() + + def test_pegasus(self): + try: + sampler = DWaveCliqueSampler( + solver=dict(topology__type='pegasus', qpu=True)) + except (ValueError, ConfigFileError, SolverNotFoundError): + raise unittest.SkipTest("no Pegasus-structured QPU available") + + dimod.testing.assert_sampler_api(sampler) + + # submit a maximum ferromagnet + bqm = dimod.AdjVectorBQM('SPIN') + for u, v in itertools.combinations(sampler.largest_clique(), 2): + bqm.quadratic[u, v] = -1 + + sampler.sample(bqm).resolve() diff --git a/tests/test_dwavecliquesampler.py b/tests/test_dwavecliquesampler.py new file mode 100644 index 00000000..93b0b6d0 --- /dev/null +++ b/tests/test_dwavecliquesampler.py @@ -0,0 +1,122 @@ +# Copyright 2020 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +import unittest +import unittest.mock + +import dimod +import dwave_networkx as dnx + +from dwave.system import DWaveCliqueSampler + + +class MockDWaveSampler(dimod.RandomSampler, dimod.Structured): + # contains the minimum needed to work with DWaveCliqueSampler + + edgelist = None + nodelist = None + + def __init__(self, **kwargs): + self.properties = dict(h_range=[-2, 2], + j_range=[-1, 1], + extended_j_range=[-2, 1],) + self.parameters = {} + + def sample(self, bqm, auto_scale=True): + assert not auto_scale + assert bqm.vartype is dimod.SPIN + + h_range = self.properties['h_range'] + j_range = self.properties['extended_j_range'] + + for bias in bqm.linear.values(): + assert h_range[0] <= bias <= h_range[1] + + for bias in bqm.quadratic.values(): + assert j_range[0] <= bias <= j_range[1] + + return super().sample(bqm) + + +class MockChimeraDWaveSampler(MockDWaveSampler): + def __init__(self, order_by=None): + super().__init__() + + self.properties.update(topology=dict(shape=[4, 4, 4], type='chimera')) + + G = dnx.chimera_graph(4, 4, 4) + + self.nodelist = list(G.nodes) + self.edgelist = list(G.edges) + + def sample(self, bqm, **kwargs): + + # per_qubit_coupling_range + ran = (-9, 6) + + # check the total coupling range + for v in bqm.variables: + bias = sum(bqm.adj[v].values()) + assert ran[0] <= bias <= ran[1] + + return super().sample(bqm, **kwargs) + + +class MockPegasusDWaveSampler(MockDWaveSampler): + def __init__(self, order_by=None): + super().__init__() + + self.properties.update(topology=dict(shape=[6], type='pegasus')) + + G = dnx.pegasus_graph(6) + + self.nodelist = list(G.nodes) + self.edgelist = list(G.edges) + + +with unittest.mock.patch('dwave.system.samplers.clique.DWaveSampler', + MockChimeraDWaveSampler): + chimera_sampler = DWaveCliqueSampler() + +with unittest.mock.patch('dwave.system.samplers.clique.DWaveSampler', + MockPegasusDWaveSampler): + pegasus_sampler = DWaveCliqueSampler() + + +@dimod.testing.load_sampler_bqm_tests(chimera_sampler) +@dimod.testing.load_sampler_bqm_tests(pegasus_sampler) +class TestDWaveCliqueSampler(unittest.TestCase): + def test_api(self): + dimod.testing.assert_sampler_api(chimera_sampler) + dimod.testing.assert_sampler_api(pegasus_sampler) + + def test_largest_clique(self): + self.assertEqual(len(chimera_sampler.largest_clique()), 16) + + def test_ferromagnet_chimera(self): + # submit a maximum ferromagnet + bqm = dimod.AdjVectorBQM('SPIN') + for u, v in itertools.combinations(chimera_sampler.largest_clique(), 2): + bqm.quadratic[u, v] = -1 + + chimera_sampler.sample(bqm).resolve() + + def test_too_large(self): + num_variables = chimera_sampler.largest_clique_size + 1 + + bqm = dimod.BinaryQuadraticModel(num_variables, 'SPIN') + + with self.assertRaises(ValueError): + chimera_sampler.sample(bqm)