Skip to content

Commit

Permalink
Split the remaining utils into submodules, update code/tests
Browse files Browse the repository at this point in the history
  • Loading branch information
randomir committed Jun 13, 2024
1 parent f1bbe45 commit cfec653
Show file tree
Hide file tree
Showing 22 changed files with 119 additions and 94 deletions.
2 changes: 1 addition & 1 deletion dwave/cloud/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from dwave.cloud.api import constants, exceptions
from dwave.cloud.config import load_config, validate_config_v1
from dwave.cloud.config.models import ClientConfig
from dwave.cloud.utils import is_caused_by
from dwave.cloud.utils.exception import is_caused_by
from dwave.cloud.utils.http import PretimedHTTPAdapter, BaseUrlSession, default_user_agent

__all__ = ['DWaveAPIClient', 'SolverAPIClient', 'MetadataAPIClient', 'LeapAPIClient']
Expand Down
2 changes: 1 addition & 1 deletion dwave/cloud/auth/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from dwave.cloud.config.models import ClientConfig
from dwave.cloud.regions import resolve_endpoints
from dwave.cloud.utils.http import default_user_agent
from dwave.cloud.utils import pretty_argvalues
from dwave.cloud.utils.logging import pretty_argvalues

__all__ = ['AuthFlow', 'LeapAuthFlow', 'OAuthError']

Expand Down
8 changes: 4 additions & 4 deletions dwave/cloud/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@
from dwave.cloud import Client
from dwave.cloud import api
from dwave.cloud.solver import StructuredSolver, BaseUnstructuredSolver
from dwave.cloud.utils.logging import configure_logging
from dwave.cloud.utils.qubo import generate_random_ising_problem
from dwave.cloud.utils.http import user_agent
from dwave.cloud.utils.time import datetime_to_timestamp, utcnow, epochnow
from dwave.cloud.utils.cli import default_text_input, strtrunc, CLIError
from dwave.cloud.utils.dist import (
get_contrib_packages, get_distribution, PackageNotFoundError, VersionNotFoundError)
from dwave.cloud.utils.http import user_agent
from dwave.cloud.utils.logging import configure_logging
from dwave.cloud.utils.qubo import generate_random_ising_problem
from dwave.cloud.utils.time import datetime_to_timestamp, utcnow, epochnow
from dwave.cloud.coders import bqm_as_file
from dwave.cloud.package_info import __title__, __version__
from dwave.cloud.exceptions import (
Expand Down
3 changes: 2 additions & 1 deletion dwave/cloud/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
from dwave.cloud.events import dispatches_events
from dwave.cloud.utils.http import PretimedHTTPAdapter, BaseUrlSession, default_user_agent
from dwave.cloud.utils.time import datetime_to_timestamp, utcnow
from dwave.cloud.utils import cached, retried, is_caused_by
from dwave.cloud.utils.decorators import cached, retried
from dwave.cloud.utils.exception import is_caused_by

__all__ = ['Client']

Expand Down
6 changes: 3 additions & 3 deletions dwave/cloud/computation.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@
from dateutil.parser import parse
from operator import itemgetter

from dwave.cloud.utils.time import utcnow, datetime_to_timestamp
from dwave.cloud.utils.decorators import aliasdict, deprecated
from dwave.cloud.exceptions import InvalidAPIResponseError
from dwave.cloud.utils.decorators import aliasdict, deprecated
from dwave.cloud.utils.time import utcnow, datetime_to_timestamp

# Use numpy if available for fast decoding
try:
Expand Down Expand Up @@ -578,7 +578,7 @@ def result(self):
Instead of adding copies of ``solutions`` and ``num_occurrences``
keys (as ``samples`` and ``occurrences``), we alias them using
:class:`~dwave.cloud.utils.aliasdict`. Values are available under
:class:`~dwave.cloud.utils.decorators.aliasdict`. Values are available under
alias keys, but the keys themselves are not stored or visible.
Examples:
Expand Down
2 changes: 1 addition & 1 deletion dwave/cloud/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
DEFAULT_REGION, DEFAULT_SOLVER_API_ENDPOINT, DEFAULT_LEAP_API_ENDPOINT,
DEFAULT_METADATA_API_ENDPOINT)
from dwave.cloud.config.models import ClientConfig, validate_config_v1
from dwave.cloud.utils import cached
from dwave.cloud.utils.decorators import cached

__all__ = ['get_regions']

Expand Down
37 changes: 30 additions & 7 deletions dwave/cloud/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@
from dwave.cloud.coders import (
encode_problem_as_qp, encode_problem_as_ref, decode_binary_ref,
decode_qp_numpy, decode_qp, decode_bq, bqm_as_file)
from dwave.cloud.utils.coders import NumpyEncoder
from dwave.cloud.utils.qubo import reformat_qubo_as_ising
from dwave.cloud.computation import Future
from dwave.cloud.concurrency import Present
from dwave.cloud.events import dispatches_events
from dwave.cloud.utils.coders import NumpyEncoder
from dwave.cloud.utils.qubo import reformat_qubo_as_ising

# Use numpy if available for fast encoding/decoding
try:
import numpy as np
import numpy
_numpy = True
except ImportError:
_numpy = False
Expand Down Expand Up @@ -594,6 +594,30 @@ class DQMSolver(BaseUnstructuredSolver):
_handled_problem_types = {"dqm"}
_handled_encoding_formats = {"bq"}

@staticmethod
def _bqm_to_dqm(bqm):
"""Represent a :class:`dimod.BQM` as a :class:`dimod.DQM`."""
try:
from dimod import DiscreteQuadraticModel
except ImportError: # pragma: no cover
raise RuntimeError(
"dimod package with support for DiscreteQuadraticModel required."
"Re-install the library with 'dqm' support.")

dqm = DiscreteQuadraticModel()

ising = bqm.spin

for v, bias in ising.linear.items():
dqm.add_variable(2, label=v)
dqm.set_linear(v, [-bias, bias])

for (u, v), bias in ising.quadratic.items():
biases = numpy.array([[bias, -bias], [-bias, bias]], dtype=numpy.float64)
dqm.set_quadratic(u, v, biases)

return dqm

def _encode_problem_for_upload(self, dqm):
try:
data = dqm.to_file()
Expand All @@ -608,10 +632,9 @@ def _encode_problem_for_upload(self, dqm):

def sample_bqm(self, bqm, label=None, **params):
"""Use for testing."""
from dwave.cloud.utils import bqm_to_dqm

# to sample BQM problems, we need to convert them to DQM
dqm = bqm_to_dqm(bqm)
dqm = self.bqm_to_dqm(bqm)

# TODO: convert sampleset back
return self.sample_dqm(dqm, label=label, **params)
Expand Down Expand Up @@ -1503,9 +1526,9 @@ def estimate_qpu_access_time(self,
q = readout_time_model_parameters[:n//2]
t = readout_time_model_parameters[n//2:]
if readout_time_model == 'pwl_log_log':
readout_time = pow(10, np.interp(np.emath.log10(num_qubits), q, t))
readout_time = pow(10, numpy.interp(numpy.emath.log10(num_qubits), q, t))
elif readout_time_model == 'pwl_linear':
readout_time = np.interp(num_qubits, q, t)
readout_time = numpy.interp(num_qubits, q, t)
else:
raise ValueError("``estimate_qpu_access_time`` does not support "
f"``readout_time_model`` value {readout_time_model} "
Expand Down
4 changes: 2 additions & 2 deletions dwave/cloud/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
"""Utilities for private and Ocean-internal use."""

# imports for backward-compat
from dwave.cloud.utils.logging import set_loglevel
from dwave.cloud.utils.logging import set_loglevel, configure_logging

from dwave.cloud.utils.qubo import (
uniform_get, reformat_qubo_as_ising, active_qubits,
)

from dwave.cloud.utils.time import utcnow

from dwave.cloud.utils.decorators import retried
from dwave.cloud.utils.decorators import cached, retried
5 changes: 4 additions & 1 deletion dwave/cloud/utils/coders.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

import json

import numpy
try:
import numpy
except ImportError: # pragma: no cover
pass # numpy not required for all cloud-client functionality

__all__ = ['NumpyEncoder', 'coerce_numpy_to_python']

Expand Down
2 changes: 1 addition & 1 deletion dwave/cloud/utils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class aliasdict(dict):
Example:
>>> from operator import itemgetter
>>> from dwave.cloud.utils import aliasdict
>>> from dwave.cloud.utils.decorators import aliasdict
>>> d = aliasdict(a=1, b=2)
>>> d.alias(c=itemgetter('a'))
Expand Down
7 changes: 3 additions & 4 deletions dwave/cloud/utils/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@
These functions previously lived under ``dwave.cloud.utils``.
"""

from collections import OrderedDict
from importlib.metadata import (
entry_points, EntryPoint, Distribution, PackageNotFoundError)
from typing import Dict, List, Union
from typing import Dict, List, OrderedDict, Union

from packaging.requirements import Requirement

Expand All @@ -38,7 +37,7 @@ def get_contrib_config() -> List[EntryPoint]:
return contrib


def get_contrib_packages() -> Dict[str, Distribution]:
def get_contrib_packages() -> OrderedDict[str, dict]:
"""Combine all contrib packages in an ordered dict. Assumes package names
are unique.
"""
Expand Down Expand Up @@ -76,7 +75,7 @@ def get_distribution(requirement: Union[str, Requirement],
Raises:
:class:`~importlib.metadata.PackageNotFoundError`:
Package by name not found.
:class:`~dwave.cloud.utils.VersionNotFoundError`:
:class:`~dwave.cloud.utils.dist.VersionNotFoundError`:
Version of the package found (distribution) does not match the
requirement.
"""
Expand Down
52 changes: 13 additions & 39 deletions dwave/cloud/utils.py → dwave/cloud/utils/exception.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2017 D-Wave Systems Inc.
# Copyright 2024 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.
Expand All @@ -12,23 +12,26 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import inspect
"""Exception handling utilities for private and Ocean-internal use.
.. versionchanged:: 0.12.0
These functions previously lived under ``dwave.cloud.utils``.
"""

__all__ = ['hasinstance', 'exception_chain', 'is_caused_by']

from typing import Any, Iterable, Tuple, Union

__all__ = ['hasinstance', 'exception_chain', 'is_caused_by']


def hasinstance(iterable, class_or_tuple):
def hasinstance(objs: Iterable[Any], class_or_tuple: Union[Any, Tuple[Any]]):
"""Extension of ``isinstance`` to iterables/sequences. Returns True iff the
sequence contains at least one object which is instance of ``class_or_tuple``.
"""

return any(isinstance(e, class_or_tuple) for e in iterable)
return any(isinstance(e, class_or_tuple) for e in objs)


def exception_chain(exception):
def exception_chain(exception: Exception):
"""Traverse the chain of embedded exceptions, yielding one at the time.
Args:
Expand Down Expand Up @@ -67,7 +70,9 @@ def f():
return


def is_caused_by(exception, exception_types):
def is_caused_by(exception: Exception,
exception_types: Union[Exception, Tuple[Exception]]
) -> bool:
"""Check if any of ``exception_types`` is causing the ``exception``.
Equivalently, check if any of ``exception_types`` is contained in the
exception chain rooted at ``exception``.
Expand All @@ -86,34 +91,3 @@ def is_caused_by(exception, exception_types):
"""

return hasinstance(exception_chain(exception), exception_types)



def pretty_argvalues():
"""Pretty-formatted function call arguments, from the caller's frame."""
return inspect.formatargvalues(*inspect.getargvalues(inspect.currentframe().f_back))



def bqm_to_dqm(bqm):
"""Represent a :class:`dimod.BQM` as a :class:`dimod.DQM`."""
try:
from dimod import DiscreteQuadraticModel
except ImportError: # pragma: no cover
raise RuntimeError(
"dimod package with support for DiscreteQuadraticModel required."
"Re-install the library with 'dqm' support.")

dqm = DiscreteQuadraticModel()

ising = bqm.spin

for v, bias in ising.linear.items():
dqm.add_variable(2, label=v)
dqm.set_linear(v, [-bias, bias])

for (u, v), bias in ising.quadratic.items():
biases = numpy.array([[bias, -bias], [-bias, bias]], dtype=numpy.float64)
dqm.set_quadratic(u, v, biases)

return dqm
6 changes: 6 additions & 0 deletions dwave/cloud/utils/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""Logging utilities for private and Ocean-internal use."""

import datetime
import inspect
import io
import json
import logging
Expand Down Expand Up @@ -208,3 +209,8 @@ def configure_logging_from_env(logger: logging.Logger) -> bool:
structured_output=(log_format.strip().lower() == 'json'))

return True


def pretty_argvalues():
"""Pretty-formatted function call arguments, from the caller's frame."""
return inspect.formatargvalues(*inspect.getargvalues(inspect.currentframe().f_back))
17 changes: 17 additions & 0 deletions releasenotes/notes/refactor-utils-560b18c32f12feee.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
upgrade:
- |
``dwave.cloud.utils`` module has been split into the following submodules:
``dwave.cloud.utils.cli``, ``dwave.cloud.utils.coders``,
``dwave.cloud.utils.decorators``, ``dwave.cloud.utils.dist``,
``dwave.cloud.utils.exception``, ``dwave.cloud.utils.http``,
``dwave.cloud.utils.logging``, ``dwave.cloud.utils.qubo``, and
``dwave.cloud.utils.time``.
First reason for doing this was code clean-up (including adding type annotations),
but a more important reason is that this will enable import time optimization
in the future. And that's why we break backwards compatibility by not importing
everything back into the top-level utils namespace.
The utils module is considered Ocean-internal as is, so this move shouldn't
affect user's code.
3 changes: 2 additions & 1 deletion tests/api/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from typing import Any, Optional, Tuple, Union

from dwave.cloud.coders import encode_problem_as_qp, encode_problem_as_ref
from dwave.cloud.utils import utcrel, generate_const_ising_problem
from dwave.cloud.utils.qubo import generate_const_ising_problem
from dwave.cloud.utils.time import utcrel
from dwave.cloud.solver import StructuredSolver, UnstructuredSolver
from dwave.cloud.testing.mocks import qpu_clique_solver_data, hybrid_bqm_solver_data

Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def test_default_flows(self, name, extra_opts, inputs):
runner = CliRunner(mix_stderr=False)
with runner.isolated_filesystem():
with mock.patch("dwave.cloud.config.loaders.get_configfile_paths", lambda: ['dwave.conf']):
with mock.patch("dwave.cloud.utils.input", side_effect=inputs):
with mock.patch("dwave.cloud.utils.cli.input", side_effect=inputs):
result = runner.invoke(cli, [
'config', 'create'
] + extra_opts, input='\n'.join('' if v is None else v for v in inputs.values()))
Expand Down
8 changes: 4 additions & 4 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
import json
import time
import unittest
from unittest import mock
from contextlib import contextmanager
from collections import defaultdict
from contextlib import contextmanager
from unittest import mock

import requests
from parameterized import parameterized
Expand All @@ -32,11 +32,11 @@
from dwave.cloud.api import constants, models, Regions
from dwave.cloud.client import Client
from dwave.cloud.config.models import dump_config_v1
from dwave.cloud.solver import StructuredSolver, UnstructuredSolver
from dwave.cloud.utils import cached
from dwave.cloud.exceptions import (
SolverAuthenticationError, SolverError, SolverNotFoundError)
from dwave.cloud.solver import StructuredSolver, UnstructuredSolver
from dwave.cloud.testing import iterable_mock_open, isolated_environ
from dwave.cloud.utils.decorators import cached
import dwave.cloud

from tests import config
Expand Down
2 changes: 1 addition & 1 deletion tests/test_coders.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
encode_problem_as_qp, decode_qp, decode_qp_numpy,
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
from dwave.cloud.utils.qubo import generate_const_ising_problem

# parse string version as tuple
if dimod:
Expand Down
Loading

0 comments on commit cfec653

Please sign in to comment.