Enable registration of add-on providers (#602)

* Allow wrapper.register to handle addon providers.

This change should be backward compatible.

* pass test

* use new provider_name in provider registration

* remove provider names. return provider handle when registering. default url in IBMQProvider

* avoid double registration by implementing __eq__

* fix wrapper tests

* fix wrapper tests

* Capture all possible exceptions from providers at registering
time.

* Fix test_offline

* Add changelog, linter
This commit is contained in:
ewinston 2018-07-01 14:44:26 -04:00 committed by Diego M. Rodríguez
parent fc779c6094
commit 60032dd056
10 changed files with 126 additions and 123 deletions

View File

@ -74,7 +74,7 @@ disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax
# disable the "too-many/few-..." refactoring hints
too-many-lines, too-many-branches, too-many-locals, too-many-nested-blocks,
too-many-statements, too-many-instance-attributes, too-many-arguments,
too-many-public-methods, too-few-public-methods
too-many-public-methods, too-few-public-methods, too-many-ancestors

View File

@ -20,7 +20,7 @@ The format is based on `Keep a Changelog`_.
Added
-----
- Retreive IBM Q jobs from server (#563, #585).
- Retrieve IBM Q jobs from server (#563, #585).
- Add German introductory documentation (``doc/de``) (#592).
- Add ``unregister()`` for removing previously registered providers (#584).
- Add matplotlib-based circuit drawer (#579).
@ -31,7 +31,8 @@ Changed
- Remove backend filtering in individual providers, keep only in wrapper (#575).
- Single source of version information (#581)
- Bumped IBMQuantumExperience dependency to 1.9.6 (#600).
- For backend status, `status['available']` is now `status['operational']`.
- For backend status, `status['available']` is now `status['operational']` (#609).
- Added support for registering third-party providers in `register()` (#602).
Removed
-------

View File

@ -659,14 +659,14 @@ class QuantumProgram(object):
warnings.warn(
"set_api() will be deprecated in upcoming versions (>0.5.0). "
"Using qiskit.register() instead is recommended.", DeprecationWarning)
qiskit.wrapper.register(token, url,
hub, group, project, proxies, verify,
provider_name='ibmq')
qiskit.wrapper.register(token, url=url,
hub=hub, group=group, project=project,
proxies=proxies, verify=verify)
# TODO: the setting of self._api and self.__api_config is left for
# backwards-compatibility.
# pylint: disable=no-member
self.__api = qiskit.wrapper._wrapper._DEFAULT_PROVIDER.providers['ibmq']._api
self.__api = qiskit.wrapper._wrapper._DEFAULT_PROVIDER.providers[-1]._api
config_dict = {
'url': url,
}

View File

@ -68,3 +68,13 @@ class BaseProvider(ABC):
dict[str: str]: {deprecated_name: backend_name}
"""
return {}
def __eq__(self, other):
"""
Assumes two providers with the same class name clash.
Derived providers can override this behavior
(e.g. IBMQProvider instances are equal if and only if
they have the same authentication attributes as well).
"""
equality = (type(self).__name__ == type(other).__name__)
return equality

View File

@ -15,7 +15,7 @@ from qiskit.backends.ibmq.ibmqbackend import IBMQBackend
class IBMQProvider(BaseProvider):
"""Provider for remote IbmQ backends."""
def __init__(self, token, url,
def __init__(self, token, url='https://quantumexperience.ng.bluemix.net/api',
hub=None, group=None, project=None, proxies=None, verify=True):
super().__init__()
@ -26,6 +26,15 @@ class IBMQProvider(BaseProvider):
# Populate the list of remote backends.
self.backends = self._discover_remote_backends()
# authentication attributes, which uniquely identify the provider instance
self._token = token
self._url = url
self._hub = hub
self._group = group
self._project = project
self._proxies = proxies
self._verify = verify
def get_backend(self, name):
return self.backends[name]
@ -124,3 +133,12 @@ class IBMQProvider(BaseProvider):
ret[config['name']] = IBMQBackend(configuration=config, api=self._api)
return ret
def __eq__(self, other):
try:
equality = (self._token == other._token and self._url == other._url and
self._hub == other._hub and self._group == other._group and
self._project == other._project)
except AttributeError:
equality = False
return equality

View File

@ -55,7 +55,8 @@ class LocalProvider(BaseProvider):
def aliased_backend_names(self):
return {
'local_qasm_simulator': ['local_qasm_simulator_cpp',
'local_qasm_simulator_py'],
'local_qasm_simulator_py',
'local_clifford_simulator_cpp'],
'local_statevector_simulator': ['local_statevector_simulator_cpp',
'local_statevector_simulator_py'],
'local_unitary_simulator': ['local_unitary_simulator_cpp',

View File

@ -9,7 +9,7 @@
import warnings
from qiskit import transpiler, QISKitError
from qiskit.backends.ibmq import IBMQProvider
from qiskit.wrapper.defaultqiskitprovider import DefaultQISKitProvider
from ._circuittoolkit import circuit_from_qasm_file, circuit_from_qasm_string
@ -19,13 +19,16 @@ from ._circuittoolkit import circuit_from_qasm_file, circuit_from_qasm_string
_DEFAULT_PROVIDER = DefaultQISKitProvider()
def register(token, url='https://quantumexperience.ng.bluemix.net/api',
hub=None, group=None, project=None, proxies=None, verify=True,
provider_name=None):
def register(*args, provider_class=IBMQProvider, **kwargs):
"""
Authenticate against an online backend provider.
This is a factory method that returns the provider that gets registered.
Args:
args (tuple): positional arguments passed to provider class initialization
provider_class (BaseProvider): provider class
kwargs (dict): keyword arguments passed to provider class initialization.
For the IBMQProvider default this can include things such as;
token (str): The token used to register on the online backend such
as the quantum experience.
url (str): The url used for online backend such as the quantum
@ -36,34 +39,43 @@ def register(token, url='https://quantumexperience.ng.bluemix.net/api',
proxies (dict): Proxy configuration for the API, as a dict with
'urls' and credential keys.
verify (bool): If False, ignores SSL certificates errors.
provider_name (str): the user-provided name for the registered
provider.
Returns:
BaseProvider: the provider instance that was just registered.
Raises:
QISKitError: if the provider name is not recognized.
QISKitError: if the provider could not be registered
(e.g. due to conflict)
"""
# Convert the credentials to a dict.
credentials = {
'token': token, 'url': url, 'hub': hub, 'group': group,
'project': project, 'proxies': proxies, 'verify': verify
}
_DEFAULT_PROVIDER.add_ibmq_provider(credentials, provider_name)
try:
provider = provider_class(*args, **kwargs)
except Exception as ex:
raise QISKitError("Couldn't instance provider!. Error: {0}".format(ex))
_DEFAULT_PROVIDER.add_provider(provider)
return provider
def unregister(provider_name):
def unregister(provider):
"""
Removes a provider of list of registered providers.
Removes a provider from list of registered providers.
Note:
If backend names from provider1 and provider2 were clashing,
`unregister(provider1)` removes the clash and makes the backends
from provider2 available.
Args:
provider_name (str): The unique name for the online provider.
provider (BaseProvider): the provider instance to unregister
Raises:
QISKitError: if the provider name is not valid.
QISKitError: if the provider instance is not registered
"""
_DEFAULT_PROVIDER.remove_provider(provider_name)
_DEFAULT_PROVIDER.remove_provider(provider)
def registered_providers():
"""Return the names of the currently registered providers."""
return list(_DEFAULT_PROVIDER.providers.keys())
"""Return the currently registered providers."""
return list(_DEFAULT_PROVIDER.providers)
# Functions for inspecting and retrieving backends.
@ -78,6 +90,18 @@ def available_backends(filters=None, compact=True):
In order for this function to return online backends, a connection with
an online backend provider needs to be established by calling the
`register()` function.
Note:
If two or more providers have backends with the same name, those names
will be shown only once. To disambiguate and choose a backend from a
specific provider, get the backend from that specific provider.
Example:
p1 = register(token1)
p2 = register(token2)
execute(circuit, p1.get_backend('ibmq_5_tenerife'))
execute(circuit, p2.get_backend('ibmq_5_tenerife'))
Args:
filters (dict or callable): filtering conditions.
compact (bool): group backend names based on compact group names.

View File

@ -8,12 +8,10 @@
"""Meta-provider that aggregates several providers."""
import logging
from collections import OrderedDict
from itertools import combinations
from qiskit import QISKitError
from qiskit.backends.baseprovider import BaseProvider
from qiskit.backends.ibmq import IBMQProvider
from qiskit.backends.local.localprovider import LocalProvider
logger = logging.getLogger(__name__)
@ -26,12 +24,12 @@ class DefaultQISKitProvider(BaseProvider):
def __init__(self):
super().__init__()
# Dict of providers.
self.providers = OrderedDict({'local': LocalProvider()})
# List of providers.
self.providers = [LocalProvider()]
def get_backend(self, name):
name = self.resolve_backend_name(name)
for provider in self.providers.values():
for provider in self.providers:
try:
return provider.get_backend(name)
except KeyError:
@ -41,6 +39,10 @@ class DefaultQISKitProvider(BaseProvider):
def available_backends(self, filters=None):
"""Get a list of available backends from all providers (after filtering).
Note:
If two or more providers share similar backend names, only the backends
belonging to the 1st registered provider will be returned.
Args:
filters (dict or callable): filtering conditions.
each will either pass through, or be filtered out.
@ -59,7 +61,7 @@ class DefaultQISKitProvider(BaseProvider):
"""
# pylint: disable=arguments-differ
backends = []
for provider in self.providers.values():
for provider in self.providers:
backends.extend(provider.available_backends())
if filters is not None:
@ -97,7 +99,7 @@ class DefaultQISKitProvider(BaseProvider):
ValueError: if a backend is mapped to multiple aliases
"""
aliases = {}
for provider in self.providers.values():
for provider in self.providers:
aliases = {**aliases, **provider.aliased_backend_names()}
for pair in combinations(aliases.values(), r=2):
if not set.isdisjoint(set(pair[0]), set(pair[1])):
@ -113,94 +115,57 @@ class DefaultQISKitProvider(BaseProvider):
dict[str: list[str]]: aggregated alias dictionary
"""
deprecates = {}
for provider in self.providers.values():
for provider in self.providers:
deprecates = {**deprecates, **provider.deprecated_backend_names()}
return deprecates
def add_provider(self, provider, provider_name):
def add_provider(self, provider):
"""
Add a new provider to the list of known providers.
Note:
If some backend in the new provider has a name in use by an
already registered provider, the backend will not be available,
and the name of the backend will still refer to that previously
registered.
Args:
provider (BaseProvider): Provider instance.
provider_name (str): User-provided name for the provider.
Returns:
BaseProvider: the provider instance.
Raises:
QISKitError: if a provider with the same name is already in the
list.
QISKitError: if trying to add a provider identical to one already registered
"""
if provider_name in self.providers.keys():
raise QISKitError(
'A provider with name "%s" is already registered.'
% provider_name)
# Check for backend name clashes, emitting a warning.
current_backends = {str(backend) for backend in self.available_backends()}
added_backends = {str(backend) for backend in provider.available_backends()}
common_backends = added_backends.intersection(current_backends)
if common_backends:
logger.warning(
'The backend names "%s" (provided by "%s") are already in use. '
'Consider using unregister() for avoiding name conflicts.',
list(common_backends), provider_name)
'The backend names "%s" of this provider are already in use. '
'Refer to documentation for `available_backends()` and `unregister()`.',
list(common_backends))
self.providers[provider_name] = provider
# checks for equality of provider instances, based on the __eq__ method
if provider not in self.providers:
self.providers.append(provider)
else:
raise QISKitError("The same provider has already been registered!")
return provider
def add_ibmq_provider(self, credentials_dict, provider_name=None):
"""
Add a new IBMQProvider to the list of known providers.
Args:
credentials_dict (dict): dictionary of credentials for a provider.
provider_name (str): User-provided name for the provider. A name
will automatically be assigned if possible.
Raises:
QISKitError: if a provider with the same name is already in the
list; or if a provider name could not be assigned.
Returns:
IBMQProvider: the new IBMQProvider instance.
"""
# Automatically assign a name if not specified.
if not provider_name:
if 'quantumexperience' in credentials_dict['url']:
provider_name = 'ibmq'
elif 'q-console' in credentials_dict['url']:
provider_name = 'qnet'
else:
raise QISKitError(
'Cannot parse provider name from credentials.')
ibmq_provider = IBMQProvider(**credentials_dict)
return self.add_provider(ibmq_provider, provider_name)
def remove_provider(self, provider_name):
def remove_provider(self, provider):
"""
Remove a provider from the list of known providers.
Args:
provider_name (str): name of the provider to be removed.
provider (BaseProvider): provider to be removed.
Raises:
QISKitError: if the provider name is not valid.
QISKitError: if the provider is not registered.
"""
if provider_name == 'local':
if isinstance(provider, LocalProvider):
raise QISKitError("Cannot unregister 'local' provider.")
try:
self.providers.pop(provider_name)
except KeyError:
self.providers.remove(provider)
except ValueError:
raise QISKitError("'%s' provider is not registered.")
def resolve_backend_name(self, name):

View File

@ -1449,7 +1449,7 @@ class TestQuantumProgram(QiskitTestCase):
''.join(random.choice(string.ascii_lowercase) for _ in range(63))
)
# SDK will throw ConnectionError on every call that implies a connection
self.assertRaises(ConnectionError, qp.set_api, FAKE_TOKEN, FAKE_URL)
self.assertRaises(QISKitError, qp.set_api, FAKE_TOKEN, FAKE_URL)
def test_results_save_load(self):
"""Test saving and loading the results of a circuit.
@ -1597,8 +1597,7 @@ class TestQuantumProgram(QiskitTestCase):
# TODO: use the backend directly when the deprecation is completed.
from ._mockutils import DummyProvider
import qiskit.wrapper
qiskit.wrapper._wrapper._DEFAULT_PROVIDER.add_provider(DummyProvider(),
'dummy')
qiskit.wrapper._wrapper._DEFAULT_PROVIDER.add_provider(DummyProvider())
q_program = QuantumProgram(specs=self.QPS_SPECS)
qr = q_program.get_quantum_register("q_name")

View File

@ -14,6 +14,7 @@ import unittest
import qiskit.wrapper
from qiskit.wrapper import registered_providers
from qiskit.backends.ibmq import IBMQProvider
from qiskit import QISKitError
from .common import QiskitTestCase, requires_qe_access
from .test_backends import remove_backends_from_list
@ -24,8 +25,7 @@ class TestWrapper(QiskitTestCase):
@requires_qe_access
def test_wrapper_register_ok(self, QE_TOKEN, QE_URL, hub, group, project):
"""Test wrapper.register()."""
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project,
provider_name='ibmq')
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project)
backends = qiskit.wrapper.available_backends()
backends = remove_backends_from_list(backends)
self.log.info(backends)
@ -34,8 +34,7 @@ class TestWrapper(QiskitTestCase):
@requires_qe_access
def test_backends_with_filter(self, QE_TOKEN, QE_URL, hub, group, project):
"""Test wrapper.available_backends(filter=...)."""
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project,
provider_name='ibmq')
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project)
backends = qiskit.wrapper.available_backends({'local': False,
'simulator': True})
self.log.info(backends)
@ -56,20 +55,8 @@ class TestWrapper(QiskitTestCase):
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project)
self.assertCountEqual(initial_providers, registered_providers())
@requires_qe_access
def test_register_twice_with_different_names(self, QE_TOKEN, QE_URL,
hub, group, project):
"""Test double registration of same credentials but different names."""
initial_providers = registered_providers()
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project,
provider_name='provider1')
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project,
provider_name='provider2')
self.assertCountEqual(initial_providers + ['provider1', 'provider2'],
registered_providers())
def test_register_unknown_name(self):
"""Test registering a provider with not explicit name."""
def test_register_bad_credentials(self):
"""Test registering a provider with bad credentials."""
initial_providers = registered_providers()
with self.assertRaises(QISKitError):
qiskit.wrapper.register('FAKE_TOKEN', 'http://unknown')
@ -79,38 +66,36 @@ class TestWrapper(QiskitTestCase):
def test_unregister(self, QE_TOKEN, QE_URL, hub, group, project):
"""Test unregistering."""
initial_providers = registered_providers()
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project,
provider_name='provider1')
self.assertCountEqual(initial_providers + ['provider1'],
ibmqprovider = qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project)
self.assertCountEqual(initial_providers + [ibmqprovider],
registered_providers())
qiskit.wrapper.unregister('provider1')
qiskit.wrapper.unregister(ibmqprovider)
self.assertEqual(initial_providers, registered_providers())
def test_unregister_non_existent(self):
@requires_qe_access
def test_unregister_non_existent(self, QE_TOKEN, QE_URL, hub, group, project):
"""Test unregistering a non existent provider."""
initial_providers = registered_providers()
ibmqprovider = IBMQProvider(QE_TOKEN, QE_URL, hub, group, project)
with self.assertRaises(QISKitError):
qiskit.wrapper.unregister('provider1')
qiskit.wrapper.unregister(ibmqprovider)
self.assertEqual(initial_providers, registered_providers())
@requires_qe_access
def test_register_backend_name_conflicts(self, QE_TOKEN, QE_URL,
hub, group, project):
"""Test backend name conflicts when registering."""
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project,
provider_name='provider1')
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project)
initial_providers = registered_providers()
initial_backends = qiskit.wrapper.available_backends()
ibmqx4_backend = qiskit.wrapper.get_backend('ibmqx4')
with self.assertLogs(level=logging.WARNING) as logs:
qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project,
provider_name='provider2')
ibmqprovider2 = qiskit.wrapper.register(QE_TOKEN, QE_URL, hub, group, project)
# Check that one, and only one warning has been issued.
self.assertEqual(len(logs.records), 1)
# Check that the provider has been registered.
self.assertCountEqual(initial_providers + ['provider2'],
self.assertCountEqual(initial_providers + [ibmqprovider2],
registered_providers())
# Check that no new backends have been added.
self.assertCountEqual(initial_backends,