Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

introduce abstract base class for backends #84

Merged
merged 15 commits into from
Oct 4, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import qiskit.extensions.standard
from ._jobprocessor import JobProcessor, QuantumJob
from ._quantumprogram import QuantumProgram
from ._result import Result
from ._quantumprogram import Result

__version__ = '0.4.0'

28 changes: 10 additions & 18 deletions qiskit/_jobprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@
import string
from qiskit._result import Result

from IBMQuantumExperience import IBMQuantumExperience
from IBMQuantumExperience import ApiError

# Stable Modules
import qiskit.backends as backends
from qiskit import QISKitError
# Local Simulator Modules
from qiskit import simulators
# compiler module
from qiskit import _openquantumcompiler as openquantumcompiler
from IBMQuantumExperience.IBMQuantumExperience import (IBMQuantumExperience,
ApiError)

def run_local_simulator(qobj):
def run_local_backend(qobj):
"""Run a program of compiled quantum circuits on the local machine.

Args:
Expand All @@ -42,9 +38,9 @@ def run_local_simulator(qobj):
compiled_circuit = openquantumcompiler.compile(circuit['circuit'],
format='json')
circuit['compiled_circuit'] = compiled_circuit
local_simulator = simulators.LocalSimulator(qobj)
local_simulator.run()
return local_simulator.result()
BackendClass = backends.get_backend_class(qobj['config']['backend'])
backend = BackendClass(qobj)
return backend.run()

def run_remote_backend(qobj, api, wait=5, timeout=60, silent=True):
"""
Expand Down Expand Up @@ -126,10 +122,6 @@ def _wait_for_job(jobid, api, wait=5, timeout=60, silent=True):
'status': job_result['qasms'][index]['status']})
return {'status': job_result['status'], 'result': job_result_return}

def local_backends():
"""Get the local backends."""
return simulators._localsimulator.local_backends()

def remote_backends(api):
"""Get the remote backends.

Expand Down Expand Up @@ -207,7 +199,7 @@ def __init__(self, circuits, backend='local_qasm_simulator',
self.names = names
else:
self.names = [names]
self._local_backends = local_backends()
self._local_backends = backends.local_backends()
self.timeout = timeout
# check whether circuits have already been compiled
# and formatted for backend.
Expand Down Expand Up @@ -288,7 +280,7 @@ def __init__(self, q_jobs, callback, max_workers=1, token=None, url=None, api=No
self.q_jobs = q_jobs
self.max_workers = max_workers
# check whether any jobs are remote
self._local_backends = local_backends()
self._local_backends = backends.local_backends()
self.online = any(qj.backend not in self._local_backends for qj in q_jobs)
self.futures = {}
self.lock = Lock()
Expand Down Expand Up @@ -350,7 +342,7 @@ def submit(self, silent=True):
executor = self.executor_class(max_workers=self.max_workers)
for q_job in self.q_jobs:
if q_job.backend in self._local_backends:
future = executor.submit(run_local_simulator,
future = executor.submit(run_local_backend,
q_job.qobj)
elif self.online and q_job.backend in self._online_backends:
future = executor.submit(run_remote_backend,
Expand Down
1 change: 0 additions & 1 deletion qiskit/_openquantumcompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import qiskit.mapper as mapper
from qiskit._qiskiterror import QISKitError


def compile(qasm_circuit, basis_gates='u1,u2,u3,cx,id', coupling_map=None,
initial_layout=None, silent=True, get_layout=False, format='dag'):
"""Compile the circuit.
Expand Down
15 changes: 4 additions & 11 deletions qiskit/_quantumprogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from . import mapper

# Local Simulator Modules
from . import simulators
import qiskit.backends
import qiskit.extensions.standard

from qiskit import _openquantumcompiler as openquantumcompiler
Expand Down Expand Up @@ -104,7 +104,7 @@ def __init__(self, specs=None):
self.__init_circuit = None # stores the intial quantum circuit of the
# program
self.__ONLINE_BACKENDS = []
self.__LOCAL_BACKENDS = self.local_backends()
self.__LOCAL_BACKENDS = qiskit.backends.local_backends()
self.mapper = mapper
if specs:
self.__init_specs(specs)
Expand Down Expand Up @@ -547,10 +547,6 @@ def available_backends(self):
"""All the backends that are seen by QISKIT."""
return self.__ONLINE_BACKENDS + self.__LOCAL_BACKENDS

def local_backends(self):
"""Get the local backends."""
return simulators._localsimulator.local_backends()

def online_backends(self):
"""Get the online backends.

Expand Down Expand Up @@ -669,11 +665,8 @@ def get_backend_configuration(self, backend, list_format=False):
cmap = configuration[key]
configuration_edit[new_key] = cmap
return configuration_edit
for configuration in simulators.local_configuration:
if configuration['name'] == backend:
return configuration
raise LookupError(
'backend configuration for "{0}" not found'.format(backend))
else:
return qiskit.backends.get_backend_configuration(backend)

def get_backend_calibration(self, backend):
"""Return the online backend calibrations.
Expand Down
8 changes: 8 additions & 0 deletions qiskit/_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ def circuit_statuses(self):
return [circuit_result['status']
for circuit_result in self.__result['result']]

def get_circuit_status(self, icircuit):
"""Return the status of circuit at index icircuit.

Args:
icircuit (int): index of circuit
"""
return self.__result['result'][icircuit]['status']

def get_ran_qasm(self, name):
"""Get the ran qasm for the named circuit and backend.

Expand Down
8 changes: 8 additions & 0 deletions qiskit/backends/__init__.py
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)
138 changes: 138 additions & 0 deletions qiskit/backends/_backendutils.py
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

40 changes: 40 additions & 0 deletions qiskit/backends/_basebackend.py
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
Copy link
Member

@diego-plan9 diego-plan9 Sep 27, 2017

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 example name (so they are available as MySimulator.name) to further enforce them (I'm picking name as an example as it is the identifier that is later used to refer to the simulator by the user).

Copy link
Contributor Author

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.


@abstractmethod
def run(self):
Copy link
Member

Choose a reason for hiding this comment

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

With the recent changes, would it be a good idea to define run_circuit(...) as an @abstractmethod, this enforcing the simulators to provide and implement it? Currently, it seems it is never called directly outside each module (ie. it is always called from FooSimulator.run()), but I'm not sure if the goal is to eventually use run_circuit() directly elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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 run_circuit() was meant to be, and keeping it just on the simulator that needs it sounds fine to me!

pass

@property
@abstractmethod
def configuration(self):
"""Return backend configuration"""
pass

@configuration.setter
@abstractmethod
def configuration(self, configuration):
"""Set backend configuration"""
Copy link
Member

Choose a reason for hiding this comment

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

Is it likely that any local backend will do something different than return self._configuration and self._configuration = configuration on these two functions?

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 BaseBackend directly (and not making them @abstractmethods) to avoid duplication - if in the end a future simulator needs to do something different it can just override them anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Loading