Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DWaveCliqueSampler #313

Merged
merged 9 commits into from
Jul 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/reference/samplers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=================

Expand Down
4 changes: 2 additions & 2 deletions dwave/system/samplers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
197 changes: 197 additions & 0 deletions dwave/system/samplers/clique.py
Original file line number Diff line number Diff line change
@@ -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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a reference to https://docs.ocean.dwavesys.com/en/stable/docs_system/reference/samplers.html#dwavesampler so new users see what a DWaveSampler is.

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")
Comment on lines +49 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fail on old solvers (with topology missing) we still have in production.

That might be acceptable, given this is a new feature, and the alternative still works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have anything other than C16s in production? A potential last-ditch workaround is to check if the edgelist is a subset of a C16...


# 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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link for posterity dwavesystems/dimod#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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use RST :math: here

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 <https://docs.dwavesys.com/docs/latest/doc_solver_ref.html>`_
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,
arcondello marked this conversation as resolved.
Show resolved Hide resolved
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))
Comment on lines +174 to +175
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where dwavesystems/dimod#402 would really pay-off.

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,
arcondello marked this conversation as resolved.
Show resolved Hide resolved
bias_range=self.qpu_linear_range,
quadratic_range=self.qpu_quadratic_range,
arcondello marked this conversation as resolved.
Show resolved Hide resolved
auto_scale=False,
chain_strength=chain_strength,
**kwargs
)

# change_vartype is non-blocking
return sampleset.change_vartype(original_bqm.vartype)
15 changes: 13 additions & 2 deletions dwave/system/samplers/dwave_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Comment on lines +108 to +113
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just realized dwavesystems/dwave-cloud-client#405 broke the design of order_by.

IMO, order_by should not be a parameter of DWaveSampler. I'll fix it in #317 after dwavesystems/dwave-cloud-client#407 is fixed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where should it be an argument?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of solver definition, e.g. solver=dict(hybrid=True, version__gt='1.0', order_by='-version').

config_file (str, optional):
Path to a configuration file that identifies a D-Wave system and provides
connection information.
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
55 changes: 55 additions & 0 deletions tests/qpu/test_dwavecliquesampler.py
Original file line number Diff line number Diff line change
@@ -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()
Loading