diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index 53acc9a1c6..79160d15e7 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -28,6 +28,7 @@ from qiskit.pulse.instruction_schedule_map import CalibrationPublisher from qiskit.quantum_info import Clifford from qiskit.quantum_info.random import random_clifford +from qiskit.transpiler import CouplingMap from qiskit_experiments.framework import BaseExperiment, Options from qiskit_experiments.framework.restless_mixin import RestlessMixin from .clifford_utils import ( @@ -194,29 +195,68 @@ def _sample_sequences(self) -> List[Sequence[SequenceElementType]]: def _get_basis_gates(self) -> Optional[Tuple[str, ...]]: """Get sorted basis gates to use in basis transformation during circuit generation. + - Return None if this experiment is an RB with 3 or more qubits. + - Return None if no basis gates are supplied via ``backend`` or ``transpile_options``. + - Return None if all 2q-gates supported on the physical qubits of the backend are one-way + directed (e.g. cx(0, 1) is supported but cx(1, 0) is not supported). + + In all those case when None are returned, basis transformation will be skipped in the + circuit generation step (i.e. :meth:`circuits`) and it will be done in the successive + transpilation step (i.e. :meth:`_transpiled_circuits`) that calls :func:`transpile`. + Returns: Sorted basis gate names. """ - basis_gates = self.transpile_options.get("basis_gates", None) - if not basis_gates and self.backend: - if isinstance(self.backend, BackendV2): - # Only the "global basis gates" are returned for v2 backend. - # Some non-global basis gates may be usable for some physical qubits. However, - # they are conservatively removed here because the basis gates are agnostic to - # the direction of each gate. - basis_gates = self.backend.operation_names - non_globals = self.backend.target.get_non_global_operation_names( - strict_direction=True - ) - if non_globals: - basis_gates = set(basis_gates) - set(non_globals) - else: - basis_gates = self.backend.configuration().basis_gates - - if basis_gates is not None: - basis_gates = tuple(sorted(basis_gates)) - - return basis_gates + # 3 or more qubits case: Return None (skip basis transformation in circuit generation) + if self.num_qubits > 2: + return None + + # 1 qubit case: Return all basis gates (or None if no basis gates are supplied) + if self.num_qubits == 1: + basis_gates = self.transpile_options.get("basis_gates", None) + if not basis_gates and self.backend: + if isinstance(self.backend, BackendV2): + basis_gates = self.backend.operation_names + elif isinstance(self.backend, BackendV1): + basis_gates = self.backend.configuration().basis_gates + return tuple(sorted(basis_gates)) if basis_gates else None + + # 2 qubits case: Return all basis gates except for one-way directed 2q-gates. + # Return None if there is no bi-directed 2q-gates in basis gates. + if self.num_qubits == 2: + basis_gates = self.transpile_options.get("basis_gates", None) + if not basis_gates and self.backend: + if isinstance(self.backend, BackendV2) and self.backend.target: + # prepare to collect one-way directed 2q-gates + supported_2q_instructions = defaultdict(list) # key: op_name, value: qargs list + for op_name, qargs_dic in self.backend.target.items(): + for qargs in qargs_dic: + if self.backend.target.operation_from_name(op_name).num_qubits != 2: + continue + if qargs is None: # the 2q-gate is not available on the qargs + supported_2q_instructions[op_name] = [] + elif set(qargs).issubset(self.physical_qubits): + reduced_qargs = tuple(self.physical_qubits.index(q) for q in qargs) + supported_2q_instructions[op_name].append(reduced_qargs) + # collect one-way directed 2q-gates + directed_basis_2q_gates = set() # one-way directed 2q-gates + for op_name, qargs_list in supported_2q_instructions.items(): + if len(qargs_list) == 1: + directed_basis_2q_gates.add(op_name) + if len(directed_basis_2q_gates) == len(supported_2q_instructions): + return None # supported 2q-gates are all directed + # all basis gates except for one-way directed 2q-gates + basis_gates = set(self.backend.operation_names) - directed_basis_2q_gates + elif isinstance(self.backend, BackendV1): + coupling_map = self.backend.configuration().coupling_map + if coupling_map: + coupling = CouplingMap(coupling_map).reduce(self.physical_qubits) + if len(coupling.get_edges()) == 1: + return None # supported 2q-gates are all directed + basis_gates = self.backend.configuration().basis_gates + return tuple(sorted(basis_gates)) if basis_gates else None + + return None def _sequences_to_circuits( self, sequences: List[Sequence[SequenceElementType]] @@ -316,7 +356,8 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]: ) or self.transpile_options.get("optimization_level", 0) != 0 ) - if self.num_qubits > 2 or has_custom_transpile_option: + has_no_undirected_2q_basis = self._get_basis_gates() is None + if self.num_qubits > 2 or has_custom_transpile_option or has_no_undirected_2q_basis: transpiled = super()._transpiled_circuits() else: transpiled = [ diff --git a/test/library/randomized_benchmarking/test_randomized_benchmarking.py b/test/library/randomized_benchmarking/test_randomized_benchmarking.py index 5833db0a6d..d7605c7a46 100644 --- a/test/library/randomized_benchmarking/test_randomized_benchmarking.py +++ b/test/library/randomized_benchmarking/test_randomized_benchmarking.py @@ -191,6 +191,20 @@ def test_calibrations_via_custom_backend(self): self.assertTrue(qc.has_calibration_for((SXGate(), [qc.qubits[q] for q in qubits], []))) self.assertEqual(qc.calibrations["sx"][(qubits, tuple())], my_sched) + def test_backend_with_directed_basis_gates(self): + """Test if correct circuits are generated from backend with directed basis gates.""" + my_backend = copy.deepcopy(self.backend) + del my_backend.target["cx"][(1, 2)] # make cx on {1, 2} one-sided + + exp = rb.StandardRB(qubits=(1, 2), lengths=[3], num_samples=4, backend=my_backend) + transpiled = exp._transpiled_circuits() + for qc in transpiled: + self.assertTrue(qc.count_ops().get("cx", 0) > 0) + expected_qubits = (qc.qubits[2], qc.qubits[1]) + for inst in qc: + if inst.operation.name == "cx": + self.assertEqual(inst.qubits, expected_qubits) + @ddt class TestInterleavedRB(QiskitExperimentsTestCase, RBTestMixin): @@ -392,6 +406,26 @@ def test_interleaved_circuit_is_decomposed(self): self.assertTrue(all(not inst.operation.name.startswith("circuit") for inst in qc)) self.assertTrue(all(not inst.operation.name.startswith("Clifford") for inst in qc)) + def test_interleaving_cnot_gate_with_non_supported_direction(self): + """Test if cx(0, 1) can be interleaved for backend that support only cx(1, 0).""" + my_backend = FakeManilaV2() + del my_backend.target["cx"][(0, 1)] # make support only cx(1, 0) + + exp = rb.InterleavedRB( + interleaved_element=CXGate(), + qubits=(0, 1), + lengths=[3], + num_samples=4, + backend=my_backend, + ) + transpiled = exp._transpiled_circuits() + for qc in transpiled: + self.assertTrue(qc.count_ops().get("cx", 0) > 0) + expected_qubits = (qc.qubits[1], qc.qubits[0]) + for inst in qc: + if inst.operation.name == "cx": + self.assertEqual(inst.qubits, expected_qubits) + class RBRunTestCase(QiskitExperimentsTestCase, RBTestMixin): """Base test case for running RB experiments defining a common noise model."""