From 07b2485cb0c05d3649de83ece9be1c50c3aff979 Mon Sep 17 00:00:00 2001 From: Ali Javadi-Abhari Date: Thu, 3 May 2018 05:25:57 -0400 Subject: [PATCH] Reorder result bits from real devices (#430) * _reorder_bits method in ibmqbackend for real backends * add reordering tests lint * qk -> qiskit * SKIP_SLOW_TESTS * check the status is COMPLETED before reordering * local_qiskit_simulator -> local_qasm_simulator * style * revise timeout for test_reordering * Reorganize authentication in test_reordering Move the `_authenticate()` call to inside `TestBitReordering`, to avoid `_requires_qe_access` raising an `SkipTest` during test discovery (at module import time). --- doc/dev_introduction.rst | 4 +- qiskit/backends/ibmq/ibmqbackend.py | 57 +++++++++++++++ test/python/common.py | 23 ++++++ test/python/test_reordering.py | 108 ++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 test/python/test_reordering.py diff --git a/doc/dev_introduction.rst b/doc/dev_introduction.rst index e9de0aeb56..1b9aa88e10 100644 --- a/doc/dev_introduction.rst +++ b/doc/dev_introduction.rst @@ -196,4 +196,6 @@ Windows: C:\..\> python -m unittest test/python/test_apps.py Additionally, an environment variable ``SKIP_ONLINE_TESTS`` can be used for -toggling the execution of the tests that require network access to the API. +toggling the execution of the tests that require network access to the API and +``SKIP_SLOW_TESTS`` can be used to toggling execution of tests that are +particularly slow (default is ``True``). diff --git a/qiskit/backends/ibmq/ibmqbackend.py b/qiskit/backends/ibmq/ibmqbackend.py index 2f5c7555e0..7533d8e34a 100644 --- a/qiskit/backends/ibmq/ibmqbackend.py +++ b/qiskit/backends/ibmq/ibmqbackend.py @@ -120,6 +120,8 @@ class IBMQBackend(BaseBackend): job_result['name'] = qobj['id'] job_result['backend'] = qobj['config']['backend_name'] this_result = Result(job_result, qobj) + if not self.configuration['simulator'] and this_result.get_status() == "COMPLETED": + _reorder_bits(this_result) # TODO: remove this after Qobj return this_result @property @@ -264,3 +266,58 @@ def _wait_for_job(job_id, api, wait=5, timeout=60): 'status': job_result['qasms'][index]['status']}) return {'job_id': job_id, 'status': job_result['status'], 'result': job_result_return} + + +def _reorder_bits(result): + """temporary fix for ibmq backends. + for every ran circuit, get reordering information from qobj + and apply reordering on result""" + for idx, circ in enumerate(result._qobj['circuits']): + + # device_qubit -> device_clbit (how it should have been) + measure_dict = {op['qubits'][0]: op['clbits'][0] + for op in circ['compiled_circuit']['operations'] + if op['name'] == 'measure'} + + res = result._result['result'][idx] + counts_dict_new = {} + for item in res['data']['counts'].items(): + # fix clbit ordering to what it should have been + bits = list(item[0]) + bits.reverse() # lsb in 0th position + count = item[1] + reordered_bits = list('x' * len(bits)) + for device_clbit, bit in enumerate(bits): + if device_clbit in measure_dict: + correct_device_clbit = measure_dict[device_clbit] + reordered_bits[correct_device_clbit] = bit + reordered_bits.reverse() + + # only keep the clbits specified by circuit, not everything on device + num_clbits = circ['compiled_circuit']['header']['number_of_clbits'] + compact_key = reordered_bits[-num_clbits:] + compact_key = "".join([b if b != 'x' else '0' + for b in compact_key]) + + # insert spaces to signify different classical registers + cregs = circ['compiled_circuit']['header']['clbit_labels'] + if sum([creg[1] for creg in cregs]) != num_clbits: + raise ResultError("creg sizes don't add up in result header.") + creg_begin_pos = [] + creg_end_pos = [] + acc = 0 + for creg in reversed(cregs): + creg_size = creg[1] + creg_begin_pos.append(acc) + creg_end_pos.append(acc + creg_size) + acc += creg_size + compact_key = " ".join([compact_key[creg_begin_pos[i]:creg_end_pos[i]] + for i in range(len(cregs))]) + + # marginalize over unwanted measured qubits + if compact_key not in counts_dict_new: + counts_dict_new[compact_key] = count + else: + counts_dict_new[compact_key] += count + + res['data']['counts'] = counts_dict_new diff --git a/test/python/common.py b/test/python/common.py index 7e2438afb0..861a11e64a 100644 --- a/test/python/common.py +++ b/test/python/common.py @@ -40,6 +40,7 @@ class Path(Enum): class QiskitTestCase(unittest.TestCase): """Helper class that contains common functionality.""" + @classmethod def setUpClass(cls): cls.moduleName = os.path.splitext(inspect.getfile(cls))[0] @@ -202,6 +203,26 @@ class _AssertNoLogsContext(unittest.case._AssertLogsContext): self._raiseFailure(msg) +def slow_test(func): + """ + Decorator that signals that the test takes minutes to run. + + Args: + func (callable): test function to be decorated. + + Returns: + callable: the decorated function. + """ + + @functools.wraps(func) + def _(*args, **kwargs): + if SKIP_SLOW_TESTS: + raise unittest.SkipTest('Skipping slow tests') + return func(*args, **kwargs) + + return _ + + def requires_qe_access(func): """ Decorator that signals that the test uses the online API: @@ -217,6 +238,7 @@ def requires_qe_access(func): Returns: callable: the decorated function. """ + @functools.wraps(func) def _(*args, **kwargs): # pylint: disable=invalid-name @@ -266,3 +288,4 @@ def _is_ci_fork_pull_request(): SKIP_ONLINE_TESTS = os.getenv('SKIP_ONLINE_TESTS', _is_ci_fork_pull_request()) +SKIP_SLOW_TESTS = os.getenv('SKIP_SLOW_TESTS', True) not in ['false', 'False', '-1'] diff --git a/test/python/test_reordering.py b/test/python/test_reordering.py new file mode 100644 index 0000000000..c15a765071 --- /dev/null +++ b/test/python/test_reordering.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# pylint: disable=invalid-name,no-value-for-parameter,broad-except + +# Copyright 2018 IBM RESEARCH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================= +"""Tests for bit reordering fix.""" + +import unittest +import qiskit +from qiskit.wrapper import register, available_backends, get_backend, execute +from .common import requires_qe_access, QiskitTestCase, slow_test + + +def lowest_pending_jobs(list_of_backends): + """Returns the backend with lowest pending jobs.""" + by_pending_jobs = sorted(list_of_backends, + key=lambda x: get_backend(x).status['pending_jobs']) + return by_pending_jobs[0] + + +class TestBitReordering(QiskitTestCase): + """Test QISKit's fix for the ibmq hardware reordering bug. + + The bug will be fixed with the introduction of qobj, + in which case these tests can be used to verify correctness. + """ + @slow_test + @requires_qe_access + def test_basic_reordering(self, QE_TOKEN, QE_URL): + """a simple reordering within a 2-qubit register""" + sim, real = self._get_backends(QE_TOKEN, QE_URL) + unittest.skipIf(not real, 'no remote device available.') + + q = qiskit.QuantumRegister(2) + c = qiskit.ClassicalRegister(2) + circ = qiskit.QuantumCircuit(q, c) + circ.h(q[0]) + circ.measure(q[0], c[1]) + circ.measure(q[1], c[0]) + + shots = 2000 + result_real = execute(circ, real, {"shots": shots}, timeout=600) + result_sim = execute(circ, sim, {"shots": shots}) + counts_real = result_real.get_counts() + counts_sim = result_sim.get_counts() + threshold = 0.1 * shots + self.assertDictAlmostEqual(counts_real, counts_sim, threshold) + + @slow_test + @requires_qe_access + def test_multi_register_reordering(self, QE_TOKEN, QE_URL): + """a more complicated reordering across 3 registers of different sizes""" + sim, real = self._get_backends(QE_TOKEN, QE_URL) + unittest.skipIf(not real, 'no remote device available.') + + q0 = qiskit.QuantumRegister(2) + q1 = qiskit.QuantumRegister(2) + q2 = qiskit.QuantumRegister(1) + c0 = qiskit.ClassicalRegister(2) + c1 = qiskit.ClassicalRegister(2) + c2 = qiskit.ClassicalRegister(1) + circ = qiskit.QuantumCircuit(q0, q1, q2, c0, c1, c2) + circ.h(q0[0]) + circ.cx(q0[0], q2[0]) + circ.x(q1[1]) + circ.h(q2[0]) + circ.ccx(q2[0], q1[1], q1[0]) + circ.barrier() + circ.measure(q0[0], c2[0]) + circ.measure(q0[1], c0[1]) + circ.measure(q1[0], c0[0]) + circ.measure(q1[1], c1[0]) + circ.measure(q2[0], c1[1]) + + shots = 4000 + result_real = execute(circ, real, {"shots": shots}, timeout=600) + result_sim = execute(circ, sim, {"shots": shots}) + counts_real = result_real.get_counts() + counts_sim = result_sim.get_counts() + threshold = 0.2 * shots + self.assertDictAlmostEqual(counts_real, counts_sim, threshold) + + def _get_backends(self, QE_TOKEN, QE_URL): + sim_backend = 'local_qasm_simulator' + try: + register(QE_TOKEN, QE_URL) + real_backends = available_backends({'simulator': False}) + real_backend = lowest_pending_jobs(real_backends) + except Exception: + real_backend = None + + return sim_backend, real_backend + + +if __name__ == '__main__': + unittest.main(verbosity=2)