diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py index 04b4d275a6..1a9a376ddc 100644 --- a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py +++ b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py @@ -12,12 +12,12 @@ """ Utilities for using the Clifford group in randomized benchmarking """ - +import copy import itertools import os from functools import lru_cache from numbers import Integral -from typing import Optional, Union, Tuple, Sequence +from typing import Optional, Union, Tuple, Sequence, FrozenSet import numpy as np import scipy.sparse @@ -30,6 +30,7 @@ from qiskit.compiler import transpile from qiskit.exceptions import QiskitError from qiskit.quantum_info import Clifford, random_clifford +from qiskit.transpiler import Target from qiskit_experiments.warnings import deprecated_function @@ -100,40 +101,83 @@ def _circuit_compose( return self -def _truncate_inactive_qubits( - circ: QuantumCircuit, active_qubits: Sequence[Qubit] -) -> QuantumCircuit: - new_data = [] - for inst in circ: - if all(q in active_qubits for q in inst.qubits): - new_data.append(inst) - - res = QuantumCircuit(active_qubits, name=circ.name) - res._calibrations = circ.calibrations - res._data = new_data - res._metadata = circ.metadata - return res +class ReducedTarget(Target): + """ + A target class reduced to represent subsystem with specified physical qubits. + + Note that this class must be treated as an immutable class since it implements + ``__hash__`` function so that its object can be a cache key, + even though it is technically mutable. + Also note that this class may not contain some data necessary to schedule circuits + or transpile pulse gates, different from the parent Target class, and hence + it works only with normal circuits. + This class must be instantiated by reducing an original Target into physical qubits. + In the reduction, the qubits are remapped. That means, for example, when a Target is + reduced into physical qubits (3, 2), the resulting ReducedTarget will have + virtual qubits (0, 1): Qubit 3 is mapped to 0 and qubit 2 to 1. + """ + + def __init__(self, target: Target, physical_qubits: Tuple[int, ...]): + description = None + if target.description: + description = f"{target.description} reduced to qubits {physical_qubits}" + super().__init__( + description=description, + num_qubits=len(physical_qubits), + ) + supported_instructions = set() + for op_name, qargs_dic in target.items(): + new_prop_dic = {} + for qargs, inst_prop in qargs_dic.items(): + if qargs is None: + new_prop_dic[None] = None + supported_instructions.add((op_name, None)) + elif set(qargs).issubset(physical_qubits): + new_prop = copy.copy(inst_prop) + if new_prop and new_prop.calibration: + new_prop.calibration = None + reduced_qargs = tuple(physical_qubits.index(q) for q in qargs) + new_prop_dic[reduced_qargs] = new_prop + supported_instructions.add((op_name, reduced_qargs)) + if new_prop_dic: + super().add_instruction(target.operation_from_name(op_name), new_prop_dic) + self._supported_instructions = frozenset(supported_instructions) + + def __hash__(self): + return hash(self._supported_instructions) + + def __eq__(self, other): + return ( + isinstance(other, ReducedTarget) + and self._supported_instructions == other._supported_instructions + ) + + @property + def supported_instructions(self) -> FrozenSet[Tuple[str, Optional[Tuple[int, ...]]]]: + """Set of instructions supported in this target. + An instruction is a pair of operation name and qubit arguments, e.g. ("cx", (0, 1)). + """ + return self._supported_instructions + + def add_instruction(self, instruction, properties=None, name=None): + """Not supported for ReducedTarget (immutable)""" + raise NotImplementedError("Not supported for ReducedTarget (immutable).") -# TODO: Naming: transform? translate? synthesis? -def _transform_clifford_circuit(circuit: QuantumCircuit, basis_gates: Tuple[str]) -> QuantumCircuit: - # The function that synthesis clifford circuits with given basis gates, +def _translate_basis(circuit: QuantumCircuit, target: ReducedTarget) -> QuantumCircuit: + # The function that translates clifford circuits into those with gates defined in given target, # which should be commonly used during custom transpilation in the RB circuit generation. - return transpile(circuit, basis_gates=list(basis_gates), optimization_level=0) + return transpile(circuit, target=target, optimization_level=0) @lru_cache(maxsize=None) -def _clifford_1q_int_to_instruction( - num: Integral, basis_gates: Optional[Tuple[str]] -) -> Instruction: - return CliffordUtils.clifford_1_qubit_circuit(num, basis_gates).to_instruction() +def _clifford_1q_int_to_instruction(num: Integral, target: Optional[ReducedTarget]) -> Instruction: + return CliffordUtils.clifford_1_qubit_circuit(num, target).to_instruction() @lru_cache(maxsize=11520) -def _clifford_2q_int_to_instruction( - num: Integral, basis_gates: Optional[Tuple[str]] -) -> Instruction: - return CliffordUtils.clifford_2_qubit_circuit(num, basis_gates).to_instruction() +def _clifford_2q_int_to_instruction(num: Integral, target: Optional[ReducedTarget]) -> Instruction: + return CliffordUtils.clifford_2_qubit_circuit(num, target).to_instruction() # The classes VGate and WGate are not actually used in the code - we leave them here to give @@ -239,7 +283,7 @@ def random_clifford_circuits( @classmethod @lru_cache(maxsize=24) - def clifford_1_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str, ...]] = None): + def clifford_1_qubit_circuit(cls, num, target: Optional[ReducedTarget] = None): """Return the 1-qubit clifford circuit corresponding to `num` where `num` is between 0 and 23. """ @@ -259,21 +303,21 @@ def clifford_1_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str, ...]] = if p == 3: qc.z(0) - if basis_gates: - qc = _transform_clifford_circuit(qc, basis_gates) + if target: + qc = _translate_basis(qc, target) return qc @classmethod @lru_cache(maxsize=11520) - def clifford_2_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str, ...]] = None): + def clifford_2_qubit_circuit(cls, num, target: Optional[ReducedTarget] = None): """Return the 2-qubit clifford circuit corresponding to `num` where `num` is between 0 and 11519. """ qc = QuantumCircuit(2, name=f"Clifford-2Q({num})") for layer, idx in enumerate(_layer_indices_from_num(num)): - if basis_gates: - layer_circ = _transformed_clifford_layer(layer, idx, basis_gates) + if target: + layer_circ = _transformed_clifford_layer(layer, idx, target) else: layer_circ = _CLIFFORD_LAYER[layer][idx] # qc.compose(layer_circ, inplace=True) @@ -577,11 +621,11 @@ def _create_cliff_2q_layer_2(): @lru_cache(maxsize=None) def _transformed_clifford_layer( - layer: int, index: Integral, basis_gates: Tuple[str, ...] + layer: int, index: Integral, target: ReducedTarget ) -> QuantumCircuit: - # Return the index-th quantum circuit of the layer translated with the basis_gates. + # Return the index-th quantum circuit of the layer translated with the target. # The result is cached for speed. - return _transform_clifford_circuit(_CLIFFORD_LAYER[layer][index], basis_gates) + return _translate_basis(_CLIFFORD_LAYER[layer][index], target) def _num_from_layer_indices(triplet: Tuple[Integral, Integral, Integral]) -> Integral: diff --git a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py index 35441621ca..e4c8a8bfd1 100644 --- a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py @@ -13,19 +13,17 @@ Interleaved RB Experiment class. """ import warnings -from typing import Union, Iterable, Optional, List, Sequence, Tuple +from typing import Union, Iterable, Optional, List, Sequence from numpy.random import Generator 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.backend_timing import BackendTiming -from .clifford_utils import _truncate_inactive_qubits +from .clifford_utils import _translate_basis, ReducedTarget from .clifford_utils import num_from_1q_circuit, num_from_2q_circuit from .interleaved_rb_analysis import InterleavedRBAnalysis from .rb_experiment import StandardRB, SequenceElementType @@ -67,8 +65,8 @@ def __init__( 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. + If the element contains any gates not defined in the ``backend``, + it will be internally translated into that with gates supported in the backend. 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` @@ -192,16 +190,16 @@ def circuits(self) -> List[QuantumCircuit]: return reference_circuits + interleaved_circuits def _to_instruction( - self, elem: SequenceElementType, basis_gates: Optional[Tuple[str]] = None + self, elem: SequenceElementType, target: Optional[ReducedTarget] = None ) -> Instruction: if elem is self._interleaved_elem: return self._interleaved_op - return super()._to_instruction(elem, basis_gates) + return super()._to_instruction(elem, target) def __set_up_interleaved_op(self) -> None: # Convert interleaved element to transpiled circuit operation and store it for speed - basis_gates = self._get_basis_gates() + target = self._get_reduced_target() # Convert interleaved element to circuit if isinstance(self._interleaved_op, Clifford): self._interleaved_op = self._interleaved_op.to_circuit() @@ -214,18 +212,18 @@ def __set_up_interleaved_op(self) -> None: 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 + need_to_translate = False + if target: + for inst in interleaved_circ: + qargs = tuple(interleaved_circ.find_bit(q).index for q in inst.qubits) + if (inst.operation.name, qargs) not in target.supported_instructions: + need_to_translate = True + break + + if need_to_translate: + # Translate basis of circuit with target + interleaved_circ = _translate_basis(interleaved_circ, target) + # Convert translated circuit to operation if len(interleaved_circ) == 1: self._interleaved_op = interleaved_circ.data[0].operation else: diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index 53acc9a1c6..99426b26f3 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -15,7 +15,7 @@ import logging from collections import defaultdict from numbers import Integral -from typing import Union, Iterable, Optional, List, Sequence, Tuple +from typing import Union, Iterable, Optional, List, Sequence import numpy as np from numpy.random import Generator, default_rng @@ -32,6 +32,7 @@ from qiskit_experiments.framework.restless_mixin import RestlessMixin from .clifford_utils import ( CliffordUtils, + ReducedTarget, compose_1q, compose_2q, inverse_1q, @@ -140,7 +141,6 @@ def _default_experiment_options(cls) -> Options: seed=None, full_sampling=None, ) - return options def _set_backend(self, backend: Backend): @@ -191,32 +191,16 @@ def _sample_sequences(self) -> List[Sequence[SequenceElementType]]: return sequences - def _get_basis_gates(self) -> Optional[Tuple[str, ...]]: - """Get sorted basis gates to use in basis transformation during circuit generation. + def _get_reduced_target(self) -> Optional[ReducedTarget]: + """Get a reduced target to use in basis transformation during circuit generation. Returns: - Sorted basis gate names. + Reduced target or None if there is no corresponding target. """ - 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 + if isinstance(self.backend, BackendV2) and self.backend.target is not None: + return ReducedTarget(self.backend.target, self.physical_qubits) + + return None def _sequences_to_circuits( self, sequences: List[Sequence[SequenceElementType]] @@ -226,7 +210,7 @@ def _sequences_to_circuits( Returns: A list of RB circuits. """ - basis_gates = self._get_basis_gates() + target = self._get_reduced_target() # Circuit generation circuits = [] for i, seq in enumerate(sequences): @@ -239,7 +223,7 @@ def _sequences_to_circuits( circ = QuantumCircuit(self.num_qubits) circ.append(Barrier(self.num_qubits), circ.qubits) for elem in seq: - circ.append(self._to_instruction(elem, basis_gates), circ.qubits) + circ.append(self._to_instruction(elem, target), circ.qubits) circ.append(Barrier(self.num_qubits), circ.qubits) # Compute inverse, compute only the difference from the previous shorter sequence @@ -247,7 +231,7 @@ def _sequences_to_circuits( prev_seq = seq inv = self.__adjoint_clifford(prev_elem) - circ.append(self._to_instruction(inv, basis_gates), circ.qubits) + circ.append(self._to_instruction(inv, target), circ.qubits) circ.measure_all() # includes insertion of the barrier before measurement circuits.append(circ) return circuits @@ -264,14 +248,14 @@ def __sample_sequence(self, length: int, rng: Generator) -> Sequence[SequenceEle return [random_clifford(self.num_qubits, rng).to_circuit() for _ in range(length)] def _to_instruction( - self, elem: SequenceElementType, basis_gates: Optional[Tuple[str, ...]] = None + self, elem: SequenceElementType, target: Optional[ReducedTarget] = None ) -> Instruction: # Switching for speed up if isinstance(elem, Integral): if self.num_qubits == 1: - return _clifford_1q_int_to_instruction(elem, basis_gates) + return _clifford_1q_int_to_instruction(elem, target) if self.num_qubits == 2: - return _clifford_2q_int_to_instruction(elem, basis_gates) + return _clifford_2q_int_to_instruction(elem, target) return elem.to_instruction() @@ -310,10 +294,7 @@ def __adjoint_clifford(self, op: SequenceElementType) -> SequenceElementType: def _transpiled_circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits, transpiled.""" has_custom_transpile_option = ( - any( - opt not in {"basis_gates", "optimization_level"} - for opt in vars(self.transpile_options) - ) + any(opt != "optimization_level" for opt in vars(self.transpile_options)) or self.transpile_options.get("optimization_level", 0) != 0 ) if self.num_qubits > 2 or has_custom_transpile_option: 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."""