From 41cf87c59a2c3381e0c8190f022058356aa8f586 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Mon, 13 Jul 2020 11:20:52 -0400 Subject: [PATCH] Support for backends with defective qubits and gates (#4110) * initial commit * faulty qubit insertion * operational False * pruned coupling map * no gate operation parameter for now * faulty gate * fake_ourense_faulty_cx13 * testing of the fake backends * a test * disconnected coupling map * add fault register at the end * transform the circuit into the faulty backend * simulators do not have properties * assertIdleQ1 * assertEqualCount * remove faulty qubtis from properties * backend_property for faulty qubits * adapt Layout2qDistance to disconnected coupling map * remove _create_qreg * assuming coupling map * layout with disconnected qubits * test for faulty gate * support for faulty gate * remove gates when they are faulty from the backend properties * test * unused import * new fault backend * test * take the largest connected component * lint * adjust test * lint * lint1 * lint2 * lint3 * lint4 * lint5 * _parse_initial_layout * initial layot support * TestFaultyCX13 * more testing * cm -> context * Update qiskit/providers/models/backendproperties.py Co-authored-by: Ali Javadi-Abhari * Apply suggestions from code review Co-authored-by: Ali Javadi-Abhari * mv qiskit/test/mock/backends/ourense/fake_ourense_faulty* to test/python/providers/faulty_backends.py * cleaning up * lint * moving faulty_qubits and faulty_gates to properties * another fix * lint Co-authored-by: Ali Javadi-Abhari Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/circuit/quantumcircuit.py | 12 + qiskit/compiler/transpile.py | 164 ++++++++++-- qiskit/providers/models/backendproperties.py | 52 ++++ qiskit/test/mock/fake_backend.py | 6 + qiskit/transpiler/coupling.py | 4 + test/python/providers/faulty_backends.py | 79 ++++++ .../providers/test_backendproperties.py | 4 + test/python/providers/test_faulty_backend.py | 70 +++++ test/python/transpiler/test_faulty_backend.py | 247 ++++++++++++++++++ 9 files changed, 623 insertions(+), 15 deletions(-) create mode 100644 test/python/providers/faulty_backends.py create mode 100644 test/python/providers/test_faulty_backend.py create mode 100644 test/python/transpiler/test_faulty_backend.py diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index cc60191bb6d8..67284c31eb77 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1460,6 +1460,18 @@ def _create_creg(self, length, name): new_creg = ClassicalRegister(length, name) return new_creg + def _create_qreg(self, length, name): + """ Creates a qreg, checking if QuantumRegister with same name exists + """ + if name in [qreg.name for qreg in self.qregs]: + save_prefix = QuantumRegister.prefix + QuantumRegister.prefix = name + new_qreg = QuantumRegister(length) + QuantumRegister.prefix = save_prefix + else: + new_qreg = QuantumRegister(length, name) + return new_qreg + def measure_active(self, inplace=True): """Adds measurement to all non-idle qubits. Creates a new ClassicalRegister with a size equal to the number of non-idle qubits being measured. diff --git a/qiskit/compiler/transpile.py b/qiskit/compiler/transpile.py index 9f047fe108ec..df91e7c5329b 100644 --- a/qiskit/compiler/transpile.py +++ b/qiskit/compiler/transpile.py @@ -20,6 +20,7 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.providers import BaseBackend from qiskit.providers.models import BackendProperties +from qiskit.providers.models.backendproperties import Gate from qiskit.transpiler import Layout, CouplingMap, PropertySet, PassManager from qiskit.transpiler.basepasses import BasePass from qiskit.dagcircuit import DAGCircuit @@ -29,7 +30,8 @@ from qiskit.circuit.quantumregister import Qubit from qiskit import user_config from qiskit.transpiler.exceptions import TranspilerError -from qiskit.converters import isinstanceint, isinstancelist +from qiskit.transpiler.passes import ApplyLayout +from qiskit.converters import isinstanceint, isinstancelist, dag_to_circuit, circuit_to_dag from qiskit.transpiler.passes.basis.ms_basis_decomposer import MSBasisDecomposer from qiskit.transpiler.preset_passmanagers import (level_0_pass_manager, level_1_pass_manager, @@ -201,17 +203,20 @@ def callback_func(**kwargs): config = user_config.get_config() optimization_level = config.get('transpile_optimization_level', 1) + faulty_qubits_map = _create_faulty_qubits_map(backend) + # Get transpile_args to configure the circuit transpilation job(s) transpile_args = _parse_transpile_args(circuits, backend, basis_gates, coupling_map, backend_properties, initial_layout, layout_method, routing_method, translation_method, seed_transpiler, optimization_level, - callback, output_name) + callback, output_name, faulty_qubits_map) _check_circuits_coupling_map(circuits, transpile_args, backend) # Transpile circuits in parallel - circuits = parallel_map(_transpile_circuit, list(zip(circuits, transpile_args))) + circuits = parallel_map(_transpile_circuit, list(zip(circuits, transpile_args)), + task_args=(faulty_qubits_map, backend)) if len(circuits) == 1: end_time = time() @@ -256,7 +261,8 @@ def _log_transpile_time(start_time, end_time): LOG.info(log_msg) -def _transpile_circuit(circuit_config_tuple: Tuple[QuantumCircuit, Dict]) -> QuantumCircuit: +def _transpile_circuit(circuit_config_tuple: Tuple[QuantumCircuit, Dict], + faulty_qubits_map: [None, Dict], backend) -> QuantumCircuit: """Select a PassManager and run a single circuit through it. Args: circuit_config_tuple (tuple): @@ -264,10 +270,14 @@ def _transpile_circuit(circuit_config_tuple: Tuple[QuantumCircuit, Dict]) -> Qua transpile_config (dict): configuration dictating how to transpile. The dictionary has the following format: {'optimization_level': int, - 'pass_manager': PassManager, 'output_name': string, 'callback': callable, 'pass_manager_config': PassManagerConfig} + faulty_qubits_map (dict or None): Only used when backends report faulty qubits/gates. It + is a map from working qubit in the backend to dumnmy qubits that are consecutive + and connected. + backend (BaseBackend or None): The backend with remap the circuit if it has faulty + qubits/gates. Returns: The transpiled circuit Raises: @@ -308,15 +318,73 @@ def _transpile_circuit(circuit_config_tuple: Tuple[QuantumCircuit, Dict]) -> Qua if ms_basis_swap is not None: pass_manager.append(MSBasisDecomposer(ms_basis_swap)) - return pass_manager.run(circuit, callback=transpile_config['callback'], - output_name=transpile_config['output_name']) + result = pass_manager.run(circuit, callback=transpile_config['callback'], + output_name=transpile_config['output_name']) + + if faulty_qubits_map: + return _remap_circuit_faulty_backend(result, backend, faulty_qubits_map) + + return result + + +def _remap_circuit_faulty_backend(circuit, backend, faulty_qubits_map): + faulty_qubits = backend.properties().faulty_qubits() if backend.properties() else [] + disconnected_qubits = {k for k, v in faulty_qubits_map.items() + if v is None}.difference(faulty_qubits) + faulty_qubits_map_reverse = {v: k for k, v in faulty_qubits_map.items()} + if faulty_qubits: + faulty_qreg = circuit._create_qreg(len(faulty_qubits), 'faulty') + else: + faulty_qreg = [] + if disconnected_qubits: + disconnected_qreg = circuit._create_qreg(len(disconnected_qubits), 'disconnected') + else: + disconnected_qreg = [] + + new_layout = Layout() + faulty_qubit = 0 + disconnected_qubit = 0 + + for real_qubit in range(backend.configuration().n_qubits): + if faulty_qubits_map[real_qubit] is not None: + new_layout[real_qubit] = circuit._layout[faulty_qubits_map[real_qubit]] + else: + if real_qubit in faulty_qubits: + new_layout[real_qubit] = faulty_qreg[faulty_qubit] + faulty_qubit += 1 + else: + new_layout[real_qubit] = disconnected_qreg[disconnected_qubit] + disconnected_qubit += 1 + physical_layout_dict = {} + for qubit in circuit.qubits: + physical_layout_dict[qubit] = faulty_qubits_map_reverse[qubit.index] + for qubit in faulty_qreg[:] + disconnected_qreg[:]: + physical_layout_dict[qubit] = new_layout[qubit] + dag_circuit = circuit_to_dag(circuit) + apply_layout_pass = ApplyLayout() + apply_layout_pass.property_set['layout'] = Layout(physical_layout_dict) + circuit = dag_to_circuit(apply_layout_pass.run(dag_circuit)) + circuit._layout = new_layout + return circuit + + +def _remap_layout_faulty_backend(layout, faulty_qubits_map): + if layout is None: + return layout + new_layout = Layout() + for virtual, physical in layout.get_virtual_bits().items(): + if faulty_qubits_map[physical] is None: + raise TranspilerError("The initial_layout parameter refers to faulty" + " or disconnected qubits") + new_layout[virtual] = faulty_qubits_map[physical] + return new_layout def _parse_transpile_args(circuits, backend, basis_gates, coupling_map, backend_properties, initial_layout, layout_method, routing_method, translation_method, seed_transpiler, optimization_level, - callback, output_name) -> List[Dict]: + callback, output_name, faulty_qubits_map) -> List[Dict]: """Resolve the various types of args allowed to the transpile() function through duck typing, overriding args, etc. Refer to the transpile() docstring for details on what types of inputs are allowed. @@ -336,9 +404,10 @@ def _parse_transpile_args(circuits, backend, num_circuits = len(circuits) basis_gates = _parse_basis_gates(basis_gates, backend, circuits) - coupling_map = _parse_coupling_map(coupling_map, backend, num_circuits) - backend_properties = _parse_backend_properties(backend_properties, backend, num_circuits) - initial_layout = _parse_initial_layout(initial_layout, circuits) + coupling_map = _parse_coupling_map(coupling_map, backend, num_circuits, faulty_qubits_map) + backend_properties = _parse_backend_properties(backend_properties, backend, num_circuits, + faulty_qubits_map) + initial_layout = _parse_initial_layout(initial_layout, circuits, faulty_qubits_map) layout_method = _parse_layout_method(layout_method, num_circuits) routing_method = _parse_routing_method(routing_method, num_circuits) translation_method = _parse_translation_method(translation_method, num_circuits) @@ -368,6 +437,37 @@ def _parse_transpile_args(circuits, backend, return list_transpile_args +def _create_faulty_qubits_map(backend): + """If the backend has faulty qubits, those should be excluded. A faulty_qubit_map is a map + from working qubit in the backend to dumnmy qubits that are consecutive and connected.""" + faulty_qubits_map = None + if backend is not None: + if backend.properties(): + faulty_qubits = backend.properties().faulty_qubits() + faulty_edges = [gates.qubits for gates in backend.properties().faulty_gates()] + else: + faulty_qubits = [] + faulty_edges = [] + + if faulty_qubits or faulty_edges: + faulty_qubits_map = {} + configuration = backend.configuration() + full_coupling_map = configuration.coupling_map + functional_cm_list = [edge for edge in full_coupling_map + if (set(edge).isdisjoint(faulty_qubits) and + edge not in faulty_edges)] + + connected_working_qubits = CouplingMap(functional_cm_list).largest_connected_component() + dummy_qubit_counter = 0 + for qubit in range(configuration.n_qubits): + if qubit in connected_working_qubits: + faulty_qubits_map[qubit] = dummy_qubit_counter + dummy_qubit_counter += 1 + else: + faulty_qubits_map[qubit] = None + return faulty_qubits_map + + def _parse_basis_gates(basis_gates, backend, circuits): # try getting basis_gates from user, else backend if basis_gates is None: @@ -381,13 +481,19 @@ def _parse_basis_gates(basis_gates, backend, circuits): return basis_gates -def _parse_coupling_map(coupling_map, backend, num_circuits): +def _parse_coupling_map(coupling_map, backend, num_circuits, faulty_map): # try getting coupling_map from user, else backend if coupling_map is None: if getattr(backend, 'configuration', None): configuration = backend.configuration() if hasattr(configuration, 'coupling_map') and configuration.coupling_map: - coupling_map = CouplingMap(configuration.coupling_map) + if faulty_map: + coupling_map = CouplingMap() + for qubit1, qubit2 in configuration.coupling_map: + if faulty_map[qubit1] is not None and faulty_map[qubit2] is not None: + coupling_map.add_edge(faulty_map[qubit1], faulty_map[qubit2]) + else: + coupling_map = CouplingMap(configuration.coupling_map) # coupling_map could be None, or a list of lists, e.g. [[0, 1], [2, 1]] if coupling_map is None or isinstance(coupling_map, CouplingMap): @@ -401,17 +507,40 @@ def _parse_coupling_map(coupling_map, backend, num_circuits): return coupling_map -def _parse_backend_properties(backend_properties, backend, num_circuits): +def _parse_backend_properties(backend_properties, backend, num_circuits, faulty_qubits_map): # try getting backend_properties from user, else backend if backend_properties is None: if getattr(backend, 'properties', None): backend_properties = backend.properties() + if backend_properties and \ + (backend_properties.faulty_qubits() or backend_properties.faulty_gates()): + faulty_qubits = sorted(backend_properties.faulty_qubits(), reverse=True) + faulty_edges = [gates.qubits for gates in backend_properties.faulty_gates()] + # remove faulty qubits in backend_properties.qubits + for faulty_qubit in faulty_qubits: + del backend_properties.qubits[faulty_qubit] + + gates = [] + for gate in backend_properties.gates: + # remove gates using faulty edges or with faulty qubits (and remap the + # gates in terms of faulty_qubits_map) + if any([faulty_qubits_map[qubits] is not None for qubits in gate.qubits]) or \ + gate.qubits in faulty_edges: + continue + gate_dict = gate.to_dict() + replacement_gate = Gate.from_dict(gate_dict) + gate_dict['qubits'] = [faulty_qubits_map[qubit] for qubit in gate.qubits] + args = '_'.join([str(qubit) for qubit in gate_dict['qubits']]) + gate_dict['name'] = "%s%s" % (gate_dict['gate'], args) + gates.append(replacement_gate) + + backend_properties.gates = gates if not isinstance(backend_properties, list): backend_properties = [backend_properties] * num_circuits return backend_properties -def _parse_initial_layout(initial_layout, circuits): +def _parse_initial_layout(initial_layout, circuits, faulty_qubits_map): # initial_layout could be None, or a list of ints, e.g. [0, 5, 14] # or a list of tuples/None e.g. [qr[0], None, qr[1]] or a dict e.g. {qr[0]: 0} def _layout_from_raw(initial_layout, circuit): @@ -436,8 +565,13 @@ def _layout_from_raw(initial_layout, circuit): else: # even if one layout, but multiple circuits, the layout needs to be adapted for each initial_layout = [_layout_from_raw(initial_layout, circ) for circ in circuits] + if not isinstance(initial_layout, list): initial_layout = [initial_layout] * len(circuits) + + if faulty_qubits_map: + initial_layout = [_remap_layout_faulty_backend(i, faulty_qubits_map) for i in + initial_layout] return initial_layout diff --git a/qiskit/providers/models/backendproperties.py b/qiskit/providers/models/backendproperties.py index c180e0290816..a97654aa3b02 100644 --- a/qiskit/providers/models/backendproperties.py +++ b/qiskit/providers/models/backendproperties.py @@ -314,6 +314,43 @@ def gate_property(self, raise BackendPropertyError("Could not find the desired property for {g}".format(g=gate)) return result + def faulty_qubits(self): + """Return a list of faulty qubits. + """ + faulty = [] + for qubit in self._qubits: + if not self.is_qubit_operational(qubit): + faulty.append(qubit) + return faulty + + def faulty_gates(self): + """Return a list of faulty gates. + """ + faulty = [] + for gate in self.gates: + if not self.is_gate_operational(gate.gate, gate.qubits): + faulty.append(gate) + return faulty + + def is_gate_operational(self, + gate: str, + qubits: Union[int, Iterable[int]] = None) -> bool: + """ + Return the operational status of the given gate. + + Args: + gate: Name of the gate. + qubits: The qubit to find the operational status for. + + Returns: + bool: Operational status of the given gate. True if the gate is operational, + False otherwise. + """ + properties = self.gate_property(gate, qubits) + if 'operational' in properties: + return bool(properties['operational'][0]) + return True # if property operational not existent, then True. + def gate_error(self, gate: str, qubits: Union[int, Iterable[int]]) -> float: """ Return gate error estimates from backend properties. @@ -416,6 +453,21 @@ def readout_error(self, qubit: int) -> float: """ return self.qubit_property(qubit, 'readout_error')[0] # Throw away datetime at index 1 + def is_qubit_operational(self, qubit: int) -> bool: + """ + Return the operational status of the given qubit. + + Args: + qubit: Qubit for which to return operational status of. + + Returns: + Operational status of the given qubit. + """ + properties = self.qubit_property(qubit) + if 'operational' in properties: + return bool(properties['operational'][0]) + return True # if property operational not existent, then True. + def _apply_prefix(self, value: float, unit: str) -> float: """ Given a SI unit prefix and value, apply the prefix to convert to diff --git a/qiskit/test/mock/fake_backend.py b/qiskit/test/mock/fake_backend.py index dcc5aae5aeaa..5b57813ab382 100644 --- a/qiskit/test/mock/fake_backend.py +++ b/qiskit/test/mock/fake_backend.py @@ -89,6 +89,12 @@ def properties(self): "name": "readout_error", "unit": "", "value": 0.0 + }, + { + "date": "2000-01-01 00:00:00Z", + "name": "operational", + "unit": "", + "value": 1 } ] for _ in range(len(unique_qubits)) ], diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 6272b8127d73..8773e1c91fd8 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -327,6 +327,10 @@ def from_grid(cls, num_rows, num_columns, bidirectional=True): cmap.add_edge(node, right) return cmap + def largest_connected_component(self): + """Return a set of qubits in the largest connected component.""" + return max(nx.strongly_connected_components(self.graph), key=len) + def __str__(self): """Return a string representation of the coupling graph.""" string = "" diff --git a/test/python/providers/faulty_backends.py b/test/python/providers/faulty_backends.py new file mode 100644 index 000000000000..453b89fa5c93 --- /dev/null +++ b/test/python/providers/faulty_backends.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Faulty fake backends for testing""" + +from qiskit.providers.models import BackendProperties +from qiskit.test.mock import FakeOurense + + +class FakeOurenseFaultyQ1(FakeOurense): + """A fake 5 qubit backend, with a faulty q1 + 0 ↔ (1) ↔ 3 ↔ 4 + ↕ + 2 + """ + + def properties(self): + """Returns a snapshot of device properties as recorded on 8/30/19. + Sets the qubit 1 as non-operational. + """ + props = super().properties().to_dict() + props['qubits'][1].append({"date": "2000-01-01 00:00:00Z", + "name": "operational", + "unit": "", + "value": 0}) + return BackendProperties.from_dict(props) + + +class FakeOurenseFaultyCX01(FakeOurense): + """A fake 5 qubit backend, with a faulty CX(Q1, Q3) + 0 (↔) 1 ↔ 3 ↔ 4 + ↕ + 2 + """ + + def properties(self): + """Returns a snapshot of device properties as recorded on 8/30/19. + Sets the gate CX(Q0, Q1) (and symmetric) as non-operational. + """ + props = super().properties().to_dict() + for gate in props['gates']: + if gate['gate'] == 'cx' and set(gate['qubits']) == set([0, 1]): + gate['parameters'].append({"date": "2000-01-01 00:00:00Z", + "name": "operational", + "unit": "", + "value": 0}) + return BackendProperties.from_dict(props) + + +class FakeOurenseFaultyCX13(FakeOurense): + """A fake 5 qubit backend, with a faulty CX(Q1, Q3) + 0 ↔ 1 (↔) 3 ↔ 4 + ↕ + 2 + """ + + def properties(self): + """Returns a snapshot of device properties as recorded on 8/30/19. + Sets the gate CX(Q1, Q3) (and symmetric) as non-operational. + """ + props = super().properties().to_dict() + for gate in props['gates']: + if gate['gate'] == 'cx' and set(gate['qubits']) == set([3, 1]): + gate['parameters'].append({"date": "2000-01-01 00:00:00Z", + "name": "operational", + "unit": "", + "value": 0}) + return BackendProperties.from_dict(props) diff --git a/test/python/providers/test_backendproperties.py b/test/python/providers/test_backendproperties.py index becf7d34a04c..91a0b0672eff 100644 --- a/test/python/providers/test_backendproperties.py +++ b/test/python/providers/test_backendproperties.py @@ -101,3 +101,7 @@ def test_apply_prefix(self): with self.assertRaises(BackendPropertyError): self.properties._apply_prefix(71.9500421005539, 'ws') + + def test_operational(self): + """Test operation status of a given qubit.""" + self.assertTrue(self.properties.is_qubit_operational(0)) diff --git a/test/python/providers/test_faulty_backend.py b/test/python/providers/test_faulty_backend.py new file mode 100644 index 000000000000..e727020af468 --- /dev/null +++ b/test/python/providers/test_faulty_backend.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Testing a Faulty Ourense Backend.""" + +from qiskit.test import QiskitTestCase +from .faulty_backends import FakeOurenseFaultyCX01, FakeOurenseFaultyQ1, FakeOurenseFaultyCX13 + + +class FaultyQubitBackendTestCase(QiskitTestCase): + """Test operational-related methods of backend.properties() with FakeOurenseFaultyQ1, + which is like FakeOurense but with a faulty 1Q""" + + backend = FakeOurenseFaultyQ1() + + def test_operational_false(self): + """Test operation status of the qubit. Q1 is non-operational """ + self.assertFalse(self.backend.properties().is_qubit_operational(1)) + + def test_faulty_qubits(self): + """Test faulty_qubits method. """ + self.assertEqual(self.backend.properties().faulty_qubits(), [1]) + + +class FaultyGate13BackendTestCase(QiskitTestCase): + """Test operational-related methods of backend.properties() with FakeOurenseFaultyCX13, + which is like FakeOurense but with a faulty CX(Q1, Q3) and symmetric.""" + + backend = FakeOurenseFaultyCX13() + + def test_operational_gate(self): + """Test is_gate_operational method. """ + self.assertFalse(self.backend.properties().is_gate_operational('cx', [1, 3])) + self.assertFalse(self.backend.properties().is_gate_operational('cx', [3, 1])) + + def test_faulty_gates(self): + """Test faulty_gates method. """ + gates = self.backend.properties().faulty_gates() + self.assertEqual(len(gates), 2) + self.assertEqual([gate.gate for gate in gates], ['cx', 'cx']) + self.assertEqual([gate.qubits for gate in gates], [[1, 3], [3, 1]]) + + +class FaultyGate01BackendTestCase(QiskitTestCase): + """Test operational-related methods of backend.properties() with FakeOurenseFaultyCX13, + which is like FakeOurense but with a faulty CX(Q1, Q3) and symmetric.""" + backend = FakeOurenseFaultyCX01() + + def test_operational_gate(self): + """Test is_gate_operational method. """ + self.assertFalse(self.backend.properties().is_gate_operational('cx', [0, 1])) + self.assertFalse(self.backend.properties().is_gate_operational('cx', [1, 0])) + + def test_faulty_gates(self): + """Test faulty_gates method. """ + gates = self.backend.properties().faulty_gates() + self.assertEqual(len(gates), 2) + self.assertEqual([gate.gate for gate in gates], ['cx', 'cx']) + self.assertEqual([gate.qubits for gate in gates], [[0, 1], [1, 0]]) diff --git a/test/python/transpiler/test_faulty_backend.py b/test/python/transpiler/test_faulty_backend.py new file mode 100644 index 000000000000..eeb69787535f --- /dev/null +++ b/test/python/transpiler/test_faulty_backend.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests preset pass manager whith faulty backends""" + +from ddt import ddt, data + +from qiskit import QuantumCircuit, QuantumRegister, BasicAer, execute +from qiskit.compiler import transpile +from qiskit.test import QiskitTestCase +from qiskit.converters import circuit_to_dag +from qiskit.extensions.standard import CnotGate +from qiskit.transpiler import TranspilerError +from ..providers.faulty_backends import FakeOurenseFaultyQ1, FakeOurenseFaultyCX01,\ + FakeOurenseFaultyCX13 + + +class TestFaultyBackendCase(QiskitTestCase): + """Base TestCase for testing transpilation if faulty backends.""" + + def assertEqualCount(self, circuit1, circuit2): + """Asserts circuit1 and circuit2 has the same result counts after execution in BasicAer""" + backend = BasicAer.get_backend('qasm_simulator') + shots = 2048 + + result1 = execute(circuit1, backend, + basis_gates=['u1', 'u2', 'u3', 'id', 'cx'], + seed_simulator=0, seed_transpiler=0, shots=shots).result().get_counts() + + result2 = execute(circuit2, backend, + basis_gates=['u1', 'u2', 'u3', 'id', 'cx'], + seed_simulator=0, seed_transpiler=0, shots=shots).result().get_counts() + + for key in set(result1.keys()).union(result2.keys()): + with self.subTest(key=key): + diff = abs(result1.get(key, 0) - result2.get(key, 0)) + self.assertLess(diff / shots * 100, 2.5) + + +@ddt +class TestFaultyCX01(TestFaultyBackendCase): + """Test preset passmanagers with FakeOurenseFaultyCX01 + A fake 5 qubit backend, with a faulty CX(Q0, Q1) (and symmetric). + 0 (↔) 1 ↔ 3 ↔ 4 + ↕ + 2 + """ + + def assertIdleCX01(self, circuit): + """Asserts the CX(0, 1) (and symmetric) is not used in the circuit""" + physical_qubits = QuantumRegister(5, 'q') + cx_nodes = circuit_to_dag(circuit).op_nodes(CnotGate) + for node in cx_nodes: + if set(node.qargs) == {physical_qubits[0], physical_qubits[1]}: + raise AssertionError('Faulty CX(Q0, Q1) (or symmetric) is being used.') + + @data(0, 1, 2, 3) + def test_level(self, level): + """Test level {level} Ourense backend with a faulty CX(Q0, Q1) """ + circuit = QuantumCircuit(QuantumRegister(4, 'qr')) + circuit.h(range(4)) + circuit.ccx(0, 1, 2) + circuit.measure_all() + result = transpile(circuit, + backend=FakeOurenseFaultyCX01(), + optimization_level=level, + seed_transpiler=42) + + self.assertIdleCX01(result) + self.assertEqualCount(circuit, result) + + @data(0, 1, 2, 3) + def test_layout_level(self, level): + """Test level {level} with a faulty CX(Q0, Q1) with a working initial layout""" + circuit = QuantumCircuit(QuantumRegister(4, 'qr')) + circuit.h(range(4)) + circuit.ccx(0, 1, 2) + circuit.measure_all() + result = transpile(circuit, + backend=FakeOurenseFaultyCX01(), + optimization_level=level, + initial_layout=[3, 4, 1, 2], + seed_transpiler=42) + + self.assertIdleCX01(result) + self.assertEqualCount(circuit, result) + + @data(0, 1, 2, 3) + def test_failing_layout_level(self, level): + """Test level {level} with a faulty CX(Q0, Q1) with a failing initial layout. Raises.""" + circuit = QuantumCircuit(QuantumRegister(4, 'qr')) + circuit.h(range(4)) + circuit.ccx(0, 1, 2) + circuit.measure_all() + + message = 'The initial_layout parameter refers to faulty or disconnected qubits' + + with self.assertRaises(TranspilerError) as context: + transpile(circuit, backend=FakeOurenseFaultyCX01(), + optimization_level=level, + initial_layout=[0, 4, 1, 2], + seed_transpiler=42) + + self.assertEqual(context.exception.message, message) + + +@ddt +class TestFaultyCX13(TestFaultyBackendCase): + """Test preset passmanagers with FakeOurenseFaultyCX13 + A fake 5 qubit backend, with a faulty CX(Q1, Q3) (and symmetric). + 0 ↔ 1 (↔) 3 ↔ 4 + ↕ + 2 + """ + + def assertIdleCX13(self, circuit): + """Asserts the CX(1, 3) (and symmetric) is not used in the circuit""" + physical_qubits = QuantumRegister(5, 'q') + cx_nodes = circuit_to_dag(circuit).op_nodes(CnotGate) + for node in cx_nodes: + if set(node.qargs) == {physical_qubits[1], physical_qubits[3]}: + raise AssertionError('Faulty CX(Q1, Q3) (or symmetric) is being used.') + + @data(0, 1, 2, 3) + def test_level(self, level): + """Test level {level} Ourense backend with a faulty CX(Q1, Q3) """ + circuit = QuantumCircuit(QuantumRegister(3, 'qr')) + circuit.h(range(3)) + circuit.ccx(0, 1, 2) + circuit.measure_all() + result = transpile(circuit, + backend=FakeOurenseFaultyCX13(), + optimization_level=level, + seed_transpiler=42) + + self.assertIdleCX13(result) + self.assertEqualCount(circuit, result) + + @data(0, 1, 2, 3) + def test_layout_level(self, level): + """Test level {level} with a faulty CX(Q1, Q3) with a working initial layout""" + circuit = QuantumCircuit(QuantumRegister(3, 'qr')) + circuit.h(range(3)) + circuit.ccx(0, 1, 2) + circuit.measure_all() + result = transpile(circuit, + backend=FakeOurenseFaultyCX13(), + optimization_level=level, + initial_layout=[0, 2, 1], + seed_transpiler=42) + + self.assertIdleCX13(result) + self.assertEqualCount(circuit, result) + + @data(0, 1, 2, 3) + def test_failing_layout_level(self, level): + """Test level {level} with a faulty CX(Q1, Q3) with a failing initial layout. Raises.""" + circuit = QuantumCircuit(QuantumRegister(3, 'qr')) + circuit.h(range(3)) + circuit.ccx(0, 1, 2) + circuit.measure_all() + + message = 'The initial_layout parameter refers to faulty or disconnected qubits' + + with self.assertRaises(TranspilerError) as context: + transpile(circuit, backend=FakeOurenseFaultyCX13(), + optimization_level=level, + initial_layout=[0, 1, 3], + seed_transpiler=42) + + self.assertEqual(context.exception.message, message) + + +@ddt +class TestFaultyQ1(TestFaultyBackendCase): + """Test preset passmanagers with FakeOurenseFaultyQ1. + A 5 qubit backend, with a faulty q1 + 0 ↔ (1) ↔ 3 ↔ 4 + ↕ + 2 + """ + + def assertIdleQ1(self, circuit): + """Asserts the Q1 in circuit is not used with operations""" + physical_qubits = QuantumRegister(5, 'q') + nodes = circuit_to_dag(circuit).nodes_on_wire(physical_qubits[1]) + for node in nodes: + if node.type == 'op': + raise AssertionError('Faulty Qubit Q1 not totally idle') + + @data(0, 1, 2, 3) + def test_level(self, level): + """Test level {level} Ourense backend with a faulty Q1 """ + circuit = QuantumCircuit(QuantumRegister(2, 'qr')) + circuit.h(range(2)) + circuit.cz(0, 1) + circuit.measure_all() + result = transpile(circuit, backend=FakeOurenseFaultyQ1(), + optimization_level=level, + seed_transpiler=42) + + self.assertIdleQ1(result) + self.assertEqualCount(circuit, result) + + @data(0, 1, 2, 3) + def test_layout_level(self, level): + """Test level {level} with a faulty Q1 with a working initial layout""" + circuit = QuantumCircuit(QuantumRegister(2, 'qr')) + circuit.h(range(2)) + circuit.cz(0, 1) + circuit.measure_all() + result = transpile(circuit, backend=FakeOurenseFaultyQ1(), + optimization_level=level, + initial_layout=[4, 3], + seed_transpiler=42) + + self.assertIdleQ1(result) + self.assertEqualCount(circuit, result) + + @data(0, 1, 2, 3) + def test_failing_layout_level(self, level): + """Test level {level} with a faulty Q1 with a failing initial layout. Raises.""" + circuit = QuantumCircuit(QuantumRegister(2, 'qr')) + circuit.h(range(2)) + circuit.cz(0, 1) + circuit.measure_all() + + message = 'The initial_layout parameter refers to faulty or disconnected qubits' + + with self.assertRaises(TranspilerError) as context: + transpile(circuit, backend=FakeOurenseFaultyQ1(), + optimization_level=level, + initial_layout=[4, 0], + seed_transpiler=42) + + self.assertEqual(context.exception.message, message)