diff --git a/MANIFEST.in b/MANIFEST.in index 1b30164d89..017221c1f8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include LICENSE.txt include requirements.txt include qiskit_experiments/VERSION.txt +recursive-include qiskit_experiments/library/randomized_benchmarking/data * diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py index 985edbc0e4..f7ded3ac76 100644 --- a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py +++ b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py @@ -12,16 +12,20 @@ """ Utilities for using the Clifford group in randomized benchmarking """ - +import itertools +import os from functools import lru_cache from numbers import Integral -from typing import Optional, Union +from typing import Optional, Union, Tuple +import numpy as np +import scipy.sparse from numpy.random import Generator, default_rng from qiskit.circuit import Gate, Instruction from qiskit.circuit import QuantumCircuit, QuantumRegister -from qiskit.circuit.library import SdgGate, HGate, SGate +from qiskit.circuit.library import SdgGate, HGate, SGate, XGate, YGate, ZGate +from qiskit.exceptions import QiskitError from qiskit.quantum_info import Clifford, random_clifford @@ -71,7 +75,7 @@ class CliffordUtils: NUM_CLIFFORD_1_QUBIT = 24 NUM_CLIFFORD_2_QUBIT = 11520 CLIFFORD_1_QUBIT_SIG = (2, 3, 4) - CLIFFORD_2_QUBIT_SIGS = [ + CLIFFORD_2_QUBIT_SIGS = [ # TODO: deprecate (2, 2, 3, 3, 4, 4), (2, 2, 3, 3, 3, 3, 4, 4), (2, 2, 3, 3, 3, 3, 4, 4), @@ -164,56 +168,9 @@ def clifford_2_qubit_circuit(cls, num): """Return the 2-qubit clifford circuit corresponding to `num` where `num` is between 0 and 11519. """ - vals = cls._unpack_num_multi_sigs(num, cls.CLIFFORD_2_QUBIT_SIGS) qc = QuantumCircuit(2, name=f"Clifford-2Q({num})") - if vals[0] == 0 or vals[0] == 3: - (form, i0, i1, j0, j1, p0, p1) = vals - else: - (form, i0, i1, j0, j1, k0, k1, p0, p1) = vals - if i0 == 1: - qc.h(0) - if i1 == 1: - qc.h(1) - if j0 == 1: - qc.sxdg(0) - if j0 == 2: - qc.s(0) - if j1 == 1: - qc.sxdg(1) - if j1 == 2: - qc.s(1) - if form in (1, 2, 3): - qc.cx(0, 1) - if form in (2, 3): - qc.cx(1, 0) - if form == 3: - qc.cx(0, 1) - if form in (1, 2): - if k0 == 1: # V gate - qc.sdg(0) - qc.h(0) - if k0 == 2: # W gate - qc.h(0) - qc.s(0) - if k1 == 1: # V gate - qc.sdg(1) - qc.h(1) - if k1 == 2: # W gate - qc.h(1) - qc.s(1) - if p0 == 1: - qc.x(0) - if p0 == 2: - qc.y(0) - if p0 == 3: - qc.z(0) - if p1 == 1: - qc.x(1) - if p1 == 2: - qc.y(1) - if p1 == 3: - qc.z(1) - + for layer, idx in enumerate(_layer_indices_from_num(num)): + qc.compose(_CLIFFORD_LAYER[layer][idx], inplace=True) return qc @staticmethod @@ -229,16 +186,317 @@ def _unpack_num(num, sig): num //= k return res - @staticmethod - def _unpack_num_multi_sigs(num, sigs): - """Returns the result of `_unpack_num` on one of the - signatures in `sigs` - """ - for i, sig in enumerate(sigs): - sig_size = 1 - for k in sig: - sig_size *= k - if num < sig_size: - return [i] + CliffordUtils._unpack_num(num, sig) - num -= sig_size - return None + +_CLIFF_SINGLE_GATE_MAP_1Q = { + ("id", (0,)): 0, + ("h", (0,)): 1, + ("sxdg", (0,)): 2, + ("s", (0,)): 4, + ("x", (0,)): 6, + ("sx", (0,)): 8, + ("y", (0,)): 12, + ("z", (0,)): 18, + ("sdg", (0,)): 22, +} +_CLIFF_SINGLE_GATE_MAP_2Q = { + ("id", (0,)): 0, + ("id", (1,)): 0, + ("h", (0,)): 5760, + ("h", (1,)): 2880, + ("sxdg", (0,)): 6720, + ("sxdg", (1,)): 3200, + ("s", (0,)): 7680, + ("s", (1,)): 3520, + ("x", (0,)): 4, + ("x", (1,)): 1, + ("sx", (0,)): 6724, + ("sx", (1,)): 3201, + ("y", (0,)): 8, + ("y", (1,)): 2, + ("z", (0,)): 12, + ("z", (1,)): 3, + ("sdg", (0,)): 7692, + ("sdg", (1,)): 3523, + ("cx", (0, 1)): 16, + ("cx", (1, 0)): 2336, + ("cz", (0, 1)): 368, + ("cz", (1, 0)): 368, +} + + +######## +# Functions for 1-qubit integer Clifford operations +def compose_1q(lhs: Integral, rhs: Integral) -> Integral: + """Return the composition of 1-qubit clifford integers.""" + return _CLIFFORD_COMPOSE_1Q[lhs, rhs] + + +def inverse_1q(num: Integral) -> Integral: + """Return the inverse of 1-qubit clifford integers.""" + return _CLIFFORD_INVERSE_1Q[num] + + +def num_from_1q_circuit(qc: QuantumCircuit) -> Integral: + """Convert a given 1-qubit Clifford circuit to the corresponding integer.""" + num = 0 + for inst in qc: + rhs = _num_from_1q_gate(op=inst.operation) + num = _CLIFFORD_COMPOSE_1Q[num, rhs] + return num + + +def _num_from_1q_gate(op: Instruction) -> int: + """ + Convert a given 1-qubit clifford operation to the corresponding integer. + Note that supported operations are limited to ones in `CLIFF_SINGLE_GATE_MAP_1Q` or Rz gate. + + Args: + op: operation to be converted. + + Returns: + An integer representing a Clifford consisting of a single operation. + + Raises: + QiskitError: if the input instruction is not a Clifford instruction. + QiskitError: if rz is given with a angle that is not Clifford. + """ + if op.name in {"delay", "barrier"}: + return 0 + try: + name = _deparameterized_name(op) + return _CLIFF_SINGLE_GATE_MAP_1Q[(name, (0,))] + except QiskitError as err: + raise QiskitError( + f"Parameterized instruction {op.name} could not be converted to integer Clifford" + ) from err + except KeyError as err: + raise QiskitError( + f"Instruction {op.name} could not be converted to integer Clifford" + ) from err + + +def _deparameterized_name(inst: Instruction) -> str: + if inst.name == "rz": + if np.isclose(inst.params[0], np.pi) or np.isclose(inst.params[0], -np.pi): + return "z" + elif np.isclose(inst.params[0], np.pi / 2): + return "s" + elif np.isclose(inst.params[0], -np.pi / 2): + return "sdg" + else: + raise QiskitError("Wrong param {} for rz in clifford".format(inst.params[0])) + + return inst.name + + +def _load_clifford_compose_1q(): + dirname = os.path.dirname(__file__) + data = np.load(f"{dirname}/data/clifford_compose_1q.npz") + return data["table"] + + +def _load_clifford_inverse_1q(): + dirname = os.path.dirname(__file__) + data = np.load(f"{dirname}/data/clifford_inverse_1q.npz") + return data["table"] + + +_CLIFFORD_COMPOSE_1Q = _load_clifford_compose_1q() +_CLIFFORD_INVERSE_1Q = _load_clifford_inverse_1q() + + +######## +# Functions for 2-qubit integer Clifford operations +def compose_2q(lhs: Integral, rhs: Integral) -> Integral: + """Return the composition of 2-qubit clifford integers.""" + num = lhs + for layer, idx in enumerate(_layer_indices_from_num(rhs)): + circ = _CLIFFORD_LAYER[layer][idx] + num = _compose_num_with_circuit_2q(num, circ) + return num + + +def inverse_2q(num: Integral) -> Integral: + """Return the inverse of 2-qubit clifford integers.""" + return _CLIFFORD_INVERSE_2Q[num] + + +def num_from_2q_circuit(qc: QuantumCircuit) -> Integral: + """Convert a given 2-qubit Clifford circuit to the corresponding integer.""" + return _compose_num_with_circuit_2q(0, qc) + + +# Shortcut to call the function converting circuit to num by number of qubits +# TODO: Too much? (if it is usefule, add more explanation) +num_from_circuit = { + 1: num_from_1q_circuit, + 2: num_from_2q_circuit, +} + + +def _compose_num_with_circuit_2q(num: Integral, qc: QuantumCircuit) -> Integral: + """Compose a number that represents a Clifford, with a Clifford circuit, and return the + number that represents the resulting Clifford.""" + lhs = num + for inst in qc: + qubits = tuple(qc.find_bit(q).index for q in inst.qubits) + rhs = _num_from_2q_gate(op=inst.operation, qubits=qubits) + try: + lhs = _CLIFFORD_COMPOSE_2Q_GATE[lhs, rhs] + except KeyError as err: + raise Exception(f"_CLIFFORD_COMPOSE_2Q_GATE[{lhs}][{rhs}]") from err + return lhs + + +def _num_from_2q_gate( + op: Instruction, qubits: Optional[Union[Tuple[int, int], Tuple[int]]] = None +) -> int: + """ + Convert a given 1-qubit clifford operation to the corresponding integer. + Note that supported operations are limited to ones in `CLIFF_SINGLE_GATE_MAP_2Q` or Rz gate. + + Args: + op: operation of instruction to be converted. + qubits: qubits to which the operation applies + + Returns: + An integer representing a Clifford consisting of a single operation. + + Raises: + QiskitError: if the input instruction is not a Clifford instruction. + QiskitError: if rz is given with a angle that is not Clifford. + """ + if op.name in {"delay", "barrier"}: + return 0 + + qubits = qubits or (0, 1) + try: + name = _deparameterized_name(op) + return _CLIFF_SINGLE_GATE_MAP_2Q[(name, qubits)] + except QiskitError as err: + raise QiskitError( + f"Parameterized instruction {op.name} could not be converted to integer Clifford" + ) from err + except KeyError as err: + raise QiskitError( + f"Instruction {op.name} on {qubits} could not be converted to integer Clifford" + ) from err + + +def _append_v_w(qc, vw0, vw1): + if vw0 == "v": + qc.sdg(0) + qc.h(0) + elif vw0 == "w": + qc.h(0) + qc.s(0) + if vw1 == "v": + qc.sdg(1) + qc.h(1) + elif vw1 == "w": + qc.h(1) + qc.s(1) + + +def _create_cliff_2q_layer_0(): + """Layer 0 consists of 0 or 1 H gates on each qubit, followed by 0/1/2 V gates on each qubit. + Number of Cliffords == 36.""" + circuits = [] + num_h = [0, 1] + v_w_gates = ["i", "v", "w"] + for h0, h1, v0, v1 in itertools.product(num_h, num_h, v_w_gates, v_w_gates): + qc = QuantumCircuit(2) + for _ in range(h0): + qc.h(0) + for _ in range(h1): + qc.h(1) + _append_v_w(qc, v0, v1) + circuits.append(qc) + return circuits + + +def _create_cliff_2q_layer_1(): + """Layer 1 consists of one of the following: + - nothing + - cx(0,1) followed by 0/1/2 V gates on each qubit + - cx(0,1), cx(1,0) followed by 0/1/2 V gates on each qubit + - cx(0,1), cx(1,0), cx(0,1) + Number of Cliffords == 20.""" + circuits = [QuantumCircuit(2)] # identity at the beginning + + v_w_gates = ["i", "v", "w"] + for v0, v1 in itertools.product(v_w_gates, v_w_gates): + qc = QuantumCircuit(2) + qc.cx(0, 1) + _append_v_w(qc, v0, v1) + circuits.append(qc) + + for v0, v1 in itertools.product(v_w_gates, v_w_gates): + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cx(1, 0) + _append_v_w(qc, v0, v1) + circuits.append(qc) + + qc = QuantumCircuit(2) # swap at the end + qc.cx(0, 1) + qc.cx(1, 0) + qc.cx(0, 1) + circuits.append(qc) + return circuits + + +def _create_cliff_2q_layer_2(): + """Layer 2 consists of a Pauli gate on each qubit {Id, X, Y, Z}. + Number of Cliffords == 16.""" + circuits = [] + pauli = ("i", XGate(), YGate(), ZGate()) + for p0, p1 in itertools.product(pauli, pauli): + qc = QuantumCircuit(2) + if p0 != "i": + qc.append(p0, [0]) + if p1 != "i": + qc.append(p1, [1]) + circuits.append(qc) + return circuits + + +_CLIFFORD_LAYER = ( + _create_cliff_2q_layer_0(), + _create_cliff_2q_layer_1(), + _create_cliff_2q_layer_2(), +) +_NUM_LAYER_0 = 36 +_NUM_LAYER_1 = 20 +_NUM_LAYER_2 = 16 + + +def _num_from_layer_indices(triplet: Tuple[Integral, Integral, Integral]) -> Integral: + """Return the clifford number corresponding to the input triplet.""" + num = triplet[0] * _NUM_LAYER_1 * _NUM_LAYER_2 + triplet[1] * _NUM_LAYER_2 + triplet[2] + return num + + +def _layer_indices_from_num(num: Integral) -> Tuple[Integral, Integral, Integral]: + """Return the triplet of layer indices corresponding to the input number.""" + idx2 = num % _NUM_LAYER_2 + num = num // _NUM_LAYER_2 + idx1 = num % _NUM_LAYER_1 + idx0 = num // _NUM_LAYER_1 + return idx0, idx1, idx2 + + +def _load_clifford_compose_2q_gate(): + dirname = os.path.dirname(__file__) + data = scipy.sparse.load_npz(f"{dirname}/data/clifford_compose_2q_sparse.npz") + return data + + +def _load_clifford_inverse_2q(): + dirname = os.path.dirname(__file__) + data = np.load(f"{dirname}/data/clifford_inverse_2q.npz") + return data["table"] + + +_CLIFFORD_COMPOSE_2Q_GATE = _load_clifford_compose_2q_gate() +_CLIFFORD_INVERSE_2Q = _load_clifford_inverse_2q() diff --git a/qiskit_experiments/library/randomized_benchmarking/data/clifford_compose_1q.npz b/qiskit_experiments/library/randomized_benchmarking/data/clifford_compose_1q.npz new file mode 100644 index 0000000000..5794227a06 Binary files /dev/null and b/qiskit_experiments/library/randomized_benchmarking/data/clifford_compose_1q.npz differ diff --git a/qiskit_experiments/library/randomized_benchmarking/data/clifford_compose_2q_sparse.npz b/qiskit_experiments/library/randomized_benchmarking/data/clifford_compose_2q_sparse.npz new file mode 100644 index 0000000000..19439b7be6 Binary files /dev/null and b/qiskit_experiments/library/randomized_benchmarking/data/clifford_compose_2q_sparse.npz differ diff --git a/qiskit_experiments/library/randomized_benchmarking/data/clifford_inverse_1q.npz b/qiskit_experiments/library/randomized_benchmarking/data/clifford_inverse_1q.npz new file mode 100644 index 0000000000..dedef02825 Binary files /dev/null and b/qiskit_experiments/library/randomized_benchmarking/data/clifford_inverse_1q.npz differ diff --git a/qiskit_experiments/library/randomized_benchmarking/data/clifford_inverse_2q.npz b/qiskit_experiments/library/randomized_benchmarking/data/clifford_inverse_2q.npz new file mode 100644 index 0000000000..62a8314f15 Binary files /dev/null and b/qiskit_experiments/library/randomized_benchmarking/data/clifford_inverse_2q.npz differ diff --git a/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py b/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py new file mode 100644 index 0000000000..7f152cbaa6 --- /dev/null +++ b/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py @@ -0,0 +1,187 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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 is a stand-alone script for generating the npz files in +:mod:`~qiskit_experiment.library.randomized_benchmarking.clifford_utils.data` directory. + +The script relies on the values of ``_CLIFF_SINGLE_GATE_MAP_2Q`` +in :mod:`~qiskit_experiment.library.randomized_benchmarking.clifford_utils` +so they must be set correctly before running the script. +""" +import itertools + +import numpy as np +import scipy.sparse + +from qiskit.circuit import QuantumCircuit +from qiskit.circuit.library import ( + IGate, + HGate, + SXdgGate, + SGate, + XGate, + SXGate, + YGate, + ZGate, + SdgGate, + CXGate, + CZGate, +) +from qiskit.quantum_info.operators.symplectic import Clifford +from qiskit_experiments.library.randomized_benchmarking.clifford_utils import ( + CliffordUtils, + _CLIFF_SINGLE_GATE_MAP_1Q, + _CLIFF_SINGLE_GATE_MAP_2Q, +) + +NUM_CLIFFORD_1Q = CliffordUtils.NUM_CLIFFORD_1_QUBIT +NUM_CLIFFORD_2Q = CliffordUtils.NUM_CLIFFORD_2_QUBIT + + +def _hash_cliff(cliff): + """Produce a hashable value that is unique for each different Clifford. This should only be + used internally when the classes being hashed are under our control, because classes of this + type are mutable.""" + table = cliff.table + abits = np.packbits(table.array) + pbits = np.packbits(table.phase) + return abits.tobytes(), pbits.tobytes() + + +_TO_CLIFF_1Q = {i: CliffordUtils.clifford_1_qubit(i) for i in range(NUM_CLIFFORD_1Q)} +_TO_INT_1Q = {_hash_cliff(cliff): i for i, cliff in _TO_CLIFF_1Q.items()} + + +def gen_clifford_inverse_1q(): + """Generate table data for integer 1Q Clifford inversion""" + invs = np.zeros(NUM_CLIFFORD_1Q, dtype=int) + for i in range(NUM_CLIFFORD_1Q): + invs[i] = _TO_INT_1Q[_hash_cliff(_TO_CLIFF_1Q[i].adjoint())] + assert all(sorted(invs) == np.arange(0, NUM_CLIFFORD_1Q)) + return invs + + +def gen_clifford_compose_1q(): + """Generate table data for integer 1Q Clifford composition.""" + products = np.zeros((NUM_CLIFFORD_1Q, NUM_CLIFFORD_1Q), dtype=int) + for i in range(NUM_CLIFFORD_1Q): + for j in range(NUM_CLIFFORD_1Q): + cliff = _TO_CLIFF_1Q[i].compose(_TO_CLIFF_1Q[j]) + products[i, j] = _TO_INT_1Q[_hash_cliff(cliff)] + assert all(sorted(products[i]) == np.arange(0, NUM_CLIFFORD_1Q)) + return products + + +_TO_CLIFF_2Q = {i: CliffordUtils.clifford_2_qubit(i) for i in range(NUM_CLIFFORD_2Q)} +_TO_INT_2Q = {_hash_cliff(cliff): i for i, cliff in _TO_CLIFF_2Q.items()} + + +def gen_clifford_inverse_2q(): + """Generate table data for integer 2Q Clifford inversion""" + invs = np.zeros(NUM_CLIFFORD_2Q, dtype=int) + for i in range(NUM_CLIFFORD_2Q): + invs[i] = _TO_INT_2Q[_hash_cliff(_TO_CLIFF_2Q[i].adjoint())] + assert all(sorted(invs) == np.arange(0, NUM_CLIFFORD_2Q)) + return invs + + +def gen_clifford_compose_2q_gate(): + """Generate table data for integer 2Q Clifford composition. + + Note that the full compose table of all-Cliffords by all-Cliffords is *NOT* created. + Instead, only the Cliffords that consist of a single gate defined in ``_CLIFF_SINGLE_GATE_MAP_2Q`` + are considered as the target Clifford. That means the compose table of + all-Cliffords by single-gate-cliffords is created. + It is sufficient because every Clifford on the right hand side of Clifford composition + can be broken down into a sequence of single gate Cliffords. + This greatly reduces the storage space for the array of + composition results (from O(n^2) to O(n)), where n is the number of Cliffords. + """ + products = scipy.sparse.lil_matrix((NUM_CLIFFORD_2Q, NUM_CLIFFORD_2Q), dtype=int) + for lhs in range(NUM_CLIFFORD_2Q): + for rhs in _CLIFF_SINGLE_GATE_MAP_2Q.values(): + composed = _TO_CLIFF_2Q[lhs].compose(_TO_CLIFF_2Q[rhs]) + products[lhs, rhs] = _TO_INT_2Q[_hash_cliff(composed)] + return products.tocsr() + + +_GATE_LIST_1Q = [ + IGate(), + HGate(), + SXdgGate(), + SGate(), + XGate(), + SXGate(), + YGate(), + ZGate(), + SdgGate(), +] + + +def gen_cliff_single_1q_gate_map(): + """ + Generates a dict mapping numbers to 1Q Cliffords, which is set to ``_CLIFF_SINGLE_GATE_MAP_1Q`` + in :mod:`~qiskit_experiment.library.randomized_benchmarking.clifford_utils`. + Based on it, we build a mapping from every single-gate-clifford to its number. + The mapping actually looks like {(gate, (0, )): num}. + """ + table = {} + for gate in _GATE_LIST_1Q: + qc = QuantumCircuit(1) + qc.append(gate, [0]) + num = _TO_INT_1Q[_hash_cliff(Clifford(qc))] + table[(gate.name, (0,))] = num + + return table + + +def gen_cliff_single_2q_gate_map(): + """ + Generates a dict mapping numbers to 2Q Cliffords, which is set to ``_CLIFF_SINGLE_GATE_MAP_2Q`` + in :mod:`~qiskit_experiment.library.randomized_benchmarking.clifford_utils`. + Based on it, we build a mapping from every single-gate-clifford to its number. + The mapping actually looks like {(gate, (0, 1)): num}. + """ + gate_list_2q = [ + CXGate(), + CZGate(), + ] + table = {} + for gate, qubit in itertools.product(_GATE_LIST_1Q, [0, 1]): + qc = QuantumCircuit(2) + qc.append(gate, [qubit]) + num = _TO_INT_2Q[_hash_cliff(Clifford(qc))] + table[(gate.name, (qubit,))] = num + + for gate, qubits in itertools.product(gate_list_2q, [(0, 1), (1, 0)]): + qc = QuantumCircuit(2) + qc.append(gate, qubits) + num = _TO_INT_2Q[_hash_cliff(Clifford(qc))] + table[(gate.name, qubits)] = num + + return table + + +if __name__ == "__main__": + if _CLIFF_SINGLE_GATE_MAP_1Q != gen_cliff_single_1q_gate_map(): + raise Exception( + "_CLIFF_SINGLE_GATE_MAP_1Q must be generated by gen_cliff_single_1q_gate_map()" + ) + np.savez_compressed("clifford_inverse_1q.npz", table=gen_clifford_inverse_1q()) + np.savez_compressed("clifford_compose_1q.npz", table=gen_clifford_compose_1q()) + + if _CLIFF_SINGLE_GATE_MAP_2Q != gen_cliff_single_2q_gate_map(): + raise Exception( + "_CLIFF_SINGLE_GATE_MAP_2Q must be generated by gen_cliff_single_2q_gate_map()" + ) + np.savez_compressed("clifford_inverse_2q.npz", table=gen_clifford_inverse_2q()) + scipy.sparse.save_npz("clifford_compose_2q_sparse.npz", gen_clifford_compose_2q_gate()) diff --git a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py index 62b03dfa85..004c37c59c 100644 --- a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py @@ -19,12 +19,12 @@ from qiskit import QuantumCircuit from qiskit.circuit import Instruction -from qiskit.quantum_info import Clifford from qiskit.exceptions import QiskitError from qiskit.providers.backend import Backend - -from .rb_experiment import StandardRB, SequenceElementType +from qiskit.quantum_info import Clifford +from .clifford_utils import num_from_circuit from .interleaved_rb_analysis import InterleavedRBAnalysis +from .rb_experiment import StandardRB, SequenceElementType class InterleavedRB(StandardRB): @@ -85,6 +85,11 @@ def __init__( raise QiskitError( f"Interleaved element {interleaved_element.name} could not be converted to Clifford." ) from err + # Convert interleaved element to integer for speed + num_qubits = len(qubits) + if len(qubits) <= 2: + interleaved_circ = self._interleaved_elem.to_circuit() + self._interleaved_elem = num_from_circuit[num_qubits](interleaved_circ) # Convert interleaved element to operation self._interleaved_op = interleaved_element if not isinstance(interleaved_element, Instruction): diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index 4dba2e5482..3ed55386b0 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -29,16 +29,19 @@ from qiskit_experiments.framework import BaseExperiment, Options from qiskit_experiments.framework.restless_mixin import RestlessMixin from .clifford_utils import ( - CliffordUtils, _clifford_1q_int_to_instruction, _clifford_2q_int_to_instruction, + compose_1q, + compose_2q, + inverse_1q, + inverse_2q, ) from .rb_analysis import RBAnalysis LOG = logging.getLogger(__name__) -SequenceElementType = Union[Clifford, Integral] +SequenceElementType = Union[Clifford, Integral, QuantumCircuit] class StandardRB(BaseExperiment, RestlessMixin): @@ -200,8 +203,7 @@ def _sequences_to_circuits( circ.barrier(qubits) # Compute inverse, compute only the difference from the previous shorter sequence - for elem in seq[len(prev_seq) :]: - prev_elem = self.__compose_clifford(prev_elem, elem) + prev_elem = self.__compose_clifford_seq(prev_elem, seq[len(prev_seq) :]) prev_seq = seq inv = self.__adjoint_clifford(prev_elem) @@ -212,13 +214,14 @@ def _sequences_to_circuits( def __sample_sequence(self, length: int, rng: Generator) -> Sequence[SequenceElementType]: # Sample a RB sequence with the given length. - # Return integer instead of Clifford object for 1 or 2 qubit case for speed + # Return integer instead of Clifford object for 1 or 2 qubits case for speed if self.num_qubits == 1: return rng.integers(24, size=length) if self.num_qubits == 2: return rng.integers(11520, size=length) - - return [random_clifford(self.num_qubits, rng) for _ in range(length)] + # Return circuit object instead of Clifford object for 3 or more qubits case for speed + # TODO: Revisit after terra#7269, #7483, #8585 + return [random_clifford(self.num_qubits, rng).to_circuit() for _ in range(length)] def _to_instruction(self, elem: SequenceElementType) -> Instruction: # TODO: basis transformation in 1Q (and 2Q) cases for speed @@ -235,31 +238,38 @@ def __identity_clifford(self) -> SequenceElementType: return 0 return Clifford(np.eye(2 * self.num_qubits)) + def __compose_clifford_seq( + self, org: SequenceElementType, seq: Sequence[SequenceElementType] + ) -> SequenceElementType: + if self.num_qubits <= 2: + new = org + for elem in seq: + new = self.__compose_clifford(new, elem) + return new + # 3 or more qubits: compose Clifford from circuits for speed + # TODO: Revisit after terra#7269, #7483, #8585 + circ = QuantumCircuit(self.num_qubits) + for elem in seq: + circ.compose(elem, inplace=True) + return org.compose(Clifford.from_circuit(circ)) + def __compose_clifford( self, lop: SequenceElementType, rop: SequenceElementType ) -> SequenceElementType: - # TODO: Speed up 1Q (and 2Q) cases using integer clifford composition - # Integer clifford composition has not yet supported if self.num_qubits == 1: - if isinstance(lop, Integral): - lop = CliffordUtils.clifford_1_qubit(lop) - if isinstance(rop, Integral): - rop = CliffordUtils.clifford_1_qubit(rop) + return compose_1q(lop, rop) if self.num_qubits == 2: - if isinstance(lop, Integral): - lop = CliffordUtils.clifford_2_qubit(lop) - if isinstance(rop, Integral): - rop = CliffordUtils.clifford_2_qubit(rop) + return compose_2q(lop, rop) return lop.compose(rop) def __adjoint_clifford(self, op: SequenceElementType) -> SequenceElementType: - # TODO: Speed up 1Q and 2Q cases using integer clifford inversion - # Integer clifford inversion has not yet supported if isinstance(op, Integral): if self.num_qubits == 1: - return CliffordUtils.clifford_1_qubit(op).adjoint() + return inverse_1q(op) if self.num_qubits == 2: - return CliffordUtils.clifford_2_qubit(op).adjoint() + return inverse_2q(op) + if isinstance(op, QuantumCircuit): + return Clifford.from_circuit(op).adjoint() return op.adjoint() def _transpiled_circuits(self) -> List[QuantumCircuit]: diff --git a/test/library/randomized_benchmarking/test_clifford_utils.py b/test/library/randomized_benchmarking/test_clifford_utils.py new file mode 100644 index 0000000000..000ed3c36c --- /dev/null +++ b/test/library/randomized_benchmarking/test_clifford_utils.py @@ -0,0 +1,197 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +""" +A Tester for the Clifford utilities +""" +from test.base import QiskitExperimentsTestCase + +import numpy as np +from ddt import ddt +from numpy.random import default_rng + +from qiskit import QuantumCircuit +from qiskit.circuit.library import ( + IGate, + XGate, + YGate, + ZGate, + HGate, + SGate, + SdgGate, + SXGate, + RZGate, +) +from qiskit.quantum_info import Operator, Clifford +from qiskit_experiments.library.randomized_benchmarking.clifford_utils import ( + CliffordUtils, + num_from_1q_circuit, + num_from_2q_circuit, + compose_1q, + compose_2q, + inverse_1q, + inverse_2q, + _num_from_layer_indices, + _layer_indices_from_num, + _CLIFFORD_LAYER, +) + + +@ddt +class TestCliffordUtils(QiskitExperimentsTestCase): + """A test for the Clifford manipulations, including number to and from Clifford mapping""" + + basis_gates = ["rz", "sx", "cx"] + seed = 123 + + def test_clifford_1_qubit_generation(self): + """Verify 1-qubit clifford indeed generates the correct group""" + clifford_dicts = [ + {"stabilizer": ["+Z"], "destabilizer": ["+X"]}, + {"stabilizer": ["+X"], "destabilizer": ["+Z"]}, + {"stabilizer": ["+Y"], "destabilizer": ["+X"]}, + {"stabilizer": ["+X"], "destabilizer": ["+Y"]}, + {"stabilizer": ["+Z"], "destabilizer": ["+Y"]}, + {"stabilizer": ["+Y"], "destabilizer": ["+Z"]}, + {"stabilizer": ["-Z"], "destabilizer": ["+X"]}, + {"stabilizer": ["+X"], "destabilizer": ["-Z"]}, + {"stabilizer": ["-Y"], "destabilizer": ["+X"]}, + {"stabilizer": ["+X"], "destabilizer": ["-Y"]}, + {"stabilizer": ["-Z"], "destabilizer": ["-Y"]}, + {"stabilizer": ["-Y"], "destabilizer": ["-Z"]}, + {"stabilizer": ["-Z"], "destabilizer": ["-X"]}, + {"stabilizer": ["-X"], "destabilizer": ["-Z"]}, + {"stabilizer": ["+Y"], "destabilizer": ["-X"]}, + {"stabilizer": ["-X"], "destabilizer": ["+Y"]}, + {"stabilizer": ["-Z"], "destabilizer": ["+Y"]}, + {"stabilizer": ["+Y"], "destabilizer": ["-Z"]}, + {"stabilizer": ["+Z"], "destabilizer": ["-X"]}, + {"stabilizer": ["-X"], "destabilizer": ["+Z"]}, + {"stabilizer": ["-Y"], "destabilizer": ["-X"]}, + {"stabilizer": ["-X"], "destabilizer": ["-Y"]}, + {"stabilizer": ["+Z"], "destabilizer": ["-Y"]}, + {"stabilizer": ["-Y"], "destabilizer": ["+Z"]}, + ] + cliffords = [Clifford.from_dict(i) for i in clifford_dicts] + for n in range(24): + clifford = CliffordUtils.clifford_1_qubit(n) + self.assertEqual(clifford, cliffords[n]) + + def test_number_to_clifford_mapping_single_gate(self): + """Test that the methods num_from_1q_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), + ] + general_cliff_list = [ + IGate(), + HGate(), + SdgGate(), + SGate(), + XGate(), + SXGate(), + YGate(), + ZGate(), + ] + for inst in transpiled_cliff_list + general_cliff_list: + qc_from_inst = QuantumCircuit(1) + qc_from_inst.append(inst, [0]) + num = num_from_1q_circuit(qc_from_inst) + qc_from_num = CliffordUtils.clifford_1_qubit_circuit(num) + self.assertTrue(Operator(qc_from_num).equiv(Operator(qc_from_inst))) + + def test_number_to_clifford_mapping_2q(self): + """Test if num -> circuit -> num round-trip succeeds for 2Q Cliffords.""" + for i in range(CliffordUtils.NUM_CLIFFORD_2_QUBIT): + qc = CliffordUtils.clifford_2_qubit_circuit(i) + num = num_from_2q_circuit(qc) + self.assertEqual(i, num) + + def test_compose_by_num_1q(self): + """Compare compose using num and Clifford to compose using two Cliffords, for a single qubit""" + num_tests = 50 + rng = default_rng(seed=self.seed) + for _ in range(num_tests): + num1 = rng.integers(CliffordUtils.NUM_CLIFFORD_1_QUBIT) + num2 = rng.integers(CliffordUtils.NUM_CLIFFORD_1_QUBIT) + cliff1 = CliffordUtils.clifford_1_qubit(num1) + cliff2 = CliffordUtils.clifford_1_qubit(num2) + clifford_expected = cliff1.compose(cliff2) + clifford_from_num = CliffordUtils.clifford_1_qubit(compose_1q(num1, num2)) + clifford_from_circuit = Clifford(cliff1.to_circuit().compose(cliff2.to_circuit())) + self.assertEqual(clifford_expected, clifford_from_num) + self.assertEqual(clifford_expected, clifford_from_circuit) + + def test_compose_by_num_2q(self): + """Compare compose using num and Clifford to compose using two Cliffords, for two qubits""" + num_tests = 100 + rng = default_rng(seed=self.seed) + for _ in range(num_tests): + num1 = rng.integers(CliffordUtils.NUM_CLIFFORD_2_QUBIT) + num2 = rng.integers(CliffordUtils.NUM_CLIFFORD_2_QUBIT) + cliff1 = CliffordUtils.clifford_2_qubit(num1) + cliff2 = CliffordUtils.clifford_2_qubit(num2) + clifford_expected = cliff1.compose(cliff2) + clifford_from_num = CliffordUtils.clifford_2_qubit(compose_2q(num1, num2)) + clifford_from_circuit = Clifford(cliff1.to_circuit().compose(cliff2.to_circuit())) + self.assertEqual(clifford_expected, clifford_from_num) + self.assertEqual(clifford_expected, clifford_from_circuit) + + def test_inverse_by_num_1q(self): + """Compare inverse using num to inverse using Clifford""" + num_tests = 24 + for num in range(num_tests): + cliff = CliffordUtils.clifford_1_qubit(num) + clifford_expected = cliff.adjoint() + clifford_from_num = CliffordUtils.clifford_1_qubit(inverse_1q(num)) + clifford_from_circuit = Clifford(cliff.to_circuit().inverse()) + self.assertEqual(clifford_expected, clifford_from_num) + self.assertEqual(clifford_expected, clifford_from_circuit) + + def test_inverse_by_num_2q(self): + """Compare inverse using num to inverse using Clifford""" + num_tests = 100 + rng = default_rng(seed=self.seed) + for _ in range(num_tests): + num = rng.integers(CliffordUtils.NUM_CLIFFORD_2_QUBIT) + cliff = CliffordUtils.clifford_2_qubit(num) + clifford_expected = cliff.adjoint() + clifford_from_num = CliffordUtils.clifford_2_qubit(inverse_2q(num)) + clifford_from_circuit = Clifford(cliff.to_circuit().inverse()) + self.assertEqual(clifford_expected, clifford_from_num) + self.assertEqual(clifford_expected, clifford_from_circuit) + + def test_num_layered_circuit_num_round_trip(self): + """Test if num -> circuit with layers -> num round-trip succeeds for 2Q Cliffords.""" + for i in range(CliffordUtils.NUM_CLIFFORD_2_QUBIT): + self.assertEqual(i, compose_2q(0, i)) + + def test_mapping_layers_to_num(self): + """Test the mapping from numbers to layer indices""" + for i in range(CliffordUtils.NUM_CLIFFORD_2_QUBIT): + indices = _layer_indices_from_num(i) + reverse_i = _num_from_layer_indices(indices) + self.assertEqual(i, reverse_i) + + def test_num_from_layer(self): + """Check if 2Q clifford from standard/layered circuit has a common integer representation.""" + for i in range(CliffordUtils.NUM_CLIFFORD_2_QUBIT): + standard = CliffordUtils.clifford_2_qubit(i) + circ = QuantumCircuit(2) + for layer, idx in enumerate(_layer_indices_from_num(i)): + circ.compose(_CLIFFORD_LAYER[layer][idx], inplace=True) + layered = Clifford(circ) + self.assertEqual(standard, layered) diff --git a/test/library/randomized_benchmarking/test_randomized_benchmarking.py b/test/library/randomized_benchmarking/test_randomized_benchmarking.py index 6bd151e386..0a5a7742e7 100644 --- a/test/library/randomized_benchmarking/test_randomized_benchmarking.py +++ b/test/library/randomized_benchmarking/test_randomized_benchmarking.py @@ -289,6 +289,24 @@ def test_expdata_serialization(self): class TestInterleavedRB(RBTestCase): """Test for interleaved RB.""" + def test_preserve_interleaved_circuit_element(self): + """Interleaved RB should not change a given interleaved circuit during RB circuit generation.""" + interleaved_circ = QuantumCircuit(2, name="bell_with_delay") + interleaved_circ.h(0) + interleaved_circ.delay(160, 0) + interleaved_circ.cx(0, 1) + + exp = rb.InterleavedRB( + interleaved_element=interleaved_circ, qubits=[2, 1], lengths=[1], num_samples=1 + ) + + circuits = exp.circuits() + # Get the first interleaved operation in the interleaved RB sequence + # assuming the standard sequence first and the interleaved sequence second + # and the interleaved element comes at 3: barrier-clifford-barrier-interleaved + actual = circuits[1][3].operation + self.assertEqual(interleaved_circ.name, actual.name) + @data([XGate(), [3], 4], [CXGate(), [4, 7], 5]) @unpack def test_interleaved_structure(self, interleaved_element, qubits, length):