From efcc491fd6245194d8a89b79ae825059d20a14a2 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Tue, 16 Jun 2020 05:45:40 -0700 Subject: [PATCH 01/17] Use ABCs from 'collections.abc' --- dwave/cloud/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dwave/cloud/client.py b/dwave/cloud/client.py index ddd92a27..216b5b2b 100644 --- a/dwave/cloud/client.py +++ b/dwave/cloud/client.py @@ -48,7 +48,6 @@ import requests import warnings import operator -import collections import queue import base64 @@ -58,6 +57,7 @@ from itertools import chain from functools import partial, wraps +from collections import abc, namedtuple, OrderedDict from concurrent.futures import ThreadPoolExecutor from dateutil.parser import parse as parse_datetime @@ -401,7 +401,7 @@ def __init__(self, endpoint=None, token=None, solver=None, proxy=None, # parse solver if not solver: solver_def = {} - elif isinstance(solver, collections.Mapping): + elif isinstance(solver, abc.Mapping): solver_def = solver elif isinstance(solver, str): # support features dict encoded as JSON in our config INI file @@ -422,7 +422,7 @@ def __init__(self, endpoint=None, token=None, solver=None, proxy=None, # parse headers if not headers: headers_dict = {} - elif isinstance(headers, collections.Mapping): + elif isinstance(headers, abc.Mapping): headers_dict = headers elif isinstance(headers, str): try: @@ -1108,7 +1108,7 @@ def _submit(self, body, future): This method is thread safe. """ self._submission_queue.put(self._submit.Message(body, future)) - _submit.Message = collections.namedtuple('Message', ['body', 'future']) + _submit.Message = namedtuple('Message', ['body', 'future']) def _do_submit_problems(self): """Pull problems from the submission queue and submit them. From 132c2741dd35ae982f34723c91846bc5d4e1a5c5 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 3 Jul 2020 11:19:40 -0700 Subject: [PATCH 02/17] Remove legacy config file (.dwrc) support --- dwave/cloud/client.py | 21 +--- dwave/cloud/config.py | 180 +++--------------------------- tests/test_client.py | 15 --- tests/test_mock_solver_loading.py | 104 +---------------- 4 files changed, 19 insertions(+), 301 deletions(-) diff --git a/dwave/cloud/client.py b/dwave/cloud/client.py index 216b5b2b..f569ab18 100644 --- a/dwave/cloud/client.py +++ b/dwave/cloud/client.py @@ -66,7 +66,7 @@ from dwave.cloud.package_info import __packagename__, __version__ from dwave.cloud.exceptions import * from dwave.cloud.computation import Future -from dwave.cloud.config import load_config, legacy_load_config, parse_float +from dwave.cloud.config import load_config, parse_float from dwave.cloud.solver import Solver, available_solvers from dwave.cloud.concurrency import PriorityThreadPoolExecutor from dwave.cloud.upload import ChunkedData @@ -186,7 +186,7 @@ class Client(object): @classmethod def from_config(cls, config_file=None, profile=None, client=None, endpoint=None, token=None, solver=None, proxy=None, - headers=None, legacy_config_fallback=False, **kwargs): + headers=None, **kwargs): """Client factory method to instantiate a client instance from configuration. Configuration values can be specified in multiple ways, ranked in the following @@ -277,10 +277,6 @@ def from_config(cls, config_file=None, profile=None, client=None, Newline-separated additional HTTP headers to include with each API request, or a dictionary of (key, value) pairs. - legacy_config_fallback (bool, default=False): - If True and loading from a standard D-Wave Cloud Client configuration - file (``dwave.conf``) fails, tries loading a legacy configuration file (``~/.dwrc``). - Other Parameters: Unrecognized keys (str): All unrecognized keys are passed through to the appropriate client class constructor @@ -321,19 +317,6 @@ def from_config(cls, config_file=None, profile=None, client=None, headers=headers) logger.debug("Config loaded: %r", config) - # fallback to legacy `.dwrc` if key variables missing - if legacy_config_fallback: - warnings.warn( - "'legacy_config_fallback' is deprecated, and it will be removed " - "in 0.7.0. please convert your legacy .dwrc file to the new " - "config format.", DeprecationWarning) - - if not config.get('token'): - config = legacy_load_config( - profile=profile, client=client, endpoint=endpoint, - token=token, solver=solver, proxy=proxy, headers=headers) - logger.debug("Legacy config loaded: %r", config) - # manual override of other (client-custom) arguments config.update(kwargs) diff --git a/dwave/cloud/config.py b/dwave/cloud/config.py index 87971a4d..3f7654c0 100644 --- a/dwave/cloud/config.py +++ b/dwave/cloud/config.py @@ -190,10 +190,10 @@ variable. Next, the second profile is selected with the explicitly named solver overriding the environment variable setting. - >>> import dwave.cloud as dc + >>> import dwave.cloud >>> import os >>> os.environ['DWAVE_API_SOLVER'] = 'EXAMPLE_2000Q_SYSTEM' # doctest: +SKIP - >>> dc.config.load_config("./dwave_c.conf") # doctest: +SKIP + >>> dwave.cloud.config.load_config("./dwave_c.conf") # doctest: +SKIP {'client': 'sw', 'endpoint': 'https://url.of.some.dwavesystem.com/sapi', 'proxy': None, @@ -220,8 +220,7 @@ from dwave.cloud.exceptions import ConfigFileReadError, ConfigFileParseError __all__ = ['get_configfile_paths', 'get_configfile_path', 'get_default_configfile_path', - 'load_config_from_files', 'load_profile_from_files', - 'load_config', 'legacy_load_config'] + 'load_config_from_files', 'load_profile_from_files', 'load_config'] CONF_APP = "dwave" CONF_AUTHOR = "dwavesystem" @@ -270,14 +269,14 @@ def get_configfile_paths(system=True, user=True, local=True, only_existing=True) This example displays all paths to configuration files on a Windows system running Python 2.7 and then finds the single existing configuration file. - >>> import dwave.cloud as dc + >>> from dwave.cloud.config import get_configfile_paths >>> # Display paths - >>> dc.config.get_configfile_paths(only_existing=False) # doctest: +SKIP + >>> get_configfile_paths(only_existing=False) # doctest: +SKIP ['C:\\ProgramData\\dwavesystem\\dwave\\dwave.conf', 'C:\\Users\\jane\\AppData\\Local\\dwavesystem\\dwave\\dwave.conf', '.\\dwave.conf'] >>> # Find existing files - >>> dc.config.get_configfile_paths() # doctest: +SKIP + >>> get_configfile_paths() # doctest: +SKIP ['C:\\Users\\jane\\AppData\\Local\\dwavesystem\\dwave\\dwave.conf'] """ @@ -318,17 +317,17 @@ def get_configfile_path(): Configuration file path. Examples: - This example displays the highest-priority configuration file on a Windows system - running Python 2.7. + This example displays the highest-priority configuration file on a + Windows system. - >>> import dwave.cloud as dc + >>> from dwave.cloud import config >>> # Display paths - >>> dc.config.get_configfile_paths(only_existing=False) # doctest: +SKIP + >>> config.get_configfile_paths(only_existing=False) # doctest: +SKIP ['C:\\ProgramData\\dwavesystem\\dwave\\dwave.conf', 'C:\\Users\\jane\\AppData\\Local\\dwavesystem\\dwave\\dwave.conf', '.\\dwave.conf'] >>> # Find highest-priority local configuration file - >>> dc.config.get_configfile_path() # doctest: +SKIP + >>> config.get_configfile_path() # doctest: +SKIP 'C:\\Users\\jane\\AppData\\Local\\dwavesystem\\dwave\\dwave.conf' """ @@ -347,19 +346,19 @@ def get_default_configfile_path(): Configuration file path. Examples: - This example displays the default configuration file on an Ubuntu Unix system - running IPython 2.7. + This example displays the default configuration file on an Ubuntu Linux + system. - >>> import dwave.cloud as dc + >>> from dwave.cloud import config >>> # Display paths - >>> dc.config.get_configfile_paths(only_existing=False) # doctest: +SKIP + >>> config.get_configfile_paths(only_existing=False) # doctest: +SKIP ['/etc/xdg/xdg-ubuntu/dwave/dwave.conf', '/usr/share/upstart/xdg/dwave/dwave.conf', '/etc/xdg/dwave/dwave.conf', '/home/mary/.config/dwave/dwave.conf', './dwave.conf'] >>> # Find default configuration path - >>> dc.config.get_default_configfile_path() # doctest: +SKIP + >>> config.get_default_configfile_path() # doctest: +SKIP '/home/mary/.config/dwave/dwave.conf' """ @@ -788,150 +787,3 @@ def load_config(config_file=None, profile=None, client=None, section['headers'] = headers or os.getenv("DWAVE_API_HEADERS", section.get('headers')) return section - - -def legacy_load_config(profile=None, endpoint=None, token=None, solver=None, - proxy=None, **kwargs): - """Load configured URLs and token for the SAPI server. - - .. warning:: Included only for backward compatibility. Please use - :func:`load_config` or the client factory - :meth:`~dwave.cloud.client.Client.from_config` instead. - - This method tries to load a legacy configuration file from ``~/.dwrc``, select a - specified `profile` (or, if not specified, the first profile), and override - individual keys with values read from environment variables or - specified explicitly as key values in the function. - - Configuration values can be specified in multiple ways, ranked in the following - order (with 1 the highest ranked): - - 1. Values specified as keyword arguments in :func:`legacy_load_config()` - 2. Values specified as environment variables - 3. Values specified in the legacy ``~/.dwrc`` configuration file - - Environment variables searched for are: - - - ``DW_INTERNAL__HTTPLINK`` - - ``DW_INTERNAL__TOKEN`` - - ``DW_INTERNAL__HTTPPROXY`` - - ``DW_INTERNAL__SOLVER`` - - Legacy configuration file format is a modified CSV where the first comma is - replaced with a bar character (``|``). Each line encodes a single profile. Its - columns are:: - - profile_name|endpoint_url,authentication_token,proxy_url,default_solver_name - - All its fields after ``authentication_token`` are optional. - - When there are multiple connections in a file, the first one is - the default. Any commas in the URLs must be percent-encoded. - - Args: - profile (str): - Profile name in the legacy configuration file. - - endpoint (str, default=None): - API endpoint URL. - - token (str, default=None): - API authorization token. - - solver (str, default=None): - Default solver to use in :meth:`~dwave.cloud.client.Client.get_solver`. - If undefined, all calls to :meth:`~dwave.cloud.client.Client.get_solver` - must explicitly specify the solver name/id. - - proxy (str, default=None): - URL for proxy to use in connections to D-Wave API. Can include - username/password, port, scheme, etc. If undefined, client uses a - system-level proxy, if defined, or connects directly to the API. - - Returns: - Dictionary with keys: endpoint, token, solver, and proxy. - - Examples: - This example creates a client using the :meth:`~dwave.cloud.client.Client.from_config` - method, which falls back on the legacy file by default when it fails to - find a D-Wave Cloud Client configuration file (setting its `legacy_config_fallback` - parameter to False precludes this fall-back operation). For this example, - no D-Wave Cloud Client configuration file is present on the local system; - instead the following ``.dwrc`` legacy configuration file is present in the - user's home directory:: - - profile-a|https://one.com,token-one - profile-b|https://two.com,token-two - - The following example code creates a client without explicitly specifying - key values, therefore auto-detection searches for existing (non-legacy) configuration - files in the standard directories of :func:`get_configfile_paths` and, failing to - find one, falls back on the existing legacy configuration file above. - - >>> import dwave.cloud as dc - >>> client = dwave.cloud.Client.from_config() # doctest: +SKIP - >>> client.endpoint # doctest: +SKIP - 'https://one.com' - >>> client.token # doctest: +SKIP - 'token-one' - - The following examples specify a profile and/or token. - - >>> # Explicitly specify a profile - >>> client = dwave.cloud.Client.from_config(profile='profile-b') # doctest: +SKIP - ... # Will try to connect with the url `https://two.com` and the token `token-two`. - >>> client = dwave.cloud.Client.from_config(profile='profile-b', token='new-token') # doctest: +SKIP - ... # Will try to connect with the url `https://two.com` and the token `new-token`. - - """ - - def _parse_config(fp, filename): - fields = ('endpoint', 'token', 'proxy', 'solver') - config = OrderedDict() - for line in fp: - # strip whitespace, skip blank and comment lines - line = line.strip() - if not line or line.startswith('#'): - continue - # parse each record, store in dict with label as key - try: - label, data = line.split('|', 1) - values = [v.strip() or None for v in data.split(',')] - config[label] = dict(zip(fields, values)) - except: - raise ConfigFileParseError( - "Failed to parse {!r}, line {!r}".format(filename, line)) - return config - - def _read_config(filename): - try: - with open(filename, 'r') as f: - return _parse_config(f, filename) - except (IOError, OSError): - raise ConfigFileReadError("Failed to read {!r}".format(filename)) - - config = {} - filename = os.path.expanduser('~/.dwrc') - if os.path.exists(filename): - config = _read_config(filename) - - # load profile if specified, or first one in file - if profile: - try: - section = config[profile] - except KeyError: - raise ValueError("Config profile {!r} not found".format(profile)) - else: - try: - _, section = next(iter(config.items())) - except StopIteration: - section = {} - - # override config variables (if any) with environment and then with arguments - section['endpoint'] = endpoint or os.getenv("DW_INTERNAL__HTTPLINK", section.get('endpoint')) - section['token'] = token or os.getenv("DW_INTERNAL__TOKEN", section.get('token')) - section['proxy'] = proxy or os.getenv("DW_INTERNAL__HTTPPROXY", section.get('proxy')) - section['solver'] = solver or os.getenv("DW_INTERNAL__SOLVER", section.get('solver')) - section.update(kwargs) - - return section diff --git a/tests/test_client.py b/tests/test_client.py index 25ed9c00..a5d3821b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -233,21 +233,6 @@ def test_custom_kwargs_overrides_config(self): init.assert_called_once_with( endpoint='endpoint', token='token', custom='new-custom') - def test_legacy_config_load_fallback(self): - conf = {k: k for k in 'endpoint token proxy solver'.split()} - with mock.patch("dwave.cloud.client.load_config", return_value={}): - with mock.patch("dwave.cloud.client.legacy_load_config", lambda **kwargs: conf): - # test fallback works (legacy config is loaded) - with dwave.cloud.Client.from_config(legacy_config_fallback=True) as client: - self.assertEqual(client.endpoint, 'endpoint') - self.assertEqual(client.token, 'token') - self.assertEqual(client.default_solver, {"name__eq": "solver"}) - self.assertEqual(client.session.proxies['http'], 'proxy') - - # test fallback is avoided (legacy config skipped) - self.assertRaises( - ValueError, dwave.cloud.Client.from_config, legacy_config_fallback=False) - def test_solver_features_from_config(self): solver_def = {"qpu": True} conf = {k: k for k in 'endpoint token'.split()} diff --git a/tests/test_mock_solver_loading.py b/tests/test_mock_solver_loading.py index 3fdc8eca..e6fa54e3 100644 --- a/tests/test_mock_solver_loading.py +++ b/tests/test_mock_solver_loading.py @@ -29,7 +29,7 @@ from dwave.cloud.exceptions import ( SolverPropertyMissingError, ConfigFileReadError, ConfigFileParseError, SolverError, SolverNotFoundError) -from dwave.cloud.config import legacy_load_config, load_config +from dwave.cloud.config import load_config from dwave.cloud.testing import iterable_mock_open @@ -319,11 +319,6 @@ def request(session, method, url, *args, **kwargs): raise RequestEvent(method, url, *args, **kwargs) -legacy_config_body = """ -prod|file-prod-url,file-prod-token -alpha|file-alpha-url,file-alpha-token,,alpha-solver -""" - config_body = """ [prod] endpoint = http://file-prod.url @@ -343,103 +338,6 @@ def request(session, method, url, *args, **kwargs): """ -# patch the new config loading mechanism, to test only legacy config loading -@mock.patch("dwave.cloud.config.get_configfile_paths", lambda: []) -# patch Session.request to raise RequestEvent with the URL requested -@mock.patch.object(requests.Session, 'request', RequestEvent.request) -class MockLegacyConfiguration(unittest.TestCase): - """Ensure that the precedence of configuration sources is followed.""" - - endpoint = "http://custom-endpoint.url" - - def setUp(self): - # clear `config_load`-relevant environment variables before testing, so - # we only need to patch the ones that we are currently testing - for key in frozenset(os.environ.keys()): - if key.startswith("DWAVE_") or key.startswith("DW_INTERNAL__"): - os.environ.pop(key, None) - - def test_explicit_only(self): - """Specify information only through function arguments.""" - with Client.from_config(endpoint=self.endpoint, token='arg-token') as client: - try: - client.get_solver('arg-solver') - except RequestEvent as event: - self.assertTrue(event.url.startswith(self.endpoint)) - return - self.fail() - - def test_nonexisting_file(self): - """With no values set, we should get an error when trying to create Client.""" - with self.assertRaises(ConfigFileReadError): - with Client.from_config(config_file='nonexisting', legacy_config_fallback=False) as client: - pass - - def test_explicit_with_file(self): - """With arguments and a config file, the config file should be ignored.""" - with mock.patch("dwave.cloud.config.open", iterable_mock_open(config_body), create=True): - with Client.from_config(endpoint=self.endpoint, token='arg-token') as client: - try: - client.get_solver('arg-solver') - except RequestEvent as event: - self.assertTrue(event.url.startswith(self.endpoint)) - return - self.fail() - - def test_only_file(self): - """With no arguments or environment variables, the default connection from the config file should be used.""" - with mock.patch("dwave.cloud.config.open", iterable_mock_open(config_body), create=True): - with Client.from_config('config_file') as client: - try: - client.get_solver('arg-solver') - except RequestEvent as event: - self.assertTrue(event.url.startswith('http://file-prod.url/')) - return - self.fail() - - def test_only_file_key(self): - """If give a name from the config file the proper URL should be loaded.""" - with mock.patch("dwave.cloud.config.open", iterable_mock_open(config_body), create=True): - with mock.patch("dwave.cloud.config.get_configfile_paths", lambda *x: ['file']): - with Client.from_config(profile='alpha') as client: - try: - client.get_solver('arg-solver') - except RequestEvent as event: - self.assertTrue(event.url.startswith('http://file-alpha.url/')) - return - self.fail() - - def test_env_with_file_set(self): - """With environment variables and a config file, the config file should be ignored.""" - with mock.patch("dwave.cloud.config.open", iterable_mock_open(legacy_config_body), create=True): - with mock.patch.dict(os.environ, {'DW_INTERNAL__HTTPLINK': 'http://env.url', 'DW_INTERNAL__TOKEN': 'env-token'}): - with Client.from_config(config_file=False, legacy_config_fallback=True) as client: - try: - client.get_solver('arg-solver') - except RequestEvent as event: - self.assertTrue(event.url.startswith('http://env.url/')) - return - self.fail() - - def test_env_args_set(self): - """With arguments and environment variables, the environment variables should be ignored.""" - with mock.patch.dict(os.environ, {'DW_INTERNAL__HTTPLINK': 'http://env.url', 'DW_INTERNAL__TOKEN': 'env-token'}): - with Client.from_config(endpoint=self.endpoint, token='args-token') as client: - try: - client.get_solver('arg-solver') - except RequestEvent as event: - self.assertTrue(event.url.startswith(self.endpoint)) - return - self.fail() - - def test_file_read_error(self): - """On config file read error, we should fail with `ConfigFileReadError`, - but only if .dwrc actually exists on disk.""" - with mock.patch("dwave.cloud.config.open", side_effect=OSError, create=True): - with mock.patch("os.path.exists", lambda fn: True): - self.assertRaises(ConfigFileReadError, legacy_load_config) - - class MockConfiguration(unittest.TestCase): def test_custom_options(self): From 420dc79c20fe88f9c80fd563b5d25c7ca195e4c5 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 3 Jul 2020 11:56:01 -0700 Subject: [PATCH 03/17] Schedule the deprecated `Client.solvers` for removal in 0.9.0 --- dwave/cloud/client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dwave/cloud/client.py b/dwave/cloud/client.py index f569ab18..9896240e 100644 --- a/dwave/cloud/client.py +++ b/dwave/cloud/client.py @@ -1014,8 +1014,14 @@ def predicate(solver, query, val): return solvers def solvers(self, refresh=False, **filters): - """Deprecated in favor of :meth:`.get_solvers`.""" - warnings.warn("'solvers' is deprecated in favor of 'get_solvers'.", DeprecationWarning) + """Deprecated in favor of :meth:`.get_solvers`. + + Scheduled for removal in 0.9.0. + """ + warnings.warn( + "'solvers' is deprecated, and it will be removed " + "in 0.9.0. please convert your code to use 'get_solvers'", + DeprecationWarning) return self.get_solvers(refresh=refresh, **filters) def get_solver(self, name=None, refresh=False, order_by='avg_load', **filters): From 4969584f61f3bf778164fd486b417bc24de089ad Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 3 Jul 2020 12:18:51 -0700 Subject: [PATCH 04/17] Remove deprecated `Solver.is_*` properties --- dwave/cloud/solver.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/dwave/cloud/solver.py b/dwave/cloud/solver.py index 762658a4..9d6a4f1d 100644 --- a/dwave/cloud/solver.py +++ b/dwave/cloud/solver.py @@ -234,24 +234,6 @@ def hybrid(self): # TODO: remove when all production solvers are updated return self.id.startswith('hybrid') - @property - def is_qpu(self): - warnings.warn("'is_qpu' property is deprecated in favor of 'qpu'." - "It will be removed in 0.8.0.", DeprecationWarning) - return self.qpu - - @property - def is_software(self): - warnings.warn("'is_software' property is deprecated in favor of 'software'." - "It will be removed in 0.8.0.", DeprecationWarning) - return self.software - - @property - def is_online(self): - warnings.warn("'is_online' property is deprecated in favor of 'online'." - "It will be removed in 0.8.0.", DeprecationWarning) - return self.online - class UnstructuredSolver(BaseSolver): """Class for D-Wave unstructured solvers. From d989b6660e218b2cf6a6083442d27fd8f4b357b8 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Mon, 6 Jul 2020 12:06:37 -0700 Subject: [PATCH 05/17] Avoid casting to fileview-able BQM on upload, if possible --- dwave/cloud/solver.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/dwave/cloud/solver.py b/dwave/cloud/solver.py index 9d6a4f1d..bff54647 100644 --- a/dwave/cloud/solver.py +++ b/dwave/cloud/solver.py @@ -378,19 +378,21 @@ def _bqm_as_fileview(bqm): # XXX: temporary until something like dwavesystems/dimod#599 is implemented. try: + import dimod from dimod.serialization.fileview import FileView as BQMFileView - from dimod import AdjVectorBQM except ImportError: # pragma: no cover return if isinstance(bqm, BQMFileView): return bqm - try: - if not isinstance(bqm, AdjVectorBQM): + # test explicitly to avoid copy on cast if possible + fileviewable = (dimod.AdjArrayBQM, dimod.AdjVectorBQM, dimod.AdjMapBQM) + if not isinstance(bqm, fileviewable): + try: bqm = AdjVectorBQM(bqm) - except: - return + except: + return try: return BQMFileView(bqm) From c79a8ee3464f171eb0d5b283ac4c457853bf8e31 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Thu, 23 Jul 2020 07:27:34 -0700 Subject: [PATCH 06/17] `bqm-ref` format is now called `ref` --- dwave/cloud/coders.py | 9 ++------- dwave/cloud/solver.py | 8 ++++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/dwave/cloud/coders.py b/dwave/cloud/coders.py index 62475fdf..cb2cce8e 100644 --- a/dwave/cloud/coders.py +++ b/dwave/cloud/coders.py @@ -262,11 +262,6 @@ def decode_qp_numpy(msg, return_matrix=True): return result -def _encode_problem_as_bq_ref(problem): - assert isinstance(problem, str) - - return problem - def _encode_problem_as_bq_json(problem): assert hasattr(problem, 'to_serializable') @@ -293,8 +288,8 @@ def encode_problem_as_bq(problem, compress=False): if isinstance(problem, str): return { - 'format': 'bqm-ref', - 'data': _encode_problem_as_bq_ref(problem) + 'format': 'ref', + 'data': problem } if compress: diff --git a/dwave/cloud/solver.py b/dwave/cloud/solver.py index bff54647..a7eb9617 100644 --- a/dwave/cloud/solver.py +++ b/dwave/cloud/solver.py @@ -307,8 +307,8 @@ def sample_qubo(self, qubo, **params): bqm = dimod.BinaryQuadraticModel.from_qubo(qubo) return self.sample_bqm(bqm, **params) - def _encode_any_problem_as_bqm_ref(self, problem, params): - """Encode `problem` for submitting in `bqm-ref` format. Upload the + def _encode_any_problem_as_ref(self, problem, params): + """Encode `problem` for submitting in `ref` format. Upload the problem if it's not already uploaded. Args: @@ -327,7 +327,7 @@ def _encode_any_problem_as_bqm_ref(self, problem, params): if isinstance(problem, str): problem_id = problem else: - logger.debug("To encode the problem for submit via 'bqm-ref', " + logger.debug("To encode the problem for submit in the 'ref' format, " "we need to upload it first.") problem_id = self.upload_bqm(problem).result() @@ -361,7 +361,7 @@ def sample_bqm(self, bqm, **params): # encode the request (body as future) body = self.client._encode_problem_executor.submit( - self._encode_any_problem_as_bqm_ref, + self._encode_any_problem_as_ref, problem=bqm, params=params) # computation future holds a reference to the remote job From abdfd5a455ab904d7ae63c650e64bc03aa34742c Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Thu, 23 Jul 2020 09:03:11 -0700 Subject: [PATCH 07/17] Drop `bq-zlib` embedded problem format support; promote dimodbqm --- dwave/cloud/cli.py | 18 ++++++------ dwave/cloud/coders.py | 64 +++++++++++++++++++++++++++++-------------- dwave/cloud/solver.py | 48 +++++++------------------------- tests/test_cli.py | 2 +- 4 files changed, 63 insertions(+), 69 deletions(-) diff --git a/dwave/cloud/cli.py b/dwave/cloud/cli.py index f0a44f1c..bb833df7 100644 --- a/dwave/cloud/cli.py +++ b/dwave/cloud/cli.py @@ -31,7 +31,7 @@ default_text_input, click_info_switch, generate_random_ising_problem, datetime_to_timestamp, utcnow, strtrunc, CLIError, set_loglevel, get_contrib_packages, user_agent) -from dwave.cloud.coders import encode_problem_as_bq +from dwave.cloud.coders import bqm_as_file from dwave.cloud.package_info import __title__, __version__ from dwave.cloud.exceptions import ( SolverAuthenticationError, InvalidAPIResponseError, UnsupportedSolverError, @@ -473,8 +473,8 @@ def echo(s, maxlen=100): help='Connection profile (section) name') @click.option('--problem-id', '-i', default=None, help='Problem ID (optional)') -@click.option('--format', '-f', default='bq-zlib', - type=click.Choice(['coo', 'bq-zlib'], case_sensitive=False), +@click.option('--format', '-f', default='dimodbqm', + type=click.Choice(['coo', 'dimodbqm'], case_sensitive=False), help='Problem data encoding') @click.argument('input_file', metavar='FILE', type=click.File('rb')) def upload(config_file, profile, problem_id, format, input_file): @@ -495,7 +495,7 @@ def upload(config_file, profile, problem_id, format, input_file): "in {!r} format.").format(input_file.name, format)) if format == 'coo': - click.echo("Transcoding 'coo' to 'bq-zlib'.") + click.echo("Transcoding 'coo' to 'dimodbqm'.") try: import dimod @@ -506,17 +506,17 @@ def upload(config_file, profile, problem_id, format, input_file): # note: `BQM.from_coo` doesn't support files opened in binary (yet); # fallback to reopen for now with open(input_file.name, 'rt') as fp: - bqm = dimod.BinaryQuadraticModel.from_coo(fp) - problem = encode_problem_as_bq(bqm, compress=True)['data'] + bqm = dimod.AdjVectorBQM.from_coo(fp) + problem_file = bqm_as_file(bqm) - elif format == 'bq-zlib': - problem = input_file + elif format == 'dimodbqm': + problem_file = input_file click.echo("Uploading...") try: future = client.upload_problem_encoded( - problem=problem, problem_id=problem_id) + problem=problem_file, problem_id=problem_id) remote_problem_id = future.result() except Exception as e: click.echo(e) diff --git a/dwave/cloud/coders.py b/dwave/cloud/coders.py index cb2cce8e..d6aa34df 100644 --- a/dwave/cloud/coders.py +++ b/dwave/cloud/coders.py @@ -262,19 +262,7 @@ def decode_qp_numpy(msg, return_matrix=True): return result -def _encode_problem_as_bq_json(problem): - assert hasattr(problem, 'to_serializable') - - return problem.to_serializable(use_bytes=False) - -def _encode_problem_as_bq_json_zlib(problem): - assert hasattr(problem, 'to_serializable') - - bqm_dict = _encode_problem_as_bq_json(problem) - return zlib.compress(codecs.encode(json.dumps(bqm_dict), "ascii")) - - -def encode_problem_as_bq(problem, compress=False): +def encode_problem_as_bq(problem): """Encode the binary quadratic problem for submission in the `bq` data format. @@ -292,18 +280,15 @@ def encode_problem_as_bq(problem, compress=False): 'data': problem } - if compress: - return { - 'format': 'bq-zlib', - 'data': _encode_problem_as_bq_json_zlib(problem) - } - - else: + # NOTE: semi-deprecated format; see `bqm_as_file`. + if hasattr(problem, 'to_serializable'): return { 'format': 'bq', - 'data': _encode_problem_as_bq_json(problem) + 'data': problem.to_serializable(use_bytes=False) } + raise TypeError("unsupported problem type") + def decode_bq(msg): """Decode answer for problem submitted in the `bq` data format.""" @@ -325,3 +310,40 @@ def decode_bq(msg): result['problem_type'] = 'bqm' return result + + +def bqm_as_file(bqm, **options): + """Encode in-memory BQM as DIMODBQM binary file format. + + Args: + bqm (:class:`~dimod.BQM`): + Binary quadratic model. + + **options (dict): + :class:`~dimod.serialization.fileview.FileView` options. + + Returns: + file-like: + Binary stream with BQM encoded in DIMODBQM format. + """ + # This now the preferred way of BQM binary serialization. + + # XXX: temporary implemented here, until something like + # dwavesystems/dimod#599 is available. + + try: + import dimod + from dimod.serialization.fileview import FileView as BQMFileView + except ImportError: # pragma: no cover + raise RuntimeError("Can't encode BQM without 'dimod'. " + "Re-install the library with 'bqm' support.") + + if isinstance(bqm, BQMFileView): + return bqm + + # test explicitly to avoid copy on cast if possible + fileviewable = (dimod.AdjArrayBQM, dimod.AdjVectorBQM, dimod.AdjMapBQM) + if not isinstance(bqm, fileviewable): + bqm = AdjVectorBQM(bqm) + + return BQMFileView(bqm, **options) diff --git a/dwave/cloud/solver.py b/dwave/cloud/solver.py index a7eb9617..7020bb53 100644 --- a/dwave/cloud/solver.py +++ b/dwave/cloud/solver.py @@ -36,7 +36,7 @@ from dwave.cloud.exceptions import * from dwave.cloud.coders import ( encode_problem_as_qp, encode_problem_as_bq, - decode_qp_numpy, decode_qp, decode_bq) + decode_qp_numpy, decode_qp, decode_bq, bqm_as_file) from dwave.cloud.utils import uniform_iterator, reformat_qubo_as_ising from dwave.cloud.computation import Future from dwave.cloud.concurrency import Present @@ -307,7 +307,7 @@ def sample_qubo(self, qubo, **params): bqm = dimod.BinaryQuadraticModel.from_qubo(qubo) return self.sample_bqm(bqm, **params) - def _encode_any_problem_as_ref(self, problem, params): + def _encode_problem_as_ref(self, problem, params): """Encode `problem` for submitting in `ref` format. Upload the problem if it's not already uploaded. @@ -361,7 +361,7 @@ def sample_bqm(self, bqm, **params): # encode the request (body as future) body = self.client._encode_problem_executor.submit( - self._encode_any_problem_as_ref, + self._encode_problem_as_ref, problem=bqm, params=params) # computation future holds a reference to the remote job @@ -372,33 +372,6 @@ def sample_bqm(self, bqm, **params): return computation - @staticmethod - def _bqm_as_fileview(bqm): - # New preferred BQM binary serialization. - # XXX: temporary until something like dwavesystems/dimod#599 is implemented. - - try: - import dimod - from dimod.serialization.fileview import FileView as BQMFileView - except ImportError: # pragma: no cover - return - - if isinstance(bqm, BQMFileView): - return bqm - - # test explicitly to avoid copy on cast if possible - fileviewable = (dimod.AdjArrayBQM, dimod.AdjVectorBQM, dimod.AdjMapBQM) - if not isinstance(bqm, fileviewable): - try: - bqm = AdjVectorBQM(bqm) - except: - return - - try: - return BQMFileView(bqm) - except: - return - def upload_bqm(self, bqm): """Upload the specified :term:`BQM` to SAPI, returning a Problem ID that can be used to submit the BQM to this solver (i.e. call the @@ -420,14 +393,13 @@ def upload_bqm(self, bqm): To use this method, dimod package has to be installed. """ - data = self._bqm_as_fileview(bqm) - if data is None: - if hasattr(bqm, 'to_serializable'): - # soon to be deprecated - data = encode_problem_as_bq(bqm, compress=True)['data'] - else: - # raw data (ready for upload) in `bqm` - data = bqm + try: + data = bqm_as_file(bqm) + except Exception as e: + logger.debug("BQM conversion to file failed with %r, " + "assuming data already encoded.", e) + # assume `bqm` given as file, ready for upload + data = bqm return self.client.upload_problem_encoded(data) diff --git a/tests/test_cli.py b/tests/test_cli.py index 162456bf..d1ceedf6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -318,7 +318,7 @@ def test_solvers(self): def test_upload(self): config_file = 'dwave.conf' profile = 'profile' - format = 'bq-zlib' + format = 'dimodbqm' problem_id = 'prob:lem:id' filename = 'filename' From 76de3ba704efcdbdd53a2616346f02f51bdd1d82 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Thu, 23 Jul 2020 09:22:52 -0700 Subject: [PATCH 08/17] Break up bq encoder to bq and ref (be explicit) --- dwave/cloud/coders.py | 57 +++++++++++++++++++++++++++++++------------ dwave/cloud/solver.py | 5 ++-- tests/test_coders.py | 29 +++++++++++++++++----- 3 files changed, 66 insertions(+), 25 deletions(-) diff --git a/dwave/cloud/coders.py b/dwave/cloud/coders.py index d6aa34df..07498d3b 100644 --- a/dwave/cloud/coders.py +++ b/dwave/cloud/coders.py @@ -21,7 +21,12 @@ from dwave.cloud.utils import ( uniform_iterator, uniform_get, strip_tail, active_qubits) -__all__ = ['encode_problem_as_qp', 'decode_qp', 'decode_qp_numpy'] +__all__ = [ + 'encode_problem_as_qp', 'decode_qp', 'decode_qp_numpy', + 'encode_problem_as_bq', 'decode_bq', + 'encode_problem_as_ref', + 'bqm_as_file', +] def encode_problem_as_qp(solver, linear, quadratic, undirected_biases=False): @@ -262,32 +267,52 @@ def decode_qp_numpy(msg, return_matrix=True): return result -def encode_problem_as_bq(problem): - """Encode the binary quadratic problem for submission in the `bq` data +def encode_problem_as_ref(problem): + """Encode the problem given via reference for submission in the `ref` data format. Args: - problem (:class:`~dimod.BinaryQuadraticModel`/str): - A binary quadratic model, or a reference to one (Problem ID). + problem (str): + A reference to an uploaded problem (Problem ID). Returns: encoded submission dictionary """ - if isinstance(problem, str): - return { - 'format': 'ref', - 'data': problem - } + if not isinstance(problem, str): + raise TypeError("unsupported problem type") + + return { + 'format': 'ref', + 'data': problem + } + + +def encode_problem_as_bq(problem): + """Encode the binary quadratic problem for submission in the `bq` data + format. + Args: + problem (:class:`~dimod.BinaryQuadraticModel`): + A binary quadratic model. + + Returns: + encoded submission dictionary + + Note: + The `bq` format assumes the complete BQM is sent embedded in the sample + job submit data, something none of the production solvers currently + support. + """ # NOTE: semi-deprecated format; see `bqm_as_file`. - if hasattr(problem, 'to_serializable'): - return { - 'format': 'bq', - 'data': problem.to_serializable(use_bytes=False) - } - raise TypeError("unsupported problem type") + if not hasattr(problem, 'to_serializable'): + raise TypeError("unsupported problem type") + + return { + 'format': 'bq', + 'data': problem.to_serializable(use_bytes=False) + } def decode_bq(msg): diff --git a/dwave/cloud/solver.py b/dwave/cloud/solver.py index 7020bb53..fb07a93b 100644 --- a/dwave/cloud/solver.py +++ b/dwave/cloud/solver.py @@ -35,7 +35,7 @@ from dwave.cloud.exceptions import * from dwave.cloud.coders import ( - encode_problem_as_qp, encode_problem_as_bq, + encode_problem_as_qp, encode_problem_as_ref, decode_qp_numpy, decode_qp, decode_bq, bqm_as_file) from dwave.cloud.utils import uniform_iterator, reformat_qubo_as_ising from dwave.cloud.computation import Future @@ -321,7 +321,6 @@ def _encode_problem_as_ref(self, problem, params): Returns: str: JSON-encoded problem submit body - """ if isinstance(problem, str): @@ -333,7 +332,7 @@ def _encode_problem_as_ref(self, problem, params): body = json.dumps({ 'solver': self.id, - 'data': encode_problem_as_bq(problem_id), + 'data': encode_problem_as_ref(problem_id), 'type': 'bqm', 'params': params }) diff --git a/tests/test_coders.py b/tests/test_coders.py index 3eba6dbf..fbc87668 100644 --- a/tests/test_coders.py +++ b/tests/test_coders.py @@ -24,7 +24,7 @@ from dwave.cloud.coders import ( encode_problem_as_qp, decode_qp, decode_qp_numpy, - encode_problem_as_bq, decode_bq) + encode_problem_as_bq, decode_bq, encode_problem_as_ref) from dwave.cloud.solver import StructuredSolver, UnstructuredSolver from dwave.cloud.utils import generate_const_ising_problem @@ -189,9 +189,9 @@ def test_qp_response_numpy_decoding_numpy_array(self): np.testing.assert_array_equal(res.get('num_occurrences'), np.array(self.res_num_occurrences)) -class TestBQCoders(unittest.TestCase): +class TestBQMCoders(unittest.TestCase): - def test_bq_request_encoding_empty_bqm(self): + def test_bq_encodes_empty_bqm(self): """Empty BQM has to be trivially encoded.""" bqm = dimod.BQM.from_qubo({}) @@ -202,7 +202,7 @@ def test_bq_request_encoding_empty_bqm(self): self.assertEqual(pluck(req, 'data.num_variables'), 0) self.assertEqual(pluck(req, 'data.num_interactions'), 0) - def test_bq_request_encoding_ising_bqm(self): + def test_bq_encodes_ising_bqm(self): """Simple Ising BQM properly encoded.""" bqm = dimod.BQM.from_ising({0: 1}, {(0, 1): 1}) @@ -214,7 +214,7 @@ def test_bq_request_encoding_ising_bqm(self): self.assertEqual(pluck(req, 'data.num_variables'), 2) self.assertEqual(pluck(req, 'data.num_interactions'), 1) - def test_bq_request_encoding_qubo_bqm(self): + def test_bq_encodes_qubo_bqm(self): """Simple Qubo BQM properly encoded.""" bqm = dimod.BQM.from_qubo({(0, 1): 1}) @@ -226,7 +226,7 @@ def test_bq_request_encoding_qubo_bqm(self): self.assertEqual(pluck(req, 'data.num_variables'), 2) self.assertEqual(pluck(req, 'data.num_interactions'), 1) - def test_bq_request_encoding_bqm_named_vars(self): + def test_bq_encodes_bqm_with_named_vars(self): """BQM with named variable properly encoded.""" bqm = dimod.BQM.from_ising({}, {'ab': 1, 'bc': 1, 'ca': 1}) @@ -239,6 +239,23 @@ def test_bq_request_encoding_bqm_named_vars(self): self.assertEqual(pluck(req, 'data.num_interactions'), 3) self.assertEqual(pluck(req, 'data.variable_labels'), list('abc')) + def test_ref_encoder(self): + problem_id = '123' + + req = encode_problem_as_ref(problem_id) + + self.assertEqual(req.get('format'), 'ref') + self.assertEqual(req.get('data'), problem_id) + + def test_bq_ref_input_validation(self): + problem_id = '123' + with self.assertRaises(TypeError): + encode_problem_as_bq(problem_id) + + bqm = dimod.BQM.from_qubo({}) + with self.assertRaises(TypeError): + encode_problem_as_ref(bqm) + def test_bq_response_decoding(self): """Answer to simple problem properly decoded.""" From ddc2927b5e04dfd7a0f9898788d13f999c525419 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 4 Sep 2020 05:38:42 -0700 Subject: [PATCH 09/17] Deprecate Future.occurrences in favor of .num_occurrences Schedule for removal in 0.10.0+ --- dwave/cloud/computation.py | 33 +++++++++++++++------- tests/test_mock_submission.py | 8 +++++- tests/test_mock_unstructured_submission.py | 8 +++--- tests/test_solver.py | 24 ++++++++++------ 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/dwave/cloud/computation.py b/dwave/cloud/computation.py index 2d7ad045..2660e402 100644 --- a/dwave/cloud/computation.py +++ b/dwave/cloud/computation.py @@ -568,7 +568,7 @@ def result(self): them anymore, on client side. For QPU solvers, please replace `'samples'` with `'solutions'` and `'occurrences'` with `'num_occurrences'`. Better yet, use :meth:`Future.samples` and - :meth:`Future.occurrences` instead. + :meth:`Future.num_occurrences` instead. Examples: This example creates a solver using the local system's default @@ -695,18 +695,17 @@ def variables(self): raise InvalidAPIResponseError("Active variables not present in the response") - # XXX: rename to num_occurrences, alias as occurrences, but deprecate it @property - def occurrences(self): - """Occurrences buffer for the submitted job. + def num_occurrences(self): + """Number of sample occurrences buffer for the submitted job. First calls to access data of a :class:`Future` object are blocking; subsequent access to this property is non-blocking. Returns: - list or NumPy matrix of doubles: Occurrences. When returned results - are ordered in a histogram, `occurrences` indicates the number of - times a particular solution recurred. + list or NumPy matrix of doubles: number of occurrences. When + returned results are ordered in a histogram, `num_occurrences` + indicates the number of times a particular solution recurred. Examples: This example creates a solver using the local system's default @@ -721,10 +720,10 @@ def occurrences(self): ... solver = client.get_solver() ... quad = {(16, 20): -1, (17, 20): 1, (16, 21): 1, (17, 21): 1} ... computation = solver.sample_ising({}, quad, num_reads=500, answer_mode='histogram') - ... for i in range(len(computation.occurrences)): + ... for i in range(len(computation.num_occurrences)): ... print(computation.samples[i][16], computation.samples[i][17], ... computation.samples[i][20], computation.samples[i][21], - ' --> ', computation.energies[i], computation.occurrences[i]) + ... ' --> ', computation.energies[i], computation.num_occurrences[i]) ... (-1, 1, -1, -1, ' --> ', -2.0, 41) (-1, -1, -1, 1, ' --> ', -2.0, 53) @@ -751,6 +750,20 @@ def occurrences(self): else: return [1] * len(result['solutions']) + @property + def occurrences(self): + """Deprecated in favor of Future.num_occurrences property. + + Scheduled for removal in 0.10.0. + """ + warnings.warn( + "'Future.occurrences' is deprecated, and it will be removed " + "in 0.10.0, or in 1.0.0 at latest. Please convert your code to use " + "'Future.num_occurrences'", + DeprecationWarning) + + return self.num_occurrences + def wait_sampleset(self): """Blocking sampleset getter.""" @@ -779,7 +792,7 @@ def wait_sampleset(self): sampleset = dimod.SampleSet.from_samples( (samples, variables), vartype=vartype, - energy=self.energies, num_occurrences=self.occurrences, + energy=self.energies, num_occurrences=self.num_occurrences, info=info, sort_labels=True) # this means that samplesets retrieved BEFORE this function are called diff --git a/tests/test_mock_submission.py b/tests/test_mock_submission.py index 76985aab..99f69e9b 100644 --- a/tests/test_mock_submission.py +++ b/tests/test_mock_submission.py @@ -19,6 +19,7 @@ import unittest import itertools import threading +import warnings import collections from unittest import mock @@ -199,7 +200,12 @@ def raise_for_status(): class _QueryTest(unittest.TestCase): def _check(self, results, linear, quad, offset=0, num_reads=1): # Did we get the right number of samples? - self.assertEqual(num_reads, sum(results.occurrences)) + self.assertEqual(num_reads, sum(results.num_occurrences)) + + # verify .occurrences property still works, although is deprecated + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertEqual(100, sum(results.occurrences)) # Make sure energies are correct in raw results for energy, state in zip(results.energies, results.samples): diff --git a/tests/test_mock_unstructured_submission.py b/tests/test_mock_unstructured_submission.py index 6c87804f..b5af6f26 100644 --- a/tests/test_mock_unstructured_submission.py +++ b/tests/test_mock_unstructured_submission.py @@ -95,14 +95,14 @@ def mock_upload(self, bqm): numpy.testing.assert_array_equal(fut.sampleset, ss) numpy.testing.assert_array_equal(fut.samples, ss.record.sample) numpy.testing.assert_array_equal(fut.energies, ss.record.energy) - numpy.testing.assert_array_equal(fut.occurrences, ss.record.num_occurrences) + numpy.testing.assert_array_equal(fut.num_occurrences, ss.record.num_occurrences) # submit of pre-uploaded bqm problem fut = solver.sample_bqm(mock_problem_id) numpy.testing.assert_array_equal(fut.sampleset, ss) numpy.testing.assert_array_equal(fut.samples, ss.record.sample) numpy.testing.assert_array_equal(fut.energies, ss.record.energy) - numpy.testing.assert_array_equal(fut.occurrences, ss.record.num_occurrences) + numpy.testing.assert_array_equal(fut.num_occurrences, ss.record.num_occurrences) # ising sampling lin, quad, _ = bqm.to_ising() @@ -114,7 +114,7 @@ def mock_upload(self, bqm): numpy.testing.assert_array_equal(fut.sampleset, ss) numpy.testing.assert_array_equal(fut.samples, ss.record.sample) numpy.testing.assert_array_equal(fut.energies, ss.record.energy) - numpy.testing.assert_array_equal(fut.occurrences, ss.record.num_occurrences) + numpy.testing.assert_array_equal(fut.num_occurrences, ss.record.num_occurrences) # qubo sampling qubo, _ = bqm.to_qubo() @@ -126,7 +126,7 @@ def mock_upload(self, bqm): numpy.testing.assert_array_equal(fut.sampleset, ss) numpy.testing.assert_array_equal(fut.samples, ss.record.sample) numpy.testing.assert_array_equal(fut.energies, ss.record.energy) - numpy.testing.assert_array_equal(fut.occurrences, ss.record.num_occurrences) + numpy.testing.assert_array_equal(fut.num_occurrences, ss.record.num_occurrences) def test_upload_failure(self): """Submit should gracefully fail if upload as part of submit fails.""" diff --git a/tests/test_solver.py b/tests/test_solver.py index 8076db19..f28abc3d 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -21,6 +21,7 @@ import unittest import random +import warnings from datetime import datetime import numpy @@ -97,7 +98,12 @@ def _submit_and_check(self, solver, linear, quad, **kwargs): results = solver.sample_ising(linear, quad, num_reads=100, **kwargs) # Did we get the right number of samples? - self.assertEqual(100, sum(results.occurrences)) + self.assertEqual(100, sum(results.num_occurrences)) + + # verify .occurrences property still works, although is deprecated + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertEqual(100, sum(results.occurrences)) # offset is optional offset = kwargs.get('offset', 0) @@ -215,7 +221,7 @@ def test_submit_bqm_ising_problem(self): sampleset = response.sampleset # Did we get the right number of samples? - self.assertEqual(100, sum(response.occurrences)) + self.assertEqual(100, sum(response.num_occurrences)) # Make sure the number of occurrences and energies are all correct numpy.testing.assert_array_almost_equal( @@ -237,7 +243,7 @@ def test_submit_bqm_qubo_problem(self): sampleset = response.sampleset # Did we get the right number of samples? - self.assertEqual(100, sum(response.occurrences)) + self.assertEqual(100, sum(response.num_occurrences)) # Make sure the number of occurrences and energies are all correct numpy.testing.assert_array_almost_equal( @@ -346,7 +352,7 @@ def test_submit_batch(self): for results, linear, quad in result_list: # Did we get the right number of samples? - self.assertEqual(10, sum(results.occurrences)) + self.assertEqual(10, sum(results.num_occurrences)) # Make sure the number of occurrences and energies are all correct for energy, state in zip(results.energies, results.samples): @@ -373,7 +379,7 @@ def test_cancel_batch(self): # Responses must be canceled or correct try: # Did we get the right number of samples? - self.assertEqual(max_num_reads, sum(results.occurrences)) + self.assertEqual(max_num_reads, sum(results.num_occurrences)) # Make sure the number of occurrences and energies are all correct for energy, state in zip(results.energies, results.samples): @@ -402,7 +408,7 @@ def test_wait_many(self): for results, linear, quad in result_list: # Did we get the right number of samples? - self.assertEqual(40, sum(results.occurrences)) + self.assertEqual(40, sum(results.num_occurrences)) # Make sure the number of occurrences and energies are all correct for energy, state in zip(results.energies, results.samples): @@ -423,7 +429,7 @@ def test_as_completed(self): # Go over computations, one by one, as they're done and check they're OK for computation in dwave.cloud.computation.Future.as_completed(computations): self.assertTrue(computation.done()) - self.assertEqual(40, sum(computation.occurrences)) + self.assertEqual(40, sum(computation.num_occurrences)) for energy, state in zip(computation.energies, computation.samples): self.assertAlmostEqual(energy, evaluate_ising(linear, quad, state)) @@ -479,7 +485,7 @@ def test_request_matrix_with_numpy(self): result = self._submit_and_check(solver, linear, quad) self.assertIsInstance(result.samples, numpy.ndarray) self.assertIsInstance(result.energies, numpy.ndarray) - self.assertIsInstance(result.occurrences, numpy.ndarray) + self.assertIsInstance(result.num_occurrences, numpy.ndarray) def test_request_list_with_no_numpy(self): """Submit a problem using a dict for the linear terms.""" @@ -543,7 +549,7 @@ def test_request_raw_matrix_with_numpy(self): result = self._submit_and_check(solver, linear, quad, answer_mode='raw') self.assertIsInstance(result.samples, numpy.ndarray) self.assertIsInstance(result.energies, numpy.ndarray) - self.assertIsInstance(result.occurrences, numpy.ndarray) + self.assertIsInstance(result.num_occurrences, numpy.ndarray) def test_request_raw_list_with_no_numpy(self): """Submit a problem using a dict for the linear terms.""" From 2e962779963628dca417f03adb6d244830456bcd Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 4 Sep 2020 13:19:53 -0700 Subject: [PATCH 10/17] Add aliasdict, a dict subclass with support for alias items --- dwave/cloud/utils.py | 96 +++++++++++++++++++++++++ tests/test_utils.py | 167 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 261 insertions(+), 2 deletions(-) diff --git a/dwave/cloud/utils.py b/dwave/cloud/utils.py index 41a026c1..2f1ae0b1 100644 --- a/dwave/cloud/utils.py +++ b/dwave/cloud/utils.py @@ -547,3 +547,99 @@ def get_platform_tags(): fs = [ep.load() for ep in iter_entry_points('dwave.common.platform.tags')] tags = list(filter(None, [f() for f in fs])) return tags + + +class aliasdict(dict): + """A dict subclass with support for item aliasing -- when you want to allow + explicit access to some keys, but not to store them in the dict. + + :class:`aliasdict` can be used as a stand-in replacement for :class:`dict`. + If no aliases are added, behavior is identical to :class:`dict`. + + Alias items added can be explicitly accessed, but they are not visible + otherwise via the dict interface. Aliases shadow original keys, and their + values can be computed on access only. + + Aliases are added with :meth:`.alias`, and they are stored in the + :attr:`.aliases` class instance dictionary. + + Example: + >>> from operator import itemgetter + >>> from dwave.cloud.utils import aliasdict + + >>> d = aliasdict(a=1, b=2) + >>> d.alias(c=itemgetter('a')) + >>> d + {'a': 1, 'b': 2} + >>> 'c' in d + True + >>> d['c'] + 1 + + """ + __slots__ = ('aliases', ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # keep alias keys and reference values separate from the base dict + self.aliases = {} + + def alias(self, *args, **kwargs): + """Update aliases dictionary with the key/value pairs from other, + overwriting existing keys. + + Args: + other (dict/Iterable[(key,value)]): + Either another dictionary object or an iterable of key/value + pairs (as tuples or other iterables of length two). If keyword + arguments are specified, the dictionary is then updated with + those key/value pairs ``d.alias(red=1, blue=2)``. + + Note: + Alias key will become available via item getter, but it will not + be listed in the container. + + Alias value can be a concrete value for the alias key, or it can be + a callable that is evaluated on the aliasdict instance, on each + access. + + """ + self.aliases.update(*args, **kwargs) + + def _alias_value(self, key): + value = self.aliases[key] + if callable(value): + value = value(self) + return value + + def __getitem__(self, key): + if key in self.aliases: + return self._alias_value(key) + return super().__getitem__(key) + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def __setitem__(self, key, value): + if key in self.aliases: + return self.aliases.__setitem__(key, value) + return super().__setitem__(key, value) + + def __delitem__(self, key): + if key in self.aliases: + return self.aliases.__delitem__(key) + return super().__delitem__(key) + + def __contains__(self, key): + if key in self.aliases: + return True + return super().__contains__(key) + + def copy(self): + new = type(self)(self) + new.alias(self.aliases) + return new diff --git a/tests/test_utils.py b/tests/test_utils.py index 2ec424f5..fad5c709 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy +import uuid import logging import unittest from unittest import mock @@ -22,8 +24,8 @@ from dwave.cloud.utils import ( uniform_iterator, uniform_get, strip_head, strip_tail, active_qubits, generate_random_ising_problem, - default_text_input, utcnow, cached, retried, parse_loglevel, - user_agent) + default_text_input, utcnow, cached, retried, aliasdict, + parse_loglevel, user_agent) class TestSimpleUtils(unittest.TestCase): @@ -342,5 +344,166 @@ def f(): sleep.assert_has_calls(calls) +class TestAliasdict(unittest.TestCase): + + def assert_dict_interface(self, aliased, origin): + "Assert `aliased` behaves exactly as `origin` dict." + + self.assertIsInstance(aliased, dict) + self.assertDictEqual(aliased, origin) + + self.assertEqual(len(aliased), len(origin)) + self.assertSetEqual(set(aliased), set(origin)) + self.assertSetEqual(set(aliased.keys()), set(origin.keys())) + self.assertSetEqual(set(aliased.items()), set(origin.items())) + self.assertListEqual(list(aliased.values()), list(origin.values())) + for k in origin: + self.assertIn(k, aliased) + stranger = "unique-{}".format(''.join(origin)) + self.assertNotIn(stranger, aliased) + + self.assertSetEqual(set(iter(aliased)), set(origin)) + + # copy + new = aliased.copy() + self.assertIsNot(new, aliased) + self.assertDictEqual(new, aliased) + + # dict copy constructor on aliasdict + new = dict(aliased) + self.assertDictEqual(new, origin) + + # pop on copy + key = next(iter(origin)) + new.pop(key) + self.assertSetEqual(set(new), set(origin).difference(key)) + self.assertDictEqual(aliased, origin) + + # get + self.assertEqual(aliased[key], origin[key]) + self.assertEqual(aliased.get(key), origin.get(key)) + + # set + new = aliased.copy() + ref = origin.copy() + new[stranger] = 4 + ref[stranger] = 4 + self.assertDictEqual(new, ref) + + # del + del new[stranger] + self.assertDictEqual(new, origin) + + def test_construction(self): + # aliasdict can be constructed from a mapping/dict + src = dict(a=1) + ad = aliasdict(src) + self.assert_dict_interface(ad, src) + + # aliasdict can be updated, without affecting the source dict + ad.update(b=2) + self.assertDictEqual(ad, dict(a=1, b=2)) + self.assertDictEqual(src, dict(a=1)) + + # source dict can be updated without affecting the aliased dict + src.update(c=3) + self.assertDictEqual(ad, dict(a=1, b=2)) + self.assertDictEqual(src, dict(a=1, c=3)) + + def test_dict_interface(self): + src = dict(a=1, b=2) + ad = aliasdict(**src) + self.assert_dict_interface(ad, src) + + def test_alias_concrete(self): + src = dict(a=1) + + ad = aliasdict(src) + ad.alias(b=2) + + self.assert_dict_interface(ad, src) + self.assertEqual(ad['b'], 2) + + def test_alias_callable(self): + src = dict(a=1) + + ad = aliasdict(src) + ad.alias(b=lambda d: d.get('a')) + + self.assert_dict_interface(ad, src) + + # 'b' equals 'a' + self.assertEqual(ad['b'], ad['a']) + self.assertEqual(ad['b'], src['a']) + self.assertEqual(ad['b'], 1) + + # it's dynamic, works also when 'a' changes + ad['a'] = 2 + self.assertEqual(ad['b'], ad['a']) + self.assertNotEqual(ad['b'], src['a']) + self.assertEqual(ad['b'], 2) + + def test_alias_get_set_del(self): + src = dict(a=1) + ad = aliasdict(src) + + # getitem and get on alias + ad.alias(b=2) + self.assertEqual(ad['b'], 2) + self.assertEqual(ad.get('b'), 2) + self.assertEqual(ad.aliases['b'], 2) + + # get with a default + randomkey = str(uuid.uuid4()) + self.assertEqual(ad.get(randomkey), None) + self.assertEqual(ad.get(randomkey, 1), 1) + + # set alias + ad['b'] = 3 + self.assertEqual(ad['b'], 3) + self.assertEqual(ad.aliases['b'], 3) + + # update alias + ad.alias(b=4) + self.assertEqual(ad['b'], 4) + self.assertEqual(ad.aliases['b'], 4) + + # delete alias + del ad['b'] + self.assertEqual(ad.get('b'), None) + + def test_shadowing(self): + "Alias keys take precedence." + + ad = aliasdict(a=1) + ad.alias(a=2) + + self.assertEqual(ad['a'], 2) + + self.assertEqual(dict.__getitem__(ad, 'a'), 1) + + def test_copy(self): + src = dict(a=1) + aliases = dict(b=2) + + ad = aliasdict(src) + ad.alias(**aliases) + + self.assertDictEqual(ad, src) + self.assertDictEqual(ad.aliases, aliases) + + new = ad.copy() + self.assertIsInstance(new, aliasdict) + self.assertIsNot(new, ad) + self.assertDictEqual(new, src) + self.assertDictEqual(new.aliases, aliases) + + new = copy.deepcopy(ad) + self.assertIsInstance(new, aliasdict) + self.assertIsNot(new, ad) + self.assertDictEqual(new, src) + self.assertDictEqual(new.aliases, aliases) + + if __name__ == '__main__': unittest.main() From 8bf8f9395e3ea09075ad53b5660b758aa713ac71 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 4 Sep 2020 13:27:15 -0700 Subject: [PATCH 11/17] Use aliasdict for Future.result() dict, instead of adding alias keys --- dwave/cloud/computation.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/dwave/cloud/computation.py b/dwave/cloud/computation.py index 2660e402..dc719a20 100644 --- a/dwave/cloud/computation.py +++ b/dwave/cloud/computation.py @@ -27,15 +27,16 @@ """ -import threading import time +import threading import functools +import operator import warnings from dateutil.parser import parse from concurrent.futures import TimeoutError -from dwave.cloud.utils import utcnow, datetime_to_timestamp +from dwave.cloud.utils import utcnow, datetime_to_timestamp, aliasdict from dwave.cloud.exceptions import InvalidAPIResponseError # Use numpy if available for fast decoding @@ -922,10 +923,11 @@ def _alias_result(self): if not self._result: return - aliases = {'samples': 'solutions', - 'occurrences': 'num_occurrences'} - for alias, original in aliases.items(): - if original in self._result and alias not in self._result: - self._result[alias] = self._result[original] + aliases = dict( + samples=operator.itemgetter('solutions'), + occurrences=operator.itemgetter('num_occurrences')) + + self._result = aliasdict(self._result) + self._result.alias(aliases) return self._result From ba495c26fae07116a171e69be8b16cc170e42d99 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 4 Sep 2020 13:50:42 -0700 Subject: [PATCH 12/17] Add `deprecated` decorator for wrapping fn calls with deprecation warn --- dwave/cloud/utils.py | 26 +++++++++++++++++++++++++ tests/test_utils.py | 46 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/dwave/cloud/utils.py b/dwave/cloud/utils.py index 2f1ae0b1..2447a824 100644 --- a/dwave/cloud/utils.py +++ b/dwave/cloud/utils.py @@ -19,6 +19,7 @@ import platform import itertools import numbers +import warnings from collections import abc, OrderedDict from urllib.parse import urljoin @@ -484,6 +485,31 @@ def __exit__(self, exc_type, exc_value, traceback): self.dt = time.perf_counter() - self.tick +class deprecated(object): + """Decorator that issues a deprecation message on each call of the + decorated function. + """ + + def __init__(self, msg=None): + self.msg = msg + + def __call__(self, fn): + if not callable(fn): + raise TypeError("decorated object must be callable") + + @wraps(fn) + def wrapped(*args, **kwargs): + msg = self.msg + if not msg: + fn_name = getattr(fn, '__name__', 'unnamed') + msg = "{}() has been deprecated".format(fn_name) + warnings.warn(msg, DeprecationWarning) + + return fn(*args, **kwargs) + + return wrapped + + def parse_loglevel(level_name, default=logging.NOTSET): """Resolve numeric and symbolic log level names to numeric levels.""" diff --git a/tests/test_utils.py b/tests/test_utils.py index fad5c709..a1a3dc5d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -16,6 +16,7 @@ import uuid import logging import unittest +import warnings from unittest import mock from collections import OrderedDict from itertools import count @@ -24,7 +25,7 @@ from dwave.cloud.utils import ( uniform_iterator, uniform_get, strip_head, strip_tail, active_qubits, generate_random_ising_problem, - default_text_input, utcnow, cached, retried, aliasdict, + default_text_input, utcnow, cached, retried, deprecated, aliasdict, parse_loglevel, user_agent) @@ -344,6 +345,49 @@ def f(): sleep.assert_has_calls(calls) +class TestDeprecatedDecorator(unittest.TestCase): + + def test_func_called(self): + """Wrapped function is called with correct arguments.""" + + @deprecated() + def f(a, b): + return a, b + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + self.assertEqual(f(1, b=2), (1, 2)) + + def test_warning_raised(self): + """Correct deprecation message is raised.""" + + msg = "deprecation message" + + @deprecated(msg) + def f(): + return + + with self.assertWarns(DeprecationWarning, msg=msg): + f() + + def test_warning_raised_automsg(self): + """Deprecation message is auto-generated and raised.""" + + @deprecated() + def f(): + return + + automsg_regex = r'f\(\) has been deprecated' + + with self.assertWarnsRegex(DeprecationWarning, automsg_regex): + f() + + def test_decorator(self): + with self.assertRaises(TypeError): + deprecated()("not-a-function") + + class TestAliasdict(unittest.TestCase): def assert_dict_interface(self, aliased, origin): From f369f9f166105029e8dfa3c36c7951f32eb61323 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 4 Sep 2020 14:14:33 -0700 Subject: [PATCH 13/17] Deprecate alias result keys (raise warning on access) --- dwave/cloud/computation.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/dwave/cloud/computation.py b/dwave/cloud/computation.py index dc719a20..3c0f11a0 100644 --- a/dwave/cloud/computation.py +++ b/dwave/cloud/computation.py @@ -30,13 +30,14 @@ import time import threading import functools -import operator import warnings +from operator import itemgetter from dateutil.parser import parse from concurrent.futures import TimeoutError -from dwave.cloud.utils import utcnow, datetime_to_timestamp, aliasdict +from dwave.cloud.utils import ( + utcnow, datetime_to_timestamp, aliasdict, deprecated) from dwave.cloud.exceptions import InvalidAPIResponseError # Use numpy if available for fast decoding @@ -751,6 +752,7 @@ def num_occurrences(self): else: return [1] * len(result['solutions']) + # TODO: remove in 0.10.0+ @property def occurrences(self): """Deprecated in favor of Future.num_occurrences property. @@ -759,8 +761,7 @@ def occurrences(self): """ warnings.warn( "'Future.occurrences' is deprecated, and it will be removed " - "in 0.10.0, or in 1.0.0 at latest. Please convert your code to use " - "'Future.num_occurrences'", + "in 0.10.0+. Please convert your code to use 'Future.num_occurrences'", DeprecationWarning) return self.num_occurrences @@ -914,18 +915,22 @@ def _decode(self): self.parse_time = time.time() - start return self._result + # TODO: schedule for removal def _alias_result(self): - """Create aliases for some of the keys in the results dict. Eventually, - those will be renamed on the server side. + """Alias `solutions` and `num_occurrences`. - Deprecated since version 0.6.0. Will be removed in 0.7.0. + Deprecated in version 0.8.0. """ if not self._result: return + msg = "'{}' alias has been deprecated in favor of '{}'" + samples_msg = msg.format('samples', 'solutions') + occurrences_msg = msg.format('occurrences', 'num_occurrences') + aliases = dict( - samples=operator.itemgetter('solutions'), - occurrences=operator.itemgetter('num_occurrences')) + samples=deprecated(samples_msg)(itemgetter('solutions')), + occurrences=deprecated(occurrences_msg)(itemgetter('num_occurrences'))) self._result = aliasdict(self._result) self._result.alias(aliases) From fa0f3e688072139d47cddf7763afe9ac46fd9941 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 4 Sep 2020 14:16:05 -0700 Subject: [PATCH 14/17] Test all computation.Future deprecations --- tests/test_mock_submission.py | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/test_mock_submission.py b/tests/test_mock_submission.py index 99f69e9b..8d26c22f 100644 --- a/tests/test_mock_submission.py +++ b/tests/test_mock_submission.py @@ -1030,3 +1030,42 @@ def create_mock_session(client): # but because the offset in answer is wrong, energies are off with self.assertRaises(AssertionError): self._check(results, linear, quadratic, offset=offset, **params) + + +@mock.patch('time.sleep', lambda *x: None) +class TestComputationDeprecations(_QueryTest): + + def test_deprecations(self): + """Proper deprecation warnings are raised.""" + + def create_mock_session(client): + session = mock.Mock() + session.post = lambda a, _: choose_reply(a, { + 'problems/': '[%s]' % complete_no_answer_reply( + '123', 'abc123')}) + session.get = lambda a: choose_reply(a, { + 'problems/123/': complete_reply( + '123', 'abc123')}) + return session + + with mock.patch.object(Client, 'create_session', create_mock_session): + with Client('endpoint', 'token') as client: + solver = Solver(client, solver_data('abc123')) + + linear, quadratic = test_problem(solver) + params = dict(num_reads=100) + results = solver.sample_ising(linear, quadratic, **params) + + # aliased keys are deprecated in 0.8.0 + with self.assertWarns(DeprecationWarning): + results['samples'] + with self.assertWarns(DeprecationWarning): + results['occurrences'] + + # .error is deprecated in 0.7.x, scheduled for removal in 0.9.0 + with self.assertWarns(DeprecationWarning): + results.error + + # .occurrences is deprecated in 0.8.0, scheduled for removal in 0.10.0+ + with self.assertWarns(DeprecationWarning): + results.occurrences From 5de4a523119eb0111f3d92da6b4651a47e06cc50 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Wed, 9 Sep 2020 09:14:00 -0700 Subject: [PATCH 15/17] Update docstrings after review --- dwave/cloud/coders.py | 27 ++++++++++++++++++--------- dwave/cloud/utils.py | 2 +- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/dwave/cloud/coders.py b/dwave/cloud/coders.py index 4128c528..4477eba6 100644 --- a/dwave/cloud/coders.py +++ b/dwave/cloud/coders.py @@ -51,7 +51,7 @@ def encode_problem_as_qp(solver, linear, quadratic, offset=0, Are (quadratic) biases specified on undirected edges? Returns: - encoded submission dictionary + Encoded submission dictionary. """ active = active_qubits(linear, quadratic) @@ -162,10 +162,11 @@ def _decode_byte(byte): """Helper for decode_qp, turns a single byte into a list of bits. Args: - byte: byte to be decoded + byte (int): + Byte to be decoded. Returns: - list of bits corresponding to byte + List of bits corresponding to byte. """ bits = [] for _ in range(8): @@ -180,6 +181,13 @@ def _decode_ints(message): The int array is stored as little endian 32 bit integers. The array has then been base64 encoded. Since we are decoding we do these steps in reverse. + + Args: + message (str): + The int array, base-64 encoded. + + Returns: + Decoded double array. """ binary = base64.b64decode(message) return struct.unpack('<' + ('i' * (len(binary) // 4)), binary) @@ -193,10 +201,11 @@ def _decode_doubles(message): steps in reverse. Args: - message: the double array + message (str): + The double array, base-64 encoded. Returns: - decoded double array + Decoded double array. """ binary = base64.b64decode(message) return struct.unpack('<' + ('d' * (len(binary) // 8)), binary) @@ -289,14 +298,14 @@ def encode_problem_as_ref(problem): Args: problem (str): - A reference to an uploaded problem (Problem ID). + A reference to an uploaded problem (problem ID). Returns: - encoded submission dictionary + Encoded submission dictionary. """ if not isinstance(problem, str): - raise TypeError("unsupported problem type") + raise TypeError("unsupported problem reference type") return { 'format': 'ref', @@ -313,7 +322,7 @@ def encode_problem_as_bq(problem): A binary quadratic model. Returns: - encoded submission dictionary + Encoded submission dictionary Note: The `bq` format assumes the complete BQM is sent embedded in the sample diff --git a/dwave/cloud/utils.py b/dwave/cloud/utils.py index 2447a824..161be65f 100644 --- a/dwave/cloud/utils.py +++ b/dwave/cloud/utils.py @@ -612,7 +612,7 @@ def __init__(self, *args, **kwargs): self.aliases = {} def alias(self, *args, **kwargs): - """Update aliases dictionary with the key/value pairs from other, + """Update aliases dictionary with the key/value pairs from ``other``, overwriting existing keys. Args: From 20b5e94867a7c12d24e709cd8a48eb2c2a9d3a2a Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Wed, 9 Sep 2020 14:30:10 -0700 Subject: [PATCH 16/17] Use Ocean's glossary instead of a local one Closes dwavesystems/dwave-ocean-sdk#74. --- docs/conf.py | 10 ++++--- docs/intro.rst | 78 ++------------------------------------------------ 2 files changed, 9 insertions(+), 79 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 55447228..0cdd555c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -102,7 +102,9 @@ def iad_add_directive_header(self, sig): # Link to Python standard lib objects -intersphinx_mapping = {'python': ('https://docs.python.org/3', None), - 'qbsolv': ('https://docs.ocean.dwavesys.com/projects/qbsolv/en/latest/', None), - 'oceandocs': ('https://docs.ocean.dwavesys.com/en/latest/', None), - 'sysdocs_gettingstarted': ('https://docs.dwavesys.com/docs/latest/', None)} +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'numpy': ('https://numpy.org/doc/stable/', None), + 'oceandocs': ('https://docs.ocean.dwavesys.com/en/stable/', None), + 'sysdocs_gettingstarted': ('https://docs.dwavesys.com/docs/latest/', None), +} diff --git a/docs/intro.rst b/docs/intro.rst index 04f1cde1..5404deae 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -24,9 +24,9 @@ Configuration It's recommended you set up your D-Wave Cloud Client configuration through the :std:doc:`interactive CLI utility `. -As described in the :std:doc:`Using a D-Wave System ` section -of Ocean Documentation, for your code to access remote D-Wave compute resources, you must -configure communication through SAPI; for example, your code needs your API +As described in the :std:doc:`Configuring Access to D-Wave Solvers ` +section of Ocean Documentation, for your code to access remote D-Wave compute resources, +you must configure communication through SAPI; for example, your code needs your API token for authentication. D-Wave Cloud Client provides multiple options for configuring the required information: @@ -166,75 +166,3 @@ A typical workflow may include the following steps: 3. Submit your problem, using your solver, and then process the returned :class:`~dwave.cloud.computation.Future`, instantiated by your solver to handle remotely executed problem solving. - -Terminology -=========== - -.. glossary:: - - Ising - Traditionally used in statistical mechanics. Variables are "spin up" - (:math:`\uparrow`) and "spin down" (:math:`\downarrow`), states that - correspond to :math:`+1` and :math:`-1` values. Relationships between - the spins, represented by couplings, are correlations or anti-correlations. - The objective function expressed as an Ising model is as follows: - - .. math:: - - \begin{equation} - \text{E}_{ising}(\pmb{s}) = \sum_{i=1}^N h_i s_i + \sum_{i=1}^N \sum_{j=i+1}^N J_{i,j} s_i s_j - \end{equation} - - where the linear coefficients corresponding to qubit biases - are :math:`h_i`, and the quadratic coefficients corresponding to coupling - strengths are :math:`J_{i,j}`. - - model - A collection of variables with associated linear and - quadratic biases. - - QUBO - Quadratic unconstrained binary optimization. - QUBO problems are traditionally used in computer science. Variables - are TRUE and FALSE, states that correspond to 1 and 0 values. - A QUBO problem is defined using an upper-diagonal matrix :math:`Q`, - which is an :math:`N` x :math:`N` upper-triangular matrix of real weights, - and :math:`x`, a vector of binary variables, as minimizing the function - - .. math:: - - \begin{equation} - f(x) = \sum_{i} {Q_{i,i}}{x_i} + \sum_{i Date: Wed, 9 Sep 2020 15:02:19 -0700 Subject: [PATCH 17/17] Fix Ocean glossary link on docs index page --- docs/index.rst | 2 +- dwave/cloud/coders.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index c0032f67..5f437b96 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -46,7 +46,7 @@ Documentation Ocean Home Ocean Documentation - Ocean Glossary + Ocean Glossary .. toctree:: :caption: D-Wave diff --git a/dwave/cloud/coders.py b/dwave/cloud/coders.py index 4477eba6..2b69cbea 100644 --- a/dwave/cloud/coders.py +++ b/dwave/cloud/coders.py @@ -322,7 +322,7 @@ def encode_problem_as_bq(problem): A binary quadratic model. Returns: - Encoded submission dictionary + Encoded submission dictionary. Note: The `bq` format assumes the complete BQM is sent embedded in the sample