diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 46f037ab2980..e99bd549497c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,7 @@ The format is based on `Keep a Changelog`_. Added ----- +- Retreive IBM Q jobs from server (#563, #585). - Add German introductory documentation (``doc/de``) (#592). - Add ``unregister()`` for removing previously registered providers (#584). - Adding backend filtering by least busy (#575). diff --git a/qiskit/backends/ibmq/ibmqbackend.py b/qiskit/backends/ibmq/ibmqbackend.py index 678284c89594..97d50823c941 100644 --- a/qiskit/backends/ibmq/ibmqbackend.py +++ b/qiskit/backends/ibmq/ibmqbackend.py @@ -15,6 +15,7 @@ from qiskit._util import _camel_case_to_snake_case from qiskit.backends import BaseBackend from qiskit.backends.ibmq.ibmqjob import IBMQJob +from qiskit.backends import JobStatus logger = logging.getLogger(__name__) @@ -141,18 +142,72 @@ def status(self): "Couldn't get backend status: {0}".format(ex)) return status - def jobs(self, limit=50, skip=0): - """Attempt to get the jobs submitted to the backend + def jobs(self, limit=50, skip=0, status=None, db_filter=None): + """Attempt to get the jobs submitted to the backend. Args: limit (int): number of jobs to retrieve skip (int): starting index of retrieval + status (None or JobStatus or str): only get jobs with this status, + where status is e.g. `JobStatus.RUNNING` or `'RUNNING'` + db_filter (dict): `loopback-based filter + `_. + This is an interface to a database ``where`` filter. Some + examples of its usage are: + + Filter last five jobs with errors:: + + job_list = backend.jobs(limit=5, status=JobStatus.ERROR) + + Filter last five jobs with counts=1024, and counts for + states ``00`` and ``11`` each exceeding 400:: + + cnts_filter = {'shots': 1024, + 'qasms.result.data.counts.00': {'gt': 400}, + 'qasms.result.data.counts.11': {'gt': 400}} + job_list = backend.jobs(limit=5, db_filter=cnts_filter) + + Filter last five jobs from 30 days ago:: + + past_date = datetime.datetime.now() - datetime.timedelta(days=30) + date_filter = {'creationDate': {'lt': past_date.isoformat()}} + job_list = backend.jobs(limit=5, db_filter=date_filter) + Returns: list(IBMQJob): list of IBMQJob instances + + Raises: + IBMQBackendValueError: status keyword value unrecognized """ backend_name = self.configuration['name'] + api_filter = {} + if status: + if isinstance(status, str): + status = JobStatus[status] + if status == JobStatus.RUNNING: + this_filter = {'status': 'RUNNING', + 'infoQueue': {'exists': False}} + elif status == JobStatus.QUEUED: + this_filter = {'status': 'RUNNING', + 'infoQueue.status': 'PENDING_IN_QUEUE'} + elif status == JobStatus.CANCELLED: + this_filter = {'status': 'CANCELLED'} + elif status == JobStatus.DONE: + this_filter = {'status': 'COMPLETED'} + elif status == JobStatus.ERROR: + this_filter = {'status': {'regexp': '^ERROR'}} + else: + raise IBMQBackendValueError('unrecongized value for "status" keyword ' + 'in job filter') + api_filter.update(this_filter) + if db_filter: + # filter ignores backend_name filter so we need to set it + api_filter['backend.name'] = backend_name + # status takes precendence over db_filter for same keys + api_filter = {**db_filter, **api_filter} job_info_list = self._api.get_jobs(limit=limit, skip=skip, - backend=backend_name) + backend=backend_name, + filter=api_filter) job_list = [] for job_info in job_info_list: is_device = not bool(self._configuration.get('simulator')) @@ -183,3 +238,8 @@ def retrieve_job(self, job_id): class IBMQBackendError(QISKitError): """IBM Q Backend Errors""" pass + + +class IBMQBackendValueError(IBMQBackendError, ValueError): + """ Value errors thrown within IBMQBackend """ + pass diff --git a/test/python/test_ibmqjob.py b/test/python/test_ibmqjob.py index 4c9c55271566..84012daa06fa 100644 --- a/test/python/test_ibmqjob.py +++ b/test/python/test_ibmqjob.py @@ -12,6 +12,7 @@ import time import unittest from concurrent import futures +import datetime import numpy from scipy.stats import chi2_contingency @@ -291,6 +292,46 @@ def test_retrieve_job_error(self): backend = _least_busy(backends) self.assertRaises(IBMQBackendError, backend.retrieve_job, 'BAD_JOB_ID') + def test_get_jobs_filter_job_status(self): + backends = self._provider.available_backends() + backend = _least_busy(backends) + job_list = backend.jobs(limit=5, skip=0, status=JobStatus.DONE) + self.log.info('found %s matching jobs', len(job_list)) + for i, job in enumerate(job_list): + self.log.info('match #%d: %s', i, job.result()._result['status']) + self.assertTrue(job.status['status'] == JobStatus.DONE) + + def test_get_jobs_filter_counts(self): + # TODO: consider generalizing backend name + backend = self._provider.get_backend('ibmq_qasm_simulator') + my_filter = {'backend.name': 'ibmq_qasm_simulator', + 'shots': 1024, + 'qasms.result.data.counts.00': {'lt': 500}} + self.log.info('searching for at most 5 jobs with 1024 shots, a count ' + 'for "00" of < 500, on the ibmq_qasm_simulator backend') + job_list = backend.jobs(limit=5, skip=0, db_filter=my_filter) + self.log.info('found %s matching jobs', len(job_list)) + for i, job in enumerate(job_list): + self.log.info('match #%d', i) + result = job.result() + self.assertTrue(any(cresult['data']['counts']['00'] < 500 + for cresult in result._result['result'])) + for circuit_name in result.get_names(): + self.log.info('\tcircuit_name: %s', circuit_name) + counts = result.get_counts(circuit_name) + self.log.info('\t%s', str(counts)) + + def test_get_jobs_filter_date(self): + backends = self._provider.available_backends() + backend = _least_busy(backends) + past_day_30 = datetime.datetime.now() - datetime.timedelta(days=30) + my_filter = {'creationDate': {'lt': past_day_30.isoformat()}} + job_list = backend.jobs(limit=5, db_filter=my_filter) + self.log.info('found %s matching jobs', len(job_list)) + for i, job in enumerate(job_list): + self.log.info('match #%d: %s', i, job.creation_date) + self.assertTrue(job.creation_date < past_day_30.isoformat()) + if __name__ == '__main__': unittest.main(verbosity=2)