diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py index f6ab757b5a..286b3a1b0f 100644 --- a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py +++ b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py @@ -110,23 +110,17 @@ def _circuit_compose( return self -def _truncate_inactive_qubits( - circ: QuantumCircuit, active_qubits: Sequence[Qubit] -) -> QuantumCircuit: - res = QuantumCircuit(active_qubits, name=circ.name, metadata=circ.metadata) - for inst in circ: - if all(q in active_qubits for q in inst.qubits): - res.append(inst) - res.calibrations = circ.calibrations - return res - - def _synthesize_clifford_circuit( circuit: QuantumCircuit, basis_gates: Tuple[str] ) -> QuantumCircuit: # synthesizes clifford circuits using given basis gates, for use during # custom transpilation during RB circuit generation. - return transpile(circuit, basis_gates=list(basis_gates), optimization_level=1) + return transpile( + circuit, + basis_gates=list(basis_gates), + coupling_map=[[0, 1]] if circuit.num_qubits == 2 else None, + optimization_level=1, + ) @lru_cache(maxsize=None) @@ -560,6 +554,7 @@ def _create_cliff_2q_layer_2(): _create_cliff_2q_layer_1(), _create_cliff_2q_layer_2(), ) +# _NUM_LAYER_0 = 36 _NUM_LAYER_1 = 20 _NUM_LAYER_2 = 16 diff --git a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py index 54746737ef..119ade411c 100644 --- a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py @@ -12,6 +12,7 @@ """ Interleaved RB Experiment class. """ +import copy import itertools import warnings from typing import Union, Iterable, Optional, List, Sequence, Tuple @@ -20,14 +21,11 @@ from numpy.random.bit_generator import BitGenerator, SeedSequence from qiskit.circuit import QuantumCircuit, Instruction, Gate, Delay -from qiskit.compiler import transpile from qiskit.exceptions import QiskitError from qiskit.providers.backend import Backend from qiskit.quantum_info import Clifford -from qiskit.transpiler.exceptions import TranspilerError from qiskit_experiments.framework import Options from qiskit_experiments.framework.backend_timing import BackendTiming -from .clifford_utils import _truncate_inactive_qubits from .clifford_utils import num_from_1q_circuit, num_from_2q_circuit from .interleaved_rb_analysis import InterleavedRBAnalysis from .standard_rb import StandardRB, SequenceElementType @@ -59,7 +57,7 @@ class InterleavedRB(StandardRB): def __init__( self, - interleaved_element: Union[QuantumCircuit, Gate, Delay, Clifford], + interleaved_element: Union[QuantumCircuit, Gate, Delay], physical_qubits: Sequence[int], lengths: Iterable[int], backend: Optional[Backend] = None, @@ -71,10 +69,9 @@ def __init__( """Initialize an interleaved randomized benchmarking experiment. Args: - interleaved_element: The element to interleave, - given either as a Clifford element, gate, delay or circuit. - If the element contains any non-basis gates, - it will be transpiled with ``transpiled_options`` of this experiment. + interleaved_element: The Clifford element to interleave, + given either as a gate, delay or circuit. + It must consists only of basis gates if a backend is supplied. If it is/contains a delay, its duration and unit must comply with the timing constraints of the ``backend`` (:class:`~qiskit_experiments.framework.backend_timing.BackendTiming` @@ -186,8 +183,7 @@ def circuits(self) -> List[QuantumCircuit]: A list of :class:`QuantumCircuit`. Raises: - QiskitError: If the ``interleaved_element`` provided to the constructor - cannot be transpiled. + QiskitError: If ``interleaved_element`` contains any gate not in backend's basis gates. """ # Convert interleaved element to transpiled circuit operation and store it for speed self.__set_up_interleaved_op() @@ -233,37 +229,16 @@ def _to_instruction( return super()._to_instruction(elem, basis_gates) def __set_up_interleaved_op(self) -> None: - # Convert interleaved element to transpiled circuit operation and store it for speed - self._interleaved_op = self._interleaved_element + self._interleaved_op = copy.deepcopy(self._interleaved_element) + # Validate if interleaved element consists only of basis gates basis_gates = self._get_basis_gates() - # Convert interleaved element to circuit - if isinstance(self._interleaved_op, Clifford): - self._interleaved_op = self._interleaved_op.to_circuit() - - if isinstance(self._interleaved_op, QuantumCircuit): - interleaved_circ = self._interleaved_op - elif isinstance(self._interleaved_op, Gate): - interleaved_circ = QuantumCircuit(self.num_qubits, name=self._interleaved_op.name) - interleaved_circ.append(self._interleaved_op, list(range(self.num_qubits))) - else: # Delay - interleaved_circ = [] - - if basis_gates and any(i.operation.name not in basis_gates for i in interleaved_circ): - # Transpile circuit with non-basis gates and remove idling qubits - try: - interleaved_circ = transpile( - interleaved_circ, self.backend, **vars(self.transpile_options) - ) - except TranspilerError as err: - raise QiskitError("Failed to transpile interleaved_element.") from err - interleaved_circ = _truncate_inactive_qubits( - interleaved_circ, active_qubits=interleaved_circ.qubits[: self.num_qubits] - ) - # Convert transpiled circuit to operation - if len(interleaved_circ) == 1: - self._interleaved_op = interleaved_circ.data[0].operation - else: - self._interleaved_op = interleaved_circ + if basis_gates: + ops = [self._interleaved_op] + if isinstance(self._interleaved_op, QuantumCircuit): + ops = [i.operation for i in self._interleaved_op] + for op in ops: + if op.name not in basis_gates: + raise QiskitError(f"interleaved_element contains a non-basis gate: {op.name}") # Store interleaved operation as Instruction if isinstance(self._interleaved_op, QuantumCircuit): diff --git a/qiskit_experiments/library/randomized_benchmarking/standard_rb.py b/qiskit_experiments/library/randomized_benchmarking/standard_rb.py index 3da209f07a..421c2e802c 100644 --- a/qiskit_experiments/library/randomized_benchmarking/standard_rb.py +++ b/qiskit_experiments/library/randomized_benchmarking/standard_rb.py @@ -216,8 +216,6 @@ def _get_basis_gates(self) -> Optional[Tuple[str, ...]]: - 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 @@ -225,6 +223,9 @@ def _get_basis_gates(self) -> Optional[Tuple[str, ...]]: Returns: Sorted basis gate names. + + Raises: + QiskitError: If ``backend`` has no 2q-basis-gate on ``physical_qubits`` in 2Q RB case. """ # 3 or more qubits case: Return None (skip basis transformation in circuit generation) if self.num_qubits > 2: @@ -236,30 +237,34 @@ def _get_basis_gates(self) -> Optional[Tuple[str, ...]]: if not basis_gates and self.backend: if isinstance(self.backend, BackendV2): basis_gates = self.backend.operation_names - elif isinstance(self.backend, BackendV1): + elif isinstance(self.backend, BackendV1): # TODO: remove after BackendV2 simulator basis_gates = self.backend.configuration().basis_gates return tuple(sorted(basis_gates)) if basis_gates else None def is_bidirectional(coupling_map): return len(coupling_map.reduce(self.physical_qubits).get_edges()) == 2 - # 2 qubits case: Return all basis gates except for one-way directed 2q-gates. - # Return None if there is no bidirectional 2q-gates in basis gates. + # 2 qubits case: Return all basis gates except for one-way directed 2q-gate + # not defined on the physical qubits. if self.num_qubits == 2: basis_gates = self.transpile_options.get("basis_gates", []) if not basis_gates and self.backend: if isinstance(self.backend, BackendV2) and self.backend.target: - has_bidirectional_2q_gates = False + has_2q_gate = False for op_name in self.backend.target: if self.backend.target.operation_from_name(op_name).num_qubits == 2: - if is_bidirectional(self.backend.target.build_coupling_map(op_name)): - has_bidirectional_2q_gates = True - else: - continue - basis_gates.append(op_name) - if not has_bidirectional_2q_gates: - basis_gates = None - elif isinstance(self.backend, BackendV1): + op_coupling = self.backend.target.build_coupling_map(op_name) + if self.physical_qubits in op_coupling.get_edges(): + basis_gates.append(op_name) + has_2q_gate = True + else: + basis_gates.append(op_name) + if not has_2q_gate: + raise QiskitError( + f"Backend {self.backend.name} has no 2q-basis-gate on " + f"physical qubit {self.physical_qubits}." + ) + elif isinstance(self.backend, BackendV1): # TODO: remove after BackendV2 simulator cmap = self.backend.configuration().coupling_map if cmap is None or is_bidirectional(CouplingMap(cmap)): basis_gates = self.backend.configuration().basis_gates diff --git a/test/library/randomized_benchmarking/test_interleaved_rb.py b/test/library/randomized_benchmarking/test_interleaved_rb.py index 6ed28d0f53..c573d0538b 100644 --- a/test/library/randomized_benchmarking/test_interleaved_rb.py +++ b/test/library/randomized_benchmarking/test_interleaved_rb.py @@ -266,15 +266,15 @@ def test_interleaved_circuit_is_decomposed(self): 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).""" + """Test if, for backend that support only cx(1, 0), cx(1, 0) can be interleaved + but cx(0, 1) cannot.""" my_backend = FakeManilaV2() del my_backend.target["cx"][(0, 1)] # make support only cx(1, 0) exp = rb.InterleavedRB( interleaved_element=CXGate(), - physical_qubits=(0, 1), + physical_qubits=(1, 0), # supported lengths=[3], - num_samples=4, backend=my_backend, seed=1234, ) @@ -286,6 +286,69 @@ def test_interleaving_cnot_gate_with_non_supported_direction(self): if inst.operation.name == "cx": self.assertEqual(inst.qubits, expected_qubits) + exp = rb.InterleavedRB( + interleaved_element=CXGate(), + physical_qubits=(0, 1), # not supported + lengths=[3], + backend=my_backend, + ) + with self.assertRaises(QiskitError): + exp._transpiled_circuits() + + def test_interleaving_circuit_with_gates_with_non_supported_direction(self): + """Test if, for backend that support only cx(2, 1), + circuit only with cx(2, 1) can be interleaved but circuit with cx(1, 2) cannot.""" + my_backend = FakeManilaV2() + del my_backend.target["cx"][(1, 2)] # make support only cx(2, 1) + + circ = QuantumCircuit(2) + circ.sx(0) + circ.cx(0, 1) + exp = rb.InterleavedRB( + interleaved_element=circ, + physical_qubits=(2, 1), # supported + lengths=[3], + 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) + + exp = rb.InterleavedRB( + interleaved_element=circ, + physical_qubits=(1, 2), # not supported + lengths=[3], + backend=my_backend, + ) + with self.assertRaises(QiskitError): + exp._transpiled_circuits() + + def test_interleaved_element_contains_non_basis_gates(self): + """Raise if interleaved_element contains any non basis gate""" + with self.assertRaises(QiskitError): + rb.InterleavedRB( + interleaved_element=CZGate(), # CZ gate is not in Manila's basis gates + physical_qubits=[0, 1], + lengths=[1, 2, 3], + backend=self.backend, + ).circuits() + + qc_cnot = QuantumCircuit(2) + qc_cnot.h(1) + qc_cnot.cz(0, 1) + qc_cnot.h(1) + with self.assertRaises(QiskitError): + rb.InterleavedRB( + interleaved_element=qc_cnot, # Equivalent with CNOT but it contains CZ (non basis) + physical_qubits=[0, 1], + lengths=[1, 2, 3], + backend=self.backend, + ).circuits() + class TestRunInterleavedRB(QiskitExperimentsTestCase, RBTestMixin): """Test for running InterleavedRB.""" diff --git a/test/library/randomized_benchmarking/test_standard_rb.py b/test/library/randomized_benchmarking/test_standard_rb.py index 6370321a2e..1d2e2842d1 100644 --- a/test/library/randomized_benchmarking/test_standard_rb.py +++ b/test/library/randomized_benchmarking/test_standard_rb.py @@ -191,9 +191,9 @@ def test_calibrations_via_custom_backend(self): 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 + del my_backend.target["cx"][(1, 2)] # make support only cx(2, 1) - exp = rb.StandardRB(physical_qubits=(1, 2), lengths=[3], num_samples=4, backend=my_backend) + exp = rb.StandardRB(physical_qubits=(2, 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)