diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_data.py b/qiskit_experiments/library/randomized_benchmarking/clifford_data.py new file mode 100644 index 0000000000..295b1234c5 --- /dev/null +++ b/qiskit_experiments/library/randomized_benchmarking/clifford_data.py @@ -0,0 +1,269 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +""" +This file contains the Clifford group represented as integers. +In CLIFF_COMPOSE_DATA, (i, j): k represents Clifford(i).compose(clifford(j)) = Clifford(k). +Since retrieving a value from an array is more efficient than from a dict, therefore +we store only the results (k) in the array CLIFF_COMPOSE_DATA. +The index is computed in CliffordUtils.compose_num_with_clifford(). +Note that for the pairs (i, j), i can be any clifford, and j represents only the +1-gate Cliffords, as listed in CliffordUtils.general_cliff_list +""" + +CLIFF_COMPOSE_DATA = [ + 0, + 1, + 2, + 4, + 6, + 8, + 12, + 18, + 22, + 1, + 0, + 3, + 5, + 7, + 9, + 13, + 19, + 23, + 2, + 23, + 6, + 15, + 8, + 0, + 14, + 20, + 9, + 3, + 22, + 7, + 14, + 9, + 1, + 15, + 21, + 8, + 4, + 9, + 17, + 18, + 10, + 23, + 16, + 22, + 0, + 5, + 8, + 16, + 19, + 11, + 22, + 17, + 23, + 1, + 6, + 19, + 8, + 16, + 0, + 2, + 18, + 12, + 10, + 7, + 18, + 9, + 17, + 1, + 3, + 19, + 13, + 11, + 8, + 5, + 0, + 3, + 2, + 6, + 20, + 14, + 21, + 9, + 4, + 1, + 2, + 3, + 7, + 21, + 15, + 20, + 10, + 15, + 23, + 6, + 4, + 17, + 22, + 16, + 12, + 11, + 14, + 22, + 7, + 5, + 16, + 23, + 17, + 13, + 12, + 13, + 20, + 10, + 18, + 14, + 0, + 6, + 16, + 13, + 12, + 21, + 11, + 19, + 15, + 1, + 7, + 17, + 14, + 11, + 12, + 21, + 20, + 18, + 2, + 8, + 3, + 15, + 10, + 13, + 20, + 21, + 19, + 3, + 9, + 2, + 16, + 21, + 11, + 12, + 22, + 5, + 4, + 10, + 6, + 17, + 20, + 10, + 13, + 23, + 4, + 5, + 11, + 7, + 18, + 7, + 14, + 22, + 12, + 20, + 6, + 0, + 4, + 19, + 6, + 15, + 23, + 13, + 21, + 7, + 1, + 5, + 20, + 17, + 18, + 9, + 14, + 12, + 8, + 2, + 15, + 21, + 16, + 19, + 8, + 15, + 13, + 9, + 3, + 14, + 22, + 3, + 5, + 0, + 16, + 11, + 10, + 4, + 18, + 23, + 2, + 4, + 1, + 17, + 10, + 11, + 5, + 19, +] + +# In CLIFF_INVERSE_DATA, i: j represents Clifford(i).inverse = Clifford(j). +# Here too, we store only the inverse (j) in the array CLIFF_INVERSE_DATA. +CLIFF_INVERSE_DATA = [ + 0, + 1, + 8, + 5, + 22, + 3, + 6, + 19, + 2, + 23, + 10, + 15, + 12, + 13, + 14, + 11, + 16, + 21, + 18, + 7, + 20, + 17, + 4, + 9, +] diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py index a7756ec126..7757f8c517 100644 --- a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py +++ b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py @@ -13,14 +13,20 @@ Utilities for using the Clifford group in randomized benchmarking """ -from typing import Optional, Union +from typing import Optional, Union, List from functools import lru_cache +from math import isclose +import numpy as np from numpy.random import Generator, default_rng + from qiskit import QuantumCircuit, QuantumRegister -from qiskit.circuit import Gate +from qiskit.circuit import Gate, Instruction from qiskit.circuit.library import SdgGate, HGate, SGate, SXdgGate from qiskit.quantum_info import Clifford, random_clifford - +from qiskit.compiler import transpile +from qiskit.providers.aer import AerSimulator +from qiskit.exceptions import QiskitError +from .clifford_data import CLIFF_COMPOSE_DATA class VGate(Gate): """V Gate used in Clifford synthesis.""" @@ -64,21 +70,27 @@ class CliffordUtils: (2, 2, 3, 3, 3, 3, 4, 4), (2, 2, 3, 3, 4, 4), ] + GENERAL_CLIFF_LIST = ["id", "h", "sdg", "s", "x", "sx", "sxdg", "y", "z", "cx"] + TRANSPILED_CLIFF_LIST = ["sx", "rz", "cx"] + NUM_SINGLE_GATE_1_QUBIT_CLIFF = 9 - def clifford_1_qubit(self, num): + @classmethod + def clifford_1_qubit(cls, num): """Return the 1-qubit clifford element corresponding to `num` where `num` is between 0 and 23. """ - return Clifford(self.clifford_1_qubit_circuit(num), validate=False) + return Clifford(cls.clifford_1_qubit_circuit(num), validate=False) - def clifford_2_qubit(self, num): + @classmethod + def clifford_2_qubit(cls, num): """Return the 2-qubit clifford element corresponding to `num` where `num` is between 0 and 11519. """ - return Clifford(self.clifford_2_qubit_circuit(num), validate=False) + return Clifford(cls.clifford_2_qubit_circuit(num), validate=False) + @classmethod def random_cliffords( - self, num_qubits: int, size: int = 1, rng: Optional[Union[int, Generator]] = None + cls, num_qubits: int, size: int = 1, rng: Optional[Union[int, Generator]] = None ): """Generate a list of random clifford elements""" if num_qubits > 2: @@ -91,14 +103,15 @@ def random_cliffords( rng = default_rng(rng) if num_qubits == 1: - samples = rng.integers(24, size=size) - return [Clifford(self.clifford_1_qubit_circuit(i), validate=False) for i in samples] + samples = rng.integers(cls.NUM_CLIFFORD_1_QUBIT, size=size) + return [Clifford(cls.clifford_1_qubit_circuit(i), validate=False) for i in samples] else: samples = rng.integers(11520, size=size) - return [Clifford(self.clifford_2_qubit_circuit(i), validate=False) for i in samples] + return [Clifford(cls.clifford_2_qubit_circuit(i), validate=False) for i in samples] + @classmethod def random_clifford_circuits( - self, num_qubits: int, size: int = 1, rng: Optional[Union[int, Generator]] = None + cls, num_qubits: int, size: int = 1, rng: Optional[Union[int, Generator]] = None ): """Generate a list of random clifford circuits""" if num_qubits > 2: @@ -111,20 +124,21 @@ def random_clifford_circuits( rng = default_rng(rng) if num_qubits == 1: - samples = rng.integers(24, size=size) - return [self.clifford_1_qubit_circuit(i) for i in samples] + samples = rng.integers(cls.NUM_CLIFFORD_1_QUBIT, size=size) + return [cls.clifford_1_qubit_circuit(i) for i in samples] else: samples = rng.integers(11520, size=size) - return [self.clifford_2_qubit_circuit(i) for i in samples] + return [cls.clifford_2_qubit_circuit(i) for i in samples] + @classmethod @lru_cache(maxsize=24) - def clifford_1_qubit_circuit(self, num): + def clifford_1_qubit_circuit(cls, num): """Return the 1-qubit clifford circuit corresponding to `num` where `num` is between 0 and 23. """ # pylint: disable=unbalanced-tuple-unpacking # This is safe since `_unpack_num` returns list the size of the sig - (i, j, p) = self._unpack_num(num, self.CLIFFORD_1_QUBIT_SIG) + (i, j, p) = cls._unpack_num(num, cls.CLIFFORD_1_QUBIT_SIG) qr = QuantumRegister(1) qc = QuantumCircuit(qr) if i == 1: @@ -141,12 +155,13 @@ def clifford_1_qubit_circuit(self, num): qc.z(0) return qc + @classmethod @lru_cache(maxsize=11520) - def clifford_2_qubit_circuit(self, num): + def clifford_2_qubit_circuit(cls, num): """Return the 2-qubit clifford circuit corresponding to `num` where `num` is between 0 and 11519. """ - vals = self._unpack_num_multi_sigs(num, self.CLIFFORD_2_QUBIT_SIGS) + vals = cls._unpack_num_multi_sigs(num, cls.CLIFFORD_2_QUBIT_SIGS) qr = QuantumRegister(2) qc = QuantumCircuit(qr) if vals[0] == 0 or vals[0] == 3: @@ -195,7 +210,8 @@ def clifford_2_qubit_circuit(self, num): qc.z(1) return qc - def _unpack_num(self, num, sig): + @classmethod + def _unpack_num(cls, num, sig): r"""Returns a tuple :math:`(a_1, \ldots, a_n)` where :math:`0 \le a_i \le \sigma_i` where sig=:math:`(\sigma_1, \ldots, \sigma_n)` and num is the sequential @@ -207,7 +223,8 @@ def _unpack_num(self, num, sig): num //= k return res - def _unpack_num_multi_sigs(self, num, sigs): + @classmethod + def _unpack_num_multi_sigs(cls, num, sigs): """Returns the result of `_unpack_num` on one of the signatures in `sigs` """ @@ -216,6 +233,93 @@ def _unpack_num_multi_sigs(self, num, sigs): for k in sig: sig_size *= k if num < sig_size: - return [i] + self._unpack_num(num, sig) + return [i] + cls._unpack_num(num, sig) num -= sig_size return None + + @classmethod + def transpile_single_clifford(cls, cliff_circ: QuantumCircuit, basis_gates: List[str]): + """Transpile a single clifford circuit using basis_gates.""" + backend = AerSimulator() + return transpile(cliff_circ, backend, optimization_level=1, basis_gates=basis_gates) + + @classmethod + def generate_1q_transpiled_clifford_circuits(cls, basis_gates: List[str]): + """Generate all transpiled clifford circuits""" + transpiled_circs = [] + for num in range(0, cls.NUM_CLIFFORD_1_QUBIT): + circ = cls.clifford_1_qubit_circuit(num=num) + transpiled_circ = cls.transpile_single_clifford(circ, basis_gates) + transpiled_circs.append(transpiled_circ) + return transpiled_circs + + @classmethod + def num_from_1_qubit_clifford_single_gate(cls, inst: Instruction, + basis_gates: List[str]) -> int: + """ + This method does the reverse of clifford_1_qubit_circuit - + given a clifford, it returns the corresponding integer, with the mapping + defined in the above method. + The mapping is in the context of the basis_gates. Therefore, we define here + the possible supersets of basis gates, and verify that the given inst belong to + one of these sets. + """ + name = inst.name + + gates_with_delay = basis_gates.copy() + gates_with_delay.append("delay") + if not name in gates_with_delay: + raise QiskitError("Instruction {} is not in the basis gates".format(inst.name)) + if set(basis_gates).issubset(set(cls.GENERAL_CLIFF_LIST)): + num_dict = { + "id": 0, + "h": 1, + "sxdg": 2, + "s": 4, + "x": 6, + "sx": 8, + "y": 12, + "z": 18, + "sdg": 22, + "delay": 0, + } + return num_dict[name] + + if set(basis_gates).issubset(set(cls.TRANSPILED_CLIFF_LIST)): + if name == "sx": + return 8 + if name == "delay": + return 0 + if name == "rz": + # The next two are identical up to a phase, which makes no difference + # for the associated Cliffords + if isclose(inst.params[0], np.pi) or isclose(inst.params[0], -np.pi): + return 18 + if isclose(inst.params[0], np.pi / 2): + return 4 + if isclose(inst.params[0], -np.pi / 2): + return 22 + else: + raise QiskitError("wrong param {} for rz in clifford".format(inst.params[0])) + + raise QiskitError("Instruction {} could not be converted to Clifford gate".format(name)) + + @classmethod + def compose_num_with_clifford( + cls, composed_num: int, qc: QuantumCircuit, basis_gates: List[str] + ) -> int: + """Compose a number that represents a Clifford, with a single-gate Clifford, and return the + number that represents the resulting Clifford.""" + + # The numbers corresponding to single gate Cliffords are not in sequence - + # see num_from_1_qubit_clifford_single_gate. To compute the index in + # the array CLIFF_COMPOSE_DATA, we map the numbers to [0, 8]. + map_clifford_num_to_array_index = {0: 0, 1: 1, 2: 2, 4: 3, 6: 4, 8: 5, 12: 6, 18: 7, 22: 8} + for inst in qc: + num = cls.num_from_1_qubit_clifford_single_gate(inst=inst[0], basis_gates=basis_gates) + index = ( + cls.NUM_SINGLE_GATE_1_QUBIT_CLIFF * composed_num + + map_clifford_num_to_array_index[num] + ) + composed_num = CLIFF_COMPOSE_DATA[index] + return composed_num diff --git a/qiskit_experiments/library/randomized_benchmarking/create_clifford_map.py b/qiskit_experiments/library/randomized_benchmarking/create_clifford_map.py new file mode 100644 index 0000000000..61ee3d546b --- /dev/null +++ b/qiskit_experiments/library/randomized_benchmarking/create_clifford_map.py @@ -0,0 +1,67 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. +""" +This is a script used to create the data in clifford_data.py. +""" +from qiskit_experiments.library.randomized_benchmarking.clifford_utils import CliffordUtils + +def create_compose_map(): + """Creates the data in CLIFF_COMPOSE_DATA and CLIFF_INVERSE_DATA""" + num_to_cliff = {} + cliff_to_num = {} + + for i in range(CliffordUtils.NUM_CLIFFORD_1_QUBIT): + cliff = CliffordUtils.clifford_1_qubit(i) + num_to_cliff[i] = cliff + cliff_to_num[cliff.__repr__()] = i + + products = {} + + single_gate_clifford_mapping = { + "id": 0, + "h": 1, + "sxdg": 2, + "s": 4, + "x": 6, + "sx": 8, + "y": 12, + "z": 18, + "sdg": 22, + } + + for i in range(CliffordUtils.NUM_CLIFFORD_1_QUBIT): + cliff1 = num_to_cliff[i] + # for gate in single_gate_clifford_mapping.keys(): + for gate in single_gate_clifford_mapping: + cliff2 = num_to_cliff[single_gate_clifford_mapping[gate]] + cliff = cliff1.compose(cliff2) + products[i, single_gate_clifford_mapping[gate]] = cliff_to_num[cliff.__repr__()] + + invs = {} + for i in range(CliffordUtils.NUM_CLIFFORD_1_QUBIT): + cliff1 = num_to_cliff[i] + cliff = cliff1.adjoint() + invs[i] = cliff_to_num[cliff.__repr__()] + + print("CLIFF_COMPOSE_DATA = [") + for i in products: + print(f" {products[i]},") + print("]") + print() + + print("CLIFF_INVERSE_DATA = [") + for i in invs: + print(f" {invs[i]},") + print("]") + print() + +create_compose_map() diff --git a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py index bee0e31c2e..c2f4dc627a 100644 --- a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py @@ -14,7 +14,7 @@ """ from typing import Union, Iterable, Optional, List, Sequence -from numpy.random import Generator +from numpy.random import Generator, default_rng from numpy.random.bit_generator import BitGenerator, SeedSequence from qiskit import QuantumCircuit @@ -23,6 +23,7 @@ from qiskit.exceptions import QiskitError from qiskit.providers.backend import Backend +from .clifford_utils import CliffordUtils from .rb_experiment import StandardRB from .interleaved_rb_analysis import InterleavedRBAnalysis @@ -76,7 +77,7 @@ def __init__( sequences are constructed by appending additional Clifford samples to shorter sequences. """ - self._set_interleaved_element(interleaved_element) + super().__init__( qubits, lengths, @@ -85,13 +86,43 @@ def __init__( seed=seed, full_sampling=full_sampling, ) + self._set_interleaved_element(interleaved_element) + self._transpiled_interleaved_elem = None self.analysis = InterleavedRBAnalysis() self.analysis.set_options(outcome="0" * self.num_qubits) + def circuits(self) -> List[QuantumCircuit]: + """Return a list of RB circuits. + + Returns: + A list of :class:`QuantumCircuit`. + """ + rng = default_rng(seed=self.experiment_options.seed) + circuits = [] + if self.num_qubits == 1 and self._transpiled_cliff_circuits is None: + self._transpiled_cliff_circuits = ( + CliffordUtils.generate_1q_transpiled_clifford_circuits( + basis_gates=self.transpile_options.basis_gates + ) + ) + for _ in range(self.experiment_options.num_samples): + if self.num_qubits == 1: + self._set_transpiled_interleaved_element() + std_circuits, int_circuits = self._build_rb_circuits( + self.experiment_options.lengths, + rng, + interleaved_element=self._transpiled_interleaved_elem, + ) + circuits += std_circuits + circuits += int_circuits + else: + circuits += self._sample_circuits(self.experiment_options.lengths, rng) + return circuits + def _sample_circuits(self, lengths, rng): circuits = [] for length in lengths if self._full_sampling else [lengths[-1]]: - elements = self._clifford_utils.random_clifford_circuits(self.num_qubits, length, rng) + elements = CliffordUtils.random_clifford_circuits(self.num_qubits, length, rng) element_lengths = [len(elements)] if self._full_sampling else lengths std_circuits = self._generate_circuit(elements, element_lengths) for circuit in std_circuits: @@ -141,3 +172,22 @@ def _set_interleaved_element(self, interleaved_element): interleaved_element.name ) ) from error + + def _set_transpiled_interleaved_element(self): + """ + Create the transpiled interleaved element. If it is a single gate, + create a circuit comprising this gate. + """ + if not isinstance(self._interleaved_element, QuantumCircuit): + qc_interleaved = QuantumCircuit(1) + qc_interleaved.append(self._interleaved_element[0], [0], []) + self._transpiled_interleaved_elem = self._interleaved_element + else: + qc_interleaved = self._interleaved_element + if hasattr(self.transpile_options, "basis_gates"): + basis_gates = self.transpile_options.basis_gates + else: + basis_gates = None + self._transpiled_interleaved_elem = CliffordUtils.transpile_single_clifford( + qc_interleaved, basis_gates=basis_gates + ) diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index 0423666cb4..c86e056ed9 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -20,8 +20,8 @@ from numpy.random import Generator, default_rng from numpy.random.bit_generator import BitGenerator, SeedSequence -from qiskit import QuantumCircuit, QiskitError -from qiskit.circuit import Instruction +from qiskit import QuantumCircuit, ClassicalRegister, QiskitError +from qiskit.circuit import Instruction, Clbit from qiskit.quantum_info import Clifford from qiskit.providers.backend import Backend @@ -29,7 +29,7 @@ from qiskit_experiments.framework.restless_mixin import RestlessMixin from .rb_analysis import RBAnalysis from .clifford_utils import CliffordUtils - +from .clifford_data import CLIFF_INVERSE_DATA LOG = logging.getLogger(__name__) @@ -92,7 +92,7 @@ def __init__( # Set fixed options self._full_sampling = full_sampling - self._clifford_utils = CliffordUtils() + self._transpiled_cliff_circuits = None def _verify_parameters(self, lengths, num_samples): """Verify input correctness, raise QiskitError if needed""" @@ -132,11 +132,27 @@ def circuits(self) -> List[QuantumCircuit]: Returns: A list of :class:`QuantumCircuit`. + + Raises: + QiskitError: if basis_gates is not set in transpile_options. """ rng = default_rng(seed=self.experiment_options.seed) circuits = [] + if self.num_qubits == 1: + if not hasattr(self.transpile_options, "basis_gates"): + raise QiskitError("transpile_options.basis_gates must be set for rb_experiment") + if self._transpiled_cliff_circuits is None: + self._transpiled_cliff_circuits = ( + CliffordUtils.generate_1q_transpiled_clifford_circuits( + basis_gates=self.transpile_options.basis_gates + ) + ) for _ in range(self.experiment_options.num_samples): - circuits += self._sample_circuits(self.experiment_options.lengths, rng) + if self.num_qubits == 1: + rb_circuits, _ = self._build_rb_circuits(self.experiment_options.lengths, rng) + circuits += rb_circuits + else: + circuits += self._sample_circuits(self.experiment_options.lengths, rng) return circuits def _sample_circuits(self, lengths: Iterable[int], rng: Generator) -> List[QuantumCircuit]: @@ -152,7 +168,7 @@ def _sample_circuits(self, lengths: Iterable[int], rng: Generator) -> List[Quant """ circuits = [] for length in lengths if self._full_sampling else [lengths[-1]]: - elements = self._clifford_utils.random_clifford_circuits(self.num_qubits, length, rng) + elements = CliffordUtils.random_clifford_circuits(self.num_qubits, length, rng) element_lengths = [len(elements)] if self._full_sampling else lengths circuits += self._generate_circuit(elements, element_lengths) return circuits @@ -211,9 +227,267 @@ def _generate_circuit( circuits.append(rb_circ) return circuits + def _build_rb_circuits( + self, lengths: List[int], rng: Generator, interleaved_element: QuantumCircuit = None + ) -> List[QuantumCircuit]: + """ + build_rb_circuits + Args: + lengths: A list of RB sequence lengths. We create random circuits + where the number of cliffords in each is defined in lengths. + rng: Generator object for random number generation. + If None, default_rng will be used. + interleaved_element: the interleaved element as a QuantumCircuit, if it exists. + + Returns: + The transpiled RB circuits. + + Additional information: + To create the RB circuit, we use a mapping between Cliffords and integers + defined in the file clifford_data.py. The operations compose and inverse are much faster + when performed on the integers rather than on the Cliffords themselves. + """ + if self._full_sampling: + return self._build_rb_circuits_full_sampling(lengths, rng, interleaved_element) + is_interleaved = interleaved_element is not None + max_qubit = max(self.physical_qubits) + 1 + all_rb_circuits = [] + + if is_interleaved: + all_rb_interleaved_circuits = [] + else: + all_rb_interleaved_circuits = None + + # When full_sampling==False, each circuit is the prefix of the next circuit (without the + # inverse Clifford at the end of the circuit. The variable 'circ' will contain + # the growing circuit. + # When each circuit reaches its length, we copy it to rb_circ, append the inverse, + # and add it to the list of circuits. + + if is_interleaved: + interleaved_circ = QuantumCircuit(max_qubit, 1) + interleaved_circ.barrier(0) + else: + interleaved_circ = None + + random_samples = rng.integers(24, size=lengths[-1]) + circ = QuantumCircuit(max_qubit, 1) + circ.barrier(0) + + # composed_cliff_num is the number representing the composition of all the Cliffords up to now + # composed_interleaved_num is the same for an interleaved circuit + composed_cliff_num = 0 # 0 is the Clifford that is Id + composed_interleaved_num = 0 + prev_length = 0 + + for length in lengths: + for i in range(prev_length, length): + rand = random_samples[i] + # choose random clifford + next_circ = self._transpiled_cliff_circuits[rand] + circ.compose(next_circ, inplace=True) + composed_cliff_num = CliffordUtils.compose_num_with_clifford( + composed_cliff_num, next_circ, self.transpile_options.basis_gates + ) + + circ.barrier(0) + if is_interleaved: + interleaved_circ.compose(next_circ, inplace=True) + composed_interleaved_num = CliffordUtils.compose_num_with_clifford( + composed_interleaved_num, next_circ, self.transpile_options.basis_gates + ) + interleaved_circ.barrier(0) + # The interleaved element is appended after every Clifford and its barrier + interleaved_circ.compose(interleaved_element, inplace=True) + composed_interleaved_num = CliffordUtils.compose_num_with_clifford( + composed_interleaved_num, + interleaved_element, + self.transpile_options.basis_gates, + ) + interleaved_circ.barrier(0) + + if i == length - 1: + rb_circ = circ.copy() # circ is used as the prefix of the next circuit + inverse_clifford_num = CLIFF_INVERSE_DATA[composed_cliff_num] + # append the inverse + rb_circ.compose( + self._transpiled_cliff_circuits[inverse_clifford_num], inplace=True + ) + rb_circ.measure(0, 0) + + rb_circ.metadata = { + "experiment_type": "rb", + "xval": length, + "group": "Clifford", + "physical_qubits": self.physical_qubits, + "interleaved": False, + } + all_rb_circuits.append(rb_circ) + if is_interleaved: + # interleaved_circ is used as the prefix of the next circuit + rb_interleaved_circ = interleaved_circ.copy() + # append the inverse + inverse_interleaved_num = CLIFF_INVERSE_DATA[composed_interleaved_num] + rb_interleaved_circ.compose( + self._transpiled_cliff_circuits[inverse_interleaved_num], inplace=True + ) + rb_interleaved_circ.measure(0, 0) + + rb_interleaved_circ.metadata = { + "experiment_type": "rb", + "xval": length, + "group": "Clifford", + "physical_qubits": self.physical_qubits, + "interleaved": True, + } + all_rb_interleaved_circuits.append(rb_interleaved_circ) + + prev_length = i + 1 + return all_rb_circuits, all_rb_interleaved_circuits + + def _build_rb_circuits_full_sampling( + self, lengths: List[int], rng: Generator, interleaved_element: QuantumCircuit = None + ) -> List[QuantumCircuit]: + """ + _build_rb_circuits_full_sampling + Args: + lengths: A list of RB sequence lengths. We create random circuits + where the number of cliffords in each is defined in lengths. + rng: Generator object for random number generation. + If None, default_rng will be used. + interleaved_element: the interleaved element as a QuantumCircuit, if it exists. + + Returns: + The transpiled RB circuits. + + Additional information: + This is similar to _build_rb_circuits for the case of full_sampling. + """ + is_interleaved = interleaved_element is not None + all_rb_circuits = [] + if is_interleaved: + all_rb_interleaved_circuits = [] + else: + all_rb_interleaved_circuits = None + + max_qubit = max(self.physical_qubits) + 1 + for length in lengths: + # We define the circuit size here, for the layout that will + # be created later + rb_circ = QuantumCircuit(max_qubit, 1) + rb_circ.barrier(0) + if is_interleaved: + rb_interleaved_circ = QuantumCircuit(max_qubit, 1) + rb_interleaved_circ.barrier(0) + else: + rb_interleaved_circ = None + + random_samples = rng.integers(24, size=length) + # composed_cliff_num is the number representing the composition of + # all the Cliffords up to now + # composed_interleaved_num is the same for an interleaved circuit + composed_cliff_num = 0 + composed_interleaved_num = 0 + # For full_sampling, we create each circuit independently. + for i in range(length): + # choose random clifford + rand = random_samples[i] + next_circ = self._transpiled_cliff_circuits[rand].copy() + rb_circ.compose(next_circ, inplace=True) + + composed_cliff_num = CliffordUtils.compose_num_with_clifford( + composed_num=composed_cliff_num, + qc=next_circ, + basis_gates=self.transpile_options.basis_gates, + ) + rb_circ.barrier(0) + if is_interleaved: + rb_interleaved_circ.compose(next_circ, inplace=True) + composed_interleaved_num = CliffordUtils.compose_num_with_clifford( + composed_num=composed_interleaved_num, + qc=next_circ, + basis_gates=self.transpile_options.basis_gates, + ) + rb_interleaved_circ.barrier(0) + rb_interleaved_circ.compose(interleaved_element, inplace=True) + composed_interleaved_num = CliffordUtils.compose_num_with_clifford( + composed_interleaved_num, + interleaved_element, + self.transpile_options.basis_gates, + ) + rb_interleaved_circ.barrier(0) + + inverse_clifford_num = CLIFF_INVERSE_DATA[composed_cliff_num] + # append the inverse + rb_circ.compose(self._transpiled_cliff_circuits[inverse_clifford_num], inplace=True) + rb_circ.measure(0, 0) + rb_circ.metadata = { + "experiment_type": "rb", + "xval": length, + "group": "Clifford", + "physical_qubits": self.physical_qubits, + "interleaved": False, + } + if is_interleaved: + inverse_interleaved_num = CLIFF_INVERSE_DATA[composed_interleaved_num] + rb_interleaved_circ.compose( + self._transpiled_cliff_circuits[inverse_interleaved_num], inplace=True + ) + rb_interleaved_circ.measure(0, 0) + rb_interleaved_circ.metadata = { + "experiment_type": "rb", + "xval": length, + "group": "Clifford", + "physical_qubits": self.physical_qubits, + "interleaved": True, + } + all_rb_circuits.append(rb_circ) + if is_interleaved: + all_rb_interleaved_circuits.append(rb_interleaved_circ) + return all_rb_circuits, all_rb_interleaved_circuits + + # This method does a quick layout to avoid calling 'transpile()' which is + # very costly in performance + # We simply copy the circuit to a new circuit where we define the mapping + # of the qubit to the single physical qubit that was requested by the user + # This is a hack, and would be better if transpile() implemented it. + # Something similar is done in ParallelExperiment._combined_circuits + def _layout_for_rb_single_qubit(self): + transpiled = [] + qargs_map = {0: self.physical_qubits[0]} + for circ in self.circuits(): + new_circ = QuantumCircuit( + *circ.qregs, + name=circ.name, + global_phase=circ.global_phase, + metadata=circ.metadata.copy(), + ) + clbits = circ.num_clbits + if clbits: + creg = ClassicalRegister(clbits) + new_cargs = [Clbit(creg, i) for i in range(clbits)] + new_circ.add_register(creg) + else: + cargs = [] + + for inst, qargs, cargs in circ.data: + mapped_cargs = [new_cargs[circ.find_bit(clbit).index] for clbit in cargs] + mapped_qargs = [circ.qubits[qargs_map[circ.find_bit(i).index]] for i in qargs] + new_circ.data.append((inst, mapped_qargs, mapped_cargs)) + # Add the calibrations + for gate, cals in circ.calibrations.items(): + for key, sched in cals.items(): + new_circ.add_calibration(gate, qubits=key[0], schedule=sched, params=key[1]) + + transpiled.append(new_circ) + return transpiled + def _transpiled_circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits, transpiled.""" - transpiled = super()._transpiled_circuits() + if self.num_qubits == 1: + transpiled = self._layout_for_rb_single_qubit() + else: + transpiled = super()._transpiled_circuits() if self.analysis.options.get("gate_error_ratio", None) is None: # Gate errors are not computed, then counting ops is not necessary. diff --git a/test/randomized_benchmarking/test_randomized_benchmarking.py b/test/randomized_benchmarking/test_randomized_benchmarking.py index 02cd291811..402128fd85 100644 --- a/test/randomized_benchmarking/test_randomized_benchmarking.py +++ b/test/randomized_benchmarking/test_randomized_benchmarking.py @@ -14,17 +14,19 @@ from test.base import QiskitExperimentsTestCase -import numpy as np +import random from ddt import ddt, data, unpack from qiskit.circuit import Delay, QuantumCircuit -from qiskit.circuit.library import SXGate, CXGate, TGate, XGate +from qiskit.circuit.library import SXGate, CXGate, TGate from qiskit.exceptions import QiskitError from qiskit.providers.aer import AerSimulator from qiskit.providers.aer.noise import NoiseModel, depolarizing_error -from qiskit.quantum_info import Clifford +from qiskit.quantum_info import Clifford, Operator from qiskit_experiments.library import randomized_benchmarking as rb +from qiskit_experiments.library.randomized_benchmarking import CliffordUtils from qiskit_experiments.database_service.exceptions import DbExperimentEntryNotFound +from qiskit_experiments.framework.composite import ParallelExperiment class RBTestCase(QiskitExperimentsTestCase): @@ -67,13 +69,9 @@ def assertAllIdentity(self, circuits): """Test if all experiment circuits are identity.""" for circ in circuits: num_qubits = circ.num_qubits - iden = Clifford(np.eye(2 * num_qubits, dtype=bool)) - + qc_iden = QuantumCircuit(num_qubits) circ.remove_final_measurements() - - self.assertEqual( - Clifford(circ), iden, f"Circuit {circ.name} doesn't result in the identity matrix." - ) + assert Operator(circ).equiv(Operator(qc_iden)) @ddt @@ -186,7 +184,34 @@ def test_return_same_circuit(self): self.assertEqual(circs1[1].decompose(), circs2[1].decompose()) self.assertEqual(circs1[2].decompose(), circs2[2].decompose()) - def test_full_sampling(self): + def test_full_sampling_single_qubit(self): + """Test if full sampling generates different circuits.""" + exp1 = rb.StandardRB( + qubits=(0,), + lengths=[10, 20, 30], + seed=123, + backend=self.backend, + full_sampling=False, + ) + exp1.set_transpile_options(**self.transpiler_options) + exp2 = rb.StandardRB( + qubits=(0,), + lengths=[10, 20, 30], + seed=123, + backend=self.backend, + full_sampling=True, + ) + exp2.set_transpile_options(**self.transpiler_options) + circs1 = exp1.circuits() + circs2 = exp2.circuits() + + self.assertEqual(circs1[0].decompose(), circs2[0].decompose()) + + # fully sampled circuits are regenerated while other is just built on top of previous length + self.assertNotEqual(circs1[1].decompose(), circs2[1].decompose()) + self.assertNotEqual(circs1[2].decompose(), circs2[2].decompose()) + + def test_full_sampling_2_qubits(self): """Test if full sampling generates different circuits.""" exp1 = rb.StandardRB( qubits=(0, 1), @@ -257,61 +282,109 @@ def test_expdata_serialization(self): self.assertRoundTripSerializable(expdata, check_func=self.experiment_data_equiv) self.assertRoundTripPickle(expdata, check_func=self.experiment_data_equiv) + def test_single_qubit_parallel(self): + """Test single qubit RB in parallel.""" + qubits = [0, 2] + lengths = list(range(1, 300, 30)) + exps = [] + for qubit in qubits: + exp = rb.StandardRB( + qubits=[qubit], lengths=lengths, seed=123, backend=self.backend + ) + exp.analysis.set_options(gate_error_ratio=None, plot_raw_data=False) + exps.append(exp) + + par_exp = ParallelExperiment(exps) + par_exp.set_transpile_options(**self.transpiler_options) + + par_expdata = par_exp.run(backend=self.backend) + self.assertExperimentDone(par_expdata) + epc_expected = 1 - (1 - 1 / 2 * self.p1q) ** 1.0 + for i in range(2): + epc = par_expdata.child_data(i).analysis_results("EPC") + self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.1 * epc_expected) + @ddt class TestInterleavedRB(RBTestCase): """Test for interleaved RB.""" - @data([XGate(), [3], 4], [CXGate(), [4, 7], 5]) + @data([SXGate(), [3], 4], [CXGate(), [4, 7], 5]) @unpack def test_interleaved_structure(self, interleaved_element, qubits, length): """Verifies that when generating an interleaved circuit, it will be identical to the original circuit up to additions of barrier and interleaved element between any two Cliffords. """ - exp = rb.InterleavedRB( - interleaved_element=interleaved_element, qubits=qubits, lengths=[length], num_samples=1 - ) - - circuits = exp.circuits() - c_std = circuits[0] - c_int = circuits[1] - if c_std.metadata["interleaved"]: - c_std, c_int = c_int, c_std - num_cliffords = c_std.metadata["xval"] - std_idx = 0 - int_idx = 0 - for _ in range(num_cliffords): - # barrier - self.assertEqual(c_std[std_idx][0].name, "barrier") - self.assertEqual(c_int[int_idx][0].name, "barrier") - # clifford - self.assertEqual(c_std[std_idx + 1], c_int[int_idx + 1]) - # for interleaved circuit: barrier + interleaved element - self.assertEqual(c_int[int_idx + 2][0].name, "barrier") - self.assertEqual(c_int[int_idx + 3][0].name, interleaved_element.name) - std_idx += 2 - int_idx += 4 + full_sampling = [True, False] + for val in full_sampling: + exp = rb.InterleavedRB( + interleaved_element=interleaved_element, + qubits=qubits, + lengths=[length], + num_samples=1, + full_sampling=val, + ) + exp.set_transpile_options(**self.transpiler_options) + circuits = exp.circuits() + c_std = circuits[0] + c_int = circuits[1] + if c_std.metadata["interleaved"]: + c_std, c_int = c_int, c_std + num_cliffords = c_std.metadata["xval"] + std_idx = 0 + int_idx = 0 + for _ in range(num_cliffords): + # barrier + self.assertEqual(c_std[std_idx][0].name, "barrier") + self.assertEqual(c_int[int_idx][0].name, "barrier") + # clifford + std_idx += 1 + int_idx += 1 + while c_std[std_idx][0].name != "barrier": + self.assertEqual(c_std[std_idx], c_int[int_idx]) + std_idx += 1 + int_idx += 1 + # for interleaved circuit: barrier + interleaved element + self.assertEqual(c_int[int_idx][0].name, "barrier") + int_idx += 1 + self.assertEqual(c_int[int_idx][0].name, interleaved_element.name) + int_idx += 1 def test_single_qubit(self): - """Test single qubit IRB.""" - exp = rb.InterleavedRB( - interleaved_element=SXGate(), - qubits=(0,), - lengths=list(range(1, 300, 30)), - seed=123, - backend=self.backend, - ) - exp.set_transpile_options(**self.transpiler_options) - self.assertAllIdentity(exp.circuits()) + """Test single qubit IRB, once with an interleaved gate, once with an interleaved + Clifford circuit. + """ + interleaved_gate = SXGate() + random.seed(123) + num = random.randint(0, 23) + interleaved_clifford = CliffordUtils.clifford_1_qubit_circuit(num) + # The circuit created for interleaved_clifford is: + # qc = QuantumCircuit(1) + # qc.rz(np.pi/2, 0) + # qc.sx(0) + # qc.rz(np.pi/2, 0) + # Since there is a single sx per interleaved_element, + # therefore epc_expected is the same as for when interleaved_element = SXGate() + for interleaved_element in [interleaved_gate, interleaved_clifford]: + exp = rb.InterleavedRB( + interleaved_element=interleaved_element, + qubits=(0,), + lengths=list(range(1, 300, 30)), + seed=123, + backend=self.backend, + ) + exp.set_transpile_options(**self.transpiler_options) - expdata = exp.run() - self.assertExperimentDone(expdata) + self.assertAllIdentity(exp.circuits()) - # Since this is interleaved, we can directly compare values, i.e. n_gpc = 1 - epc = expdata.analysis_results("EPC") - epc_expected = 1 / 2 * self.p1q - self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.1 * epc_expected) + expdata = exp.run() + self.assertExperimentDone(expdata) + + # Since this is interleaved, we can directly compare values, i.e. n_gpc = 1 + epc = expdata.analysis_results("EPC") + epc_expected = 1 / 2 * self.p1q + self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.1 * epc_expected) def test_two_qubit(self): """Test two qubit IRB.""" @@ -335,7 +408,7 @@ def test_two_qubit(self): def test_non_clifford_interleaved_element(self): """Verifies trying to run interleaved RB with non Clifford element throws an exception""" - qubits = 1 + qubits = [0] lengths = [1, 4, 6, 9, 13, 16] interleaved_element = TGate() # T gate is not Clifford, this should fail self.assertRaises( @@ -355,12 +428,20 @@ def test_interleaving_delay(self): qubits=[0], lengths=[1], num_samples=1, + seed=1234, # This seed gives a 2-gate clifford ) - # Not raises an error - _, int_circ = exp.circuits() + exp.set_transpile_options(**self.transpiler_options) + + # Does not raise an error + _, int_circs = exp.circuits() + + # barrier, 2-gate clifford, barrier, "delay", barrier, ... + self.assertEqual(int_circs.data[4][0].name, interleaved_element.name) - # barrier, clifford, barrier, "delay", barrier, ... - self.assertEqual(int_circ.data[3][0], interleaved_element) + # Transpiled delay duration is represented in seconds, so must convert from us + self.assertEqual(int_circs.data[4][0].unit, "s") + self.assertAlmostEqual(int_circs.data[4][0].params[0], interleaved_element.params[0] * 1e-6) + self.assertAllIdentity([int_circs]) def test_interleaving_circuit_with_delay(self): """Test circuit with delay can be interleaved.""" @@ -369,7 +450,11 @@ def test_interleaving_circuit_with_delay(self): delay_qc.x(1) exp = rb.InterleavedRB( - interleaved_element=delay_qc, qubits=[1, 2], lengths=[1], seed=123, num_samples=1 + interleaved_element=delay_qc, + qubits=[1, 2], + lengths=[1], + seed=123, + num_samples=1, ) _, int_circ = exp.circuits() @@ -382,7 +467,10 @@ def test_interleaving_circuit_with_delay(self): def test_experiment_config(self): """Test converting to and from config works""" exp = rb.InterleavedRB( - interleaved_element=SXGate(), qubits=(0,), lengths=[10, 20, 30], seed=123 + interleaved_element=SXGate(), + qubits=(0,), + lengths=[10, 20, 30], + seed=123, ) loaded_exp = rb.InterleavedRB.from_config(exp.config()) self.assertNotEqual(exp, loaded_exp) @@ -419,9 +507,9 @@ def test_expdata_serialization(self): class TestEPGAnalysis(QiskitExperimentsTestCase): - """Test case for EPG colculation from EPC. + """Test case for EPG calculation from EPC. - EPG and depplarizing probability p are assumed to have following relationship + EPG and depolarizing probability p are assumed to have following relationship EPG = (2^n - 1) / 2^n ยท p diff --git a/test/randomized_benchmarking/test_rb_utils.py b/test/randomized_benchmarking/test_rb_utils.py index c9163e287d..e60daaa529 100644 --- a/test/randomized_benchmarking/test_rb_utils.py +++ b/test/randomized_benchmarking/test_rb_utils.py @@ -18,7 +18,8 @@ from uncertainties import ufloat from ddt import ddt, data, unpack -from qiskit import QuantumCircuit +from qiskit import QuantumCircuit, QuantumRegister +from qiskit.quantum_info import Operator from qiskit.circuit.library import ( IGate, XGate, @@ -27,13 +28,16 @@ HGate, SGate, SdgGate, + SXGate, CXGate, CZGate, SwapGate, + RZGate, ) from qiskit.quantum_info import Clifford import qiskit_experiments.library.randomized_benchmarking as rb from qiskit_experiments.framework import AnalysisResultData +from qiskit_experiments.library.randomized_benchmarking.clifford_utils import CliffordUtils @ddt @@ -204,14 +208,12 @@ def test_clifford_1_qubit_generation(self): {"stabilizer": ["-Y"], "destabilizer": ["+Z"]}, ] cliffords = [Clifford.from_dict(i) for i in clifford_dicts] - utils = rb.CliffordUtils() for n in range(24): - clifford = utils.clifford_1_qubit(n) + clifford = CliffordUtils.clifford_1_qubit(n) self.assertEqual(clifford, cliffords[n]) def test_clifford_2_qubit_generation(self): """Verify 2-qubit clifford indeed generates the correct group""" - utils = rb.CliffordUtils() pauli_free_elements = [ 0, 1, @@ -936,7 +938,7 @@ def test_clifford_2_qubit_generation(self): ] cliffords = [] for n in pauli_free_elements: - clifford = utils.clifford_2_qubit(n) + clifford = CliffordUtils.clifford_2_qubit(n) phase = clifford.table.phase for i in range(4): self.assertFalse(phase[i]) @@ -1005,7 +1007,7 @@ def test_clifford_2_qubit_generation(self): phases = [] table = None for n in pauli_check_elements: - clifford = utils.clifford_2_qubit(n) + clifford = CliffordUtils.clifford_2_qubit(n) if table is None: table = clifford.table.array else: @@ -1014,3 +1016,62 @@ def test_clifford_2_qubit_generation(self): for other_phase in phases: self.assertNotEqual(phase, other_phase) phases.append(phase) + + def test_number_to_clifford_mapping_single_gate(self): + """Testing that the methods num_from_1_qubit_clifford_single_gate and + clifford_1_qubit_circuit perform the reverse operations from + each other""" + transpiled_cliff_list = [ + SXGate(), + RZGate(np.pi), + RZGate(-np.pi), + RZGate(np.pi / 2), + RZGate(-np.pi / 2), + ] + transpiled_cliff_names = [gate.name for gate in transpiled_cliff_list] + for inst in transpiled_cliff_list: + num = CliffordUtils.num_from_1_qubit_clifford_single_gate(inst, transpiled_cliff_names) + qc_from_num = CliffordUtils.clifford_1_qubit_circuit(num=num) + qr = QuantumRegister(1) + qc_from_inst = QuantumCircuit(qr) + qc_from_inst._append(inst, [qr[0]], []) + assert Operator(qc_from_num).equiv(Operator(qc_from_inst)) + + general_cliff_list = [ + IGate(), + HGate(), + SdgGate(), + SGate(), + XGate(), + SXGate(), + YGate(), + ZGate(), + ] + general_cliff_names = [gate.name for gate in general_cliff_list] + for inst in general_cliff_list: + num = CliffordUtils.num_from_1_qubit_clifford_single_gate(inst, general_cliff_names) + qc_from_num = CliffordUtils.clifford_1_qubit_circuit(num=num) + qr = QuantumRegister(1) + qc_from_inst = QuantumCircuit(qr) + qc_from_inst._append(inst, [qr[0]], []) + assert Operator(qc_from_num).equiv(Operator(qc_from_inst)) + + def test_number_to_clifford_mapping(self): + """Test that the number generated by compose_num_with_clifford on qc + corresponds to the index of the circuit qc. + + """ + transpiled_cliff_list = [ + SXGate(), + RZGate(np.pi), + RZGate(-np.pi), + RZGate(np.pi / 2), + RZGate(-np.pi / 2), + ] + transpiled_cliff_names = [gate.name for gate in transpiled_cliff_list] + all_transpiled_circuits = CliffordUtils.generate_1q_transpiled_clifford_circuits( + transpiled_cliff_names + ) + for index, qc in enumerate(all_transpiled_circuits): + num = CliffordUtils.compose_num_with_clifford(0, qc, transpiled_cliff_names) + assert num == index