-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
introduce abstract base class for backends #84
Changes from 6 commits
c8ff985
f5b036a
e9d16b2
459aa90
24e3cb0
0f2cbf6
5e91536
7759b29
8344f6b
04efb6a
8a645a2
573e1d9
29f7f6c
8cade88
1dfc7de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from ._backendutils import (update_implemented_backends, | ||
_backend_classes, | ||
find_runnable_backends, | ||
get_backend_class, | ||
get_backend_configuration, | ||
local_backends) | ||
_backend_classes = update_implemented_backends() | ||
runnable_backends = find_runnable_backends(_backend_classes) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import os | ||
import pkgutil | ||
import importlib | ||
import inspect | ||
import sys | ||
from qiskit.backends._basebackend import BaseBackend | ||
|
||
# This dict holds '<backend name>': <backend class object> records and | ||
# is imported to package scope. | ||
_backend_classes = {} | ||
_backend_configurations = {} | ||
|
||
def update_implemented_backends(): | ||
"""This function attempts to discover all backend modules. | ||
|
||
Backend modules should subclass BaseBackend. Alternatively they need | ||
to define a module level __configuration dictionary and a class which | ||
implements a run() method. | ||
|
||
Returns: | ||
dict of '<backend name>': <backend class object> | ||
""" | ||
for mod_info, name, ispkg in pkgutil.iter_modules([os.path.dirname(__file__)]): | ||
if name not in __name__: # skip this module | ||
fullname = os.path.splitext(__name__)[0] + '.' + name | ||
modspec = importlib.util.find_spec(fullname) | ||
mod = importlib.util.module_from_spec(modspec) | ||
modspec.loader.exec_module(mod) | ||
if hasattr(mod, '__configuration'): | ||
_backend_configurations[mod.__configuration['name']] = mod.__configuration | ||
for class_name, class_obj in inspect.getmembers(mod, | ||
inspect.isclass): | ||
if hasattr(class_obj, 'run'): | ||
class_obj = getattr(mod, class_name) | ||
_backend_classes[mod.__configuration['name']] = class_obj | ||
importlib.import_module(fullname) | ||
else: | ||
for class_name, class_obj in inspect.getmembers( | ||
mod, inspect.isclass): | ||
if issubclass(class_obj, BaseBackend): | ||
try: | ||
instance = class_obj({}) | ||
except: | ||
instance = None | ||
if isinstance(instance, BaseBackend): | ||
backend_name = instance.configuration['name'] | ||
_backend_classes[backend_name] = class_obj | ||
_backend_configurations[backend_name] = instance.configuration | ||
importlib.import_module(fullname) | ||
return _backend_classes | ||
|
||
def find_runnable_backends(backend_classes): | ||
backend_list = [] | ||
circuit = {'header': {'clbit_labels': [['cr', 1]], | ||
'number_of_clbits': 1, | ||
'number_of_qubits': 1, | ||
'qubit_labels': [['qr', 0]]}, | ||
'operations': | ||
[{'name': 'h', | ||
'params': [], | ||
'qubits': [0]}, | ||
{'clbits': [0], | ||
'name': 'measure', | ||
'qubits': [0]}]} | ||
qobj = {'id': 'backend_discovery', | ||
'config': { | ||
'max_credits': 3, | ||
'shots': 1, | ||
'backend': None, | ||
}, | ||
'circuits': [{'compiled_circuit': circuit}] | ||
} | ||
for backend_id, backend in _backend_classes.items(): | ||
try: | ||
backend(qobj) | ||
except FileNotFoundError as fnferr: | ||
# this is for discovery so just don't add to discovered list | ||
pass | ||
else: | ||
backend_list.append(backend_id) | ||
return backend_list | ||
|
||
def get_backend_class(backend_name): | ||
"""Return the class object for the named backend. | ||
|
||
Args: | ||
backend_name (str): the backend name | ||
|
||
Returns: | ||
class object for backend_name | ||
|
||
Raises: | ||
LookupError if backend is unavailable | ||
""" | ||
if backend_name in _backend_classes: | ||
return _backend_classes[backend_name] | ||
else: | ||
raise LookupError('backend "{}" is not available'.format(backend_name)) | ||
|
||
def get_backend_configuration(backend_name): | ||
"""Return the configuration for the named backend. | ||
|
||
Args: | ||
backend_name (str): the backend name | ||
|
||
Returns: | ||
configuration dict | ||
|
||
Raises: | ||
LookupError if backend is unavailable | ||
""" | ||
if backend_name in _backend_configurations: | ||
return _backend_configurations[backend_name] | ||
else: | ||
raise LookupError('backend "{}" is not available'.format(backend_name)) | ||
|
||
def local_backends(): | ||
"""Get the local backends.""" | ||
local_backends = [] | ||
for backend in _backend_configurations: | ||
configuration = get_backend_configuration(backend) | ||
# can drop this check once qobj works for remote | ||
if 'local' in configuration: | ||
if configuration['local'] == True: | ||
local_backends.append(backend) | ||
return local_backends | ||
|
||
def remote_backends(): | ||
"""Get the remote backends.""" | ||
remote_backends = [] | ||
for backend in _backend_configurations: | ||
configuration = get_backend_configuration(backend) | ||
# can drop this check once qobj works for remote | ||
if 'local' in configuration: | ||
if configuration['local'] == False: | ||
remote_backends.append(backend) | ||
return remote_backends | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
"""This module implements the abstract base class for backend modules. | ||
|
||
To create add-on backend modules subclass the Backend class in this module. | ||
Doing so requires that the required backend interface is implemented. | ||
""" | ||
from abc import ABC, abstractmethod | ||
|
||
class BaseBackend(ABC): | ||
|
||
@abstractmethod | ||
def __init__(self, qobj): | ||
"""initialize | ||
|
||
This method should initialize the module and raise a FileNotFoundError | ||
exception if a component of the moduel is not available. | ||
|
||
Args: | ||
qobj (dict): qobj dictionary | ||
|
||
Raises: | ||
FileNotFoundError if backend executable is not available. | ||
""" | ||
self._qobj = qobj | ||
self._configuration = None # IMPLEMENT for your backend | ||
|
||
@abstractmethod | ||
def run(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the recent changes, would it be a good idea to define There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we have changed to using qobj for communicating tasks the simulator, and perhaps the online backends, don't need to run circuits individually. For instance a c simulator can take in the whole qobj. The run_circuit() method is sort of a non-essential fix to get the python simulators to work with qobj without making too many changes. It seems like a good method to have for python simulators but some simulators might want to do optimizations on batches of circuits which don't need running individual ones. At least this way we make it optional. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It sounds sensible, thanks for the clarification - I was mostly not sure how "universal" the |
||
pass | ||
|
||
@property | ||
@abstractmethod | ||
def configuration(self): | ||
"""Return backend configuration""" | ||
pass | ||
|
||
@configuration.setter | ||
@abstractmethod | ||
def configuration(self, configuration): | ||
"""Set backend configuration""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it likely that any local backend will do something different than All of the existing simulators seem to be have exactly the same implementation for these two methods, and it might be worth just moving the implementation for these two functions to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did this in case a user wanted to do some kind of module specific validation of configuration but since I don't currently have a clear picture of this perhaps it would be ok to implement it in BaseBackend. |
||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also at least document the format of the
configuration
somewhere on the docstrings of this file, to make as clear as possible for users interested in subclassing it? Going a bit further, we might want to make some configuration values class methods, for examplename
(so they are available asMySimulator.name
) to further enforce them (I'm pickingname
as an example as it is the identifier that is later used to refer to the simulator by the user).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the nice thing about the discovery is that the backend developer does not have to put
import
statements in any qiskit file for the backend to be used. They only need to know the name of the backend they put into the configuration of their own backend module. Even for the number of backends we have this keeps the core code cleaner.I think you could be right about the user putting the module in the backends folder if they've installed with pip. Perhaps if they are developing backends they are more likely to use a git repo and would be comfortable with putting modules there. Still, maybe it's strange to have "local" modules mixed with qiskit ones. Perhaps we can have a ".qiskit/backends" folder in their home directory, or other locations, which are also checked.
Another nice thing about the discovery mechanics is that if, for instance wants to make several versions of a compiled simulator (e.g. different optimizations) it would be easier to implement.
We could also use the discovery function to "discover" online backends and make them subclass BaseBackend. This would make us less dependent on changes to the set of available online backends.
I'm a little unclear about what niceties we are giving up. In the previous discovery I intentionally hid backends and just made LocalSimulator the only one that was visible. With this PR I import all the backend modules into the namespace as you suggested so you should see no difference from doing a normal import (that was the goal anyway). For instance documentation and tab completion seems to be available now.