Isolate legacy code.

This PR factor out the decisions about when to use the new implementation
for Qobj-based jobs (IBMQJob) or the previous one (IBMQJobPreQobj) and a
bungh of module-level functions.

For distinguishing between jobs sent using Qobj or the old format, the
backend team included a `kind: "q-object" field in the job response.

After conversations with the backend team, it is assumed that all the
backends should answer with the new QobjResult format if the job was a
Qobj-based job and with the previous format if the job was using the old
format.

The PR also extends `from_dict` capabilities to cast numpy types to Python
types.
This commit is contained in:
Salvador de la Puente González 2018-09-13 07:38:30 +02:00
parent fcaf3d5649
commit c67f33629b
7 changed files with 171 additions and 126 deletions

View File

@ -54,10 +54,7 @@ class IBMQBackend(BaseBackend):
Returns:
IBMQJob: an instance derived from BaseJob
"""
if self.configuration().get('allow_q_object'):
job_class = IBMQJob
else:
job_class = IBMQJobPreQobj
job_class = _job_class_from_backend_support(self)
job = job_class(self._api, not self.configuration()['simulator'], qobj=qobj)
job.submit()
return job
@ -216,12 +213,8 @@ class IBMQBackend(BaseBackend):
filter=api_filter)
job_list = []
for job_info in job_info_list:
job_class = self._job_info_to_job_class(job_info)
job_class = _job_class_from_job_response(job_info)
is_device = not bool(self._configuration.get('simulator'))
if 'header' in job_info:
backend_name = job_info['header'].get('backend_name')
elif 'backend' in job_info: # old style job_info
backend_name = job_info['backend'].get('name')
job = job_class(self._api, is_device,
job_id=job_info.get('id'),
backend_name=backend_name,
@ -229,15 +222,6 @@ class IBMQBackend(BaseBackend):
job_list.append(job)
return job_list
def _job_info_to_job_class(self, job_info):
if 'result' in job_info:
job_class = IBMQJob
elif 'qasms' in job_info:
job_class = IBMQJobPreQobj
else:
raise IBMQBackendError('unrecognised job record from API')
return job_class
def retrieve_job(self, job_id):
"""Attempt to get the specified job by job_id
@ -258,7 +242,7 @@ class IBMQBackend(BaseBackend):
except ApiError as ex:
raise IBMQBackendError('Failed to get job "{}":{}'
.format(job_id, str(ex)))
job_class = self._job_info_to_job_class(job_info)
job_class = _job_class_from_job_response(job_info)
is_device = not bool(self._configuration.get('simulator'))
job = job_class(self._api, is_device,
job_id=job_info.get('id'),
@ -275,3 +259,13 @@ class IBMQBackendError(QISKitError):
class IBMQBackendValueError(IBMQBackendError, ValueError):
""" Value errors thrown within IBMQBackend """
pass
def _job_class_from_job_response(job_response):
is_qobj = job_response.get('kind', None) == 'q-object'
return IBMQJob if is_qobj else IBMQJobPreQobj
def _job_class_from_backend_support(backend):
support_qobj = backend.configuration().get('allow_q_object')
return IBMQJob if support_qobj else IBMQJobPreQobj

View File

@ -114,7 +114,7 @@ class IBMQJob(BaseJob):
_executor = futures.ThreadPoolExecutor()
def __init__(self, api, is_device, qobj=None, job_id=None, backend_name=None,
creation_date=None, backend_allows_qobj=False):
creation_date=None):
"""IBMQJob init function.
We can instantiate jobs from two sources: A QObj, and an already submitted job returned by
the API servers.
@ -126,7 +126,6 @@ class IBMQJob(BaseJob):
job_id (String): The job ID of an already submitted job.
backend_name(String): The name of the backend that run the job.
creation_date(String): When the job was run.
backend_allows_qobj (Bool): whether backend allows qobj input directly
Notes:
It is mandatory to pass either ``qobj`` or ``job_id``. Passing a ``qobj``
@ -139,7 +138,7 @@ class IBMQJob(BaseJob):
if qobj is not None:
validate_qobj_against_schema(qobj)
self._qobj = qobj_to_dict(qobj, version='1.0.0')
self._qobj_payload = qobj_to_dict(qobj, version='1.0.0')
# TODO: No need for this conversion, just use the new equivalent members above
old_qobj = qobj_to_dict(qobj, version='0.0.1')
self._job_data = {
@ -170,7 +169,6 @@ class IBMQJob(BaseJob):
self._creation_date = creation_date or current_utc_time()
self._future = None
self._api_error_msg = None
self._backend_allows_qobj = backend_allows_qobj
# pylint: disable=arguments-differ
def result(self, timeout=None, wait=5):
@ -186,48 +184,33 @@ class IBMQJob(BaseJob):
Raises:
JobError: exception raised during job initialization
"""
job_response = self._wait_for_result(timeout=timeout, wait=wait)
return self._result_from_job_response(job_response)
def _wait_for_result(self, timeout=None, wait=5):
self._wait_for_submission()
try:
job_data = self._wait_for_job(timeout=timeout, wait=wait)
job_response = self._wait_for_job(timeout=timeout, wait=wait)
except ApiError as api_err:
raise JobError(str(api_err))
if 'result' in job_data:
return IBMQJob._result_from_api_response(job_data)
elif 'qasms' in job_data:
if self._is_device:
_reorder_bits(job_data)
return IBMQJobPreQobj._result_from_api_response(job_data,
self.id(),
self.backend_name(),
self._is_device,
self.status())
else:
raise JobError('unrecognized job data from API ({})'.format(self._id))
@staticmethod
def _result_from_api_response(api_response):
# Build the Result.
status = self.status()
if status is not JobStatus.DONE:
raise JobError('Invalid job state. The job should be DONE but '
'it is {}'.format(str(status)))
return job_response
def _result_from_job_response(self, job_response):
experiment_results = []
job_result = api_response['result']
for resultobj in job_result['results']:
qobj_exp_result_args = [resultobj.get(arg) for arg in
QobjExperimentResult.REQUIRED_ARGS]
qobj_exp_result_kwargs = {key: value for (key, value) in
resultobj.items() if key not in
QobjExperimentResult.REQUIRED_ARGS}
result_json = job_response['qObjectResult']
for experiment_result_json in result_json['results']:
qobj_experiment_result = QobjExperimentResult(**experiment_result_json)
experiment_results.append(qobj_experiment_result)
qobj_exp_result = QobjExperimentResult(*qobj_exp_result_args,
**qobj_exp_result_kwargs)
experiment_results.append(qobj_exp_result)
qobj_result_args = [job_result.get(arg) for arg in
QobjResult.REQUIRED_ARGS]
qobj_result_kwargs = {key: value for (key, value) in
job_result.items() if key not in
QobjResult.REQUIRED_ARGS}
qobj_result = QobjResult(*qobj_result_args, **qobj_result_kwargs)
# replace job_result list of dict with list of Qobj ExperimentResult
qobj_result.results = experiment_results
return Result(qobj_result)
result_kwargs = {**result_json, 'results': experiment_results}
return Result(QobjResult(**result_kwargs))
def cancel(self):
"""Attempt to cancel a job.
@ -358,10 +341,10 @@ class IBMQJob(BaseJob):
Returns:
dict: A dictionary with the response of the submitted job
"""
backend_name = self._qobj['header']['backend_name']
backend_name = self._backend_name
try:
submit_info = self._api.run_job(self._qobj, backend=backend_name)
submit_info = self._api.run_job(self._qobj_payload, backend=backend_name)
# pylint: disable=broad-except
except Exception as err:
# Undefined error during submission:
@ -482,66 +465,30 @@ class IBMQJobPreQobj(IBMQJob):
self._id = submit_info.get('id')
return submit_info
# pylint disable since this version of the function signature will hopefully
# be removed later
# pylint: disable=arguments-differ
@staticmethod
def _result_from_api_response(api_response, job_id, backend_name, is_device,
job_status):
# Build the Result.
experiment_results = []
if is_device and job_status == JobStatus.DONE:
_reorder_bits(api_response)
def _result_from_job_response(self, job_response):
if self._is_device:
_reorder_bits(job_response)
for circuit_result in api_response['qasms']:
experiment_results = []
for circuit_result in job_response['qasms']:
this_result = {'data': circuit_result['data'],
'name': circuit_result.get('name'),
'compiled_circuit_qasm': circuit_result.get('qasm'),
'status': circuit_result['status'],
'success': circuit_result['status'] == 'DONE',
'shots': api_response['shots']}
'shots': job_response['shots']}
if 'metadata' in circuit_result:
this_result['metadata'] = circuit_result['metadata']
experiment_results.append(this_result)
return result_from_old_style_dict({
'id': job_id,
'status': api_response['status'],
'used_credits': api_response.get('usedCredits'),
'id': self._id,
'status': job_response['status'],
'used_credits': job_response.get('usedCredits'),
'result': experiment_results,
'backend_name': backend_name,
'success': api_response['status'] == 'DONE'
}, [circuit_result['name'] for circuit_result in api_response['qasms']])
def _result_from_api_response(api_response, job_id=None, backend_name=None,
is_device=None):
"""
Decides whether job_data is in pre-qobj format and returns appropriate
job instance
Args:
api_response (dict): dict with the bare contents of the API.get_job request.
job_id (str): job identity on frontend (for pre-qobj results)
backend_name (str): backend name (for pre-qobj results)
is_device (bool): whether backend is a real device
Raises:
JobError: api response doesn't have 'result' or 'qasms' record
Returns:
Result: qiskit.result.Result object
"""
if 'result' in api_response:
return IBMQJob._result_from_api_response(api_response)
elif 'qasms' in api_response:
if is_device:
_reorder_bits(api_response)
return IBMQJobPreQobj._result_from_api_response(api_response, job_id,
backend_name,
is_device)
else:
raise JobError('unrecognized job data from API ({})'.format(job_id))
'backend_name': self.backend_name(),
'success': job_response['status'] == 'DONE'
}, [circuit_result['name'] for circuit_result in job_response['qasms']])
def _reorder_bits(job_data):

View File

@ -9,6 +9,8 @@
from types import SimpleNamespace
import numpy
from ._validation import QobjValidationError
from ._utils import QobjType
@ -42,10 +44,16 @@ class QobjItem(SimpleNamespace):
"""
Return a valid representation of `obj` depending on its type.
"""
if isinstance(obj, list):
if isinstance(obj, (list, tuple)):
return [cls._expand_item(item) for item in obj]
if isinstance(obj, QobjItem):
return obj.as_dict()
if isinstance(obj, numpy.integer):
return int(obj)
if isinstance(obj, numpy.float):
return float(obj)
if isinstance(obj, numpy.ndarray):
return obj.tolist()
return obj
@classmethod

View File

@ -1,5 +1,5 @@
jsonschema>=2.6,<2.7
IBMQuantumExperience>=2.0.1
IBMQuantumExperience>=2.0.3
matplotlib>=2.1
networkx>=2.0
numpy>=1.13

View File

@ -17,12 +17,12 @@ from setuptools.dist import Distribution
requirements = [
"jsonschema>=2.6,<2.7",
"IBMQuantumExperience>=1.9.8",
"IBMQuantumExperience>=2.0.3",
"matplotlib>=2.1",
"networkx>=2.0",
"numpy>=1.13",
"ply>=3.10",
"scipy>=0.19",
"scipy>=0.19,!=0.19.1",
"sympy>=1.0",
"pillow>=4.2.1"
]

View File

@ -9,6 +9,7 @@
"""IBMQJob Test."""
import os
import time
import unittest
from concurrent import futures
@ -45,14 +46,7 @@ class TestIBMQJob(JobTestCase):
def setUp(self):
super().setUp()
# create QuantumCircuit
qr = QuantumRegister(2, 'q')
cr = ClassicalRegister(2, 'c')
qc = QuantumCircuit(qr, cr)
qc.h(qr[0])
qc.cx(qr[0], qr[1])
qc.measure(qr, cr)
self._qc = qc
self._qc = _bell_circuit()
@requires_qe_access
def test_run_simulator(self, qe_token, qe_url):
@ -337,5 +331,48 @@ class TestIBMQJob(JobTestCase):
job.submit()
class TestQObjectBasedIBMQJob(JobTestCase):
"""Test jobs supporting QObject."""
def setUp(self):
super().setUp()
self._testing_device = os.getenv('IBMQ_QOBJ_DEVICE', None)
self._qe_token = os.getenv('IBMQ_TOKEN', None)
self._qe_url = os.getenv('IBMQ_QOBJ_URL')
if not self._testing_device or not self._qe_token or not self._qe_url:
self.skipTest('No credentials or testing device available for '
'testing Qobj capabilities.')
self._provider = IBMQProvider(self._qe_token, self._qe_url)
self._backend = self._provider.get_backend(self._testing_device)
self._qc = _bell_circuit()
def test_qobject_enabled_job(self):
"""Job should be an instance of IBMQJob."""
qobj = transpiler.compile(self._qc, self._backend)
job = self._backend.run(qobj)
self.assertIsInstance(job, IBMQJob)
def test_qobject_result(self):
"""Jobs can be retrieved."""
qobj = transpiler.compile(self._qc, self._backend)
job = self._backend.run(qobj)
try:
job.result()
except JobError as err:
self.fail(err)
def _bell_circuit():
qr = QuantumRegister(2, 'q')
cr = ClassicalRegister(2, 'c')
qc = QuantumCircuit(qr, cr)
qc.h(qr[0])
qc.cx(qr[0], qr[1])
qc.measure(qr, cr)
return qc
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@ -15,7 +15,7 @@ import time
from contextlib import suppress
from IBMQuantumExperience import ApiError
from qiskit.backends.jobstatus import JobStatus
from qiskit.backends.ibmq.ibmqjob import IBMQJob
from qiskit.backends.ibmq.ibmqjob import IBMQJobPreQobj, IBMQJob
from qiskit.backends.ibmq.ibmqjob import API_FINAL_STATES
from qiskit.backends import JobError, JobTimeoutError
from .common import JobTestCase
@ -258,13 +258,27 @@ class TestIBMQJobStates(JobTestCase):
else:
self.assertFalse(self._current_api.get_job.called)
def run_with_api(self, api):
"""Creates a new `IBMQJob` instance running with the provided API
# TODO: Once qobj results come by default from all the simulator
# backends, move to integration tests in test_result.py
def test_qobj_result(self):
job = self.run_with_api(QObjResultAPI(), job_class=IBMQJob)
self.wait_for_initialization(job)
self._current_api.progress()
result = job.result()
self.assertEqual(result.get_status(), 'COMPLETED')
self.assertEqual(result.get_counts('Bell state'),
{'0x0': 480, '0x3': 490, '0x1': 20, '0x2': 34})
self.assertEqual(result.get_counts('Bell state XY'),
{'0x0': 29, '0x3': 15, '0x1': 510, '0x2': 480})
self.assertEqual(len(result), 2)
def run_with_api(self, api, job_class=IBMQJobPreQobj):
"""Creates a new ``IBMQJobPreQobj`` instance running with the provided API
object.
"""
self._current_api = api
self._current_qjob = IBMQJob(api, False, qobj=new_fake_qobj(),
backend_allows_qobj=True)
self._current_qjob = job_class(api, False, qobj=new_fake_qobj())
self._current_qjob.submit()
return self._current_qjob
@ -450,7 +464,7 @@ class ThrowingGetJobAPI(BaseFakeAPI):
return self._job_status[self._state]
def get_job(self, job_id):
raise ApiError("Unexpected error")
raise ApiError('Unexpected error')
class CancellableAPI(BaseFakeAPI):
@ -491,5 +505,50 @@ class ErroredCancellationAPI(BaseFakeAPI):
return {'status': 'Error', 'error': 'test-error-while-cancelling'}
# TODO: Remove once qobj results come by default from all the simulator
# backends.
class QObjResultAPI(BaseFakeAPI):
"""Class for emulating a successfully-completed non-queued API."""
_job_status = [
{'status': 'RUNNING'},
{
'status': 'COMPLETED',
'qObjectResult': {
'backend_name': 'ibmqx2',
'backend_version': '1.1.1',
'job_id': 'XC1323XG2',
'qobj_id': 'Experiment1',
'success': True,
'status': 'COMPLETED',
'results': [
{
'header': {'name': 'Bell state'},
'shots': 1024,
'status': 'DONE',
'success': True,
'data': {
'counts': {
'0x0': 480, '0x3': 490, '0x1': 20, '0x2': 34
}
}
},
{
'header': {'name': 'Bell state XY'},
'shots': 1024,
'status': 'DONE',
'success': True,
'data': {
'counts': {
'0x0': 29, '0x3': 15, '0x1': 510, '0x2': 480
}
}
}
]
}
}
]
if __name__ == '__main__':
unittest.main(verbosity=2)