diff --git a/MANIFEST.in b/MANIFEST.in index 1b30164d89..e90b69375d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ include LICENSE.txt include requirements.txt include qiskit_experiments/VERSION.txt +include qiskit_experiments/library/randomized_benchmarking/data/*.npz +exclude qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py index 985edbc0e4..99a1edd9c7 100644 --- a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py +++ b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -13,28 +13,136 @@ 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, Sequence +import numpy as np +import scipy.sparse from numpy.random import Generator, default_rng +from qiskit.circuit import CircuitInstruction, Qubit 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.compiler import transpile +from qiskit.exceptions import QiskitError from qiskit.quantum_info import Clifford, random_clifford +from qiskit_experiments.warnings import deprecated_function + + +_DATA_FOLDER = os.path.join(os.path.dirname(__file__), "data") + +_CLIFFORD_COMPOSE_1Q = np.load(f"{_DATA_FOLDER}/clifford_compose_1q.npz")["table"] +_CLIFFORD_INVERSE_1Q = np.load(f"{_DATA_FOLDER}/clifford_inverse_1q.npz")["table"] +_CLIFFORD_COMPOSE_2Q = scipy.sparse.load_npz(f"{_DATA_FOLDER}/clifford_compose_2q_sparse.npz") +_CLIFFORD_INVERSE_2Q = np.load(f"{_DATA_FOLDER}/clifford_inverse_2q.npz")["table"] + + +# Transpilation utilities +def _transpile_clifford_circuit( + circuit: QuantumCircuit, physical_qubits: Sequence[int] +) -> QuantumCircuit: + # Simplified transpile that only decomposes Clifford circuits and creates the layout. + return _apply_qubit_layout(_decompose_clifford_ops(circuit), physical_qubits=physical_qubits) + + +def _decompose_clifford_ops(circuit: QuantumCircuit) -> QuantumCircuit: + # Simplified QuantumCircuit.decompose, which decomposes only Clifford ops + # Note that the resulting circuit depends on the input circuit, + # that means the changes on the input circuit may affect the resulting circuit. + # For example, the resulting circuit shares the parameter_table of the input circuit, + res = circuit.copy_empty_like() + res._parameter_table = circuit._parameter_table + for inst in circuit: + if inst.operation.name.startswith("Clifford"): # Decompose + rule = inst.operation.definition.data + if len(rule) == 1 and len(inst.qubits) == len(rule[0].qubits): + if inst.operation.definition.global_phase: + res.global_phase += inst.operation.definition.global_phase + res._data.append( + CircuitInstruction( + operation=rule[0].operation, + qubits=inst.qubits, + clbits=inst.clbits, + ) + ) + else: + _circuit_compose(res, inst.operation.definition, qubits=inst.qubits) + else: # Keep the original instruction + res._data.append(inst) + return res + + +def _apply_qubit_layout(circuit: QuantumCircuit, physical_qubits: Sequence[int]) -> QuantumCircuit: + # Mapping qubits in circuit to physical qubits (layout) + res = QuantumCircuit(1 + max(physical_qubits), name=circuit.name, metadata=circuit.metadata) + res.add_bits(circuit.clbits) + for reg in circuit.cregs: + res.add_register(reg) + _circuit_compose(res, circuit, qubits=physical_qubits) + res._parameter_table = circuit._parameter_table + return res + + +def _circuit_compose( + self: QuantumCircuit, other: QuantumCircuit, qubits: Sequence[Union[Qubit, int]] +) -> QuantumCircuit: + # Simplified QuantumCircuit.compose with clbits=None, front=False, inplace=True, wrap=False + # without any validation, parameter_table/calibrations updates and copy of operations + # The input circuit `self` is changed inplace. + qubit_map = { + other.qubits[i]: (self.qubits[q] if isinstance(q, int) else q) for i, q in enumerate(qubits) + } + for instr in other: + self._data.append( + CircuitInstruction( + operation=instr.operation, + qubits=[qubit_map[q] for q in instr.qubits], + clbits=instr.clbits, + ), + ) + self.global_phase += other.global_phase + return self + + +def _truncate_inactive_qubits( + circ: QuantumCircuit, active_qubits: Sequence[Qubit] +) -> QuantumCircuit: + res = QuantumCircuit(active_qubits, name=circ.name, metadata=circ.metadata) + for inst in circ: + if all(q in active_qubits for q in inst.qubits): + res.append(inst) + res.calibrations = circ.calibrations + return res + + +def _synthesize_clifford_circuit( + circuit: QuantumCircuit, basis_gates: Tuple[str] +) -> QuantumCircuit: + # synthesizes clifford circuits using given basis gates, for use during + # custom transpilation during RB circuit generation. + return transpile(circuit, basis_gates=list(basis_gates), optimization_level=0) @lru_cache(maxsize=None) -def _clifford_1q_int_to_instruction(num: Integral) -> Instruction: - return CliffordUtils.clifford_1_qubit_circuit(num).to_instruction() +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() @lru_cache(maxsize=11520) -def _clifford_2q_int_to_instruction(num: Integral) -> Instruction: - return CliffordUtils.clifford_2_qubit_circuit(num).to_instruction() +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() +# The classes VGate and WGate are not actually used in the code - we leave them here to give +# a better understanding of the composition of the layers for 2-qubit Cliffords. class VGate(Gate): """V Gate used in Clifford synthesis.""" @@ -71,7 +179,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), @@ -94,53 +202,53 @@ def clifford_2_qubit(cls, num): """ return Clifford(cls.clifford_2_qubit_circuit(num), validate=False) + @deprecated_function() + @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: - return random_clifford(num_qubits, seed=rng) - if rng is None: rng = default_rng() - - if isinstance(rng, int): + elif isinstance(rng, int): 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] - else: - samples = rng.integers(11520, size=size) - return [Clifford(self.clifford_2_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] + if num_qubits == 2: + samples = rng.integers(cls.NUM_CLIFFORD_2_QUBIT, size=size) + return [Clifford(cls.clifford_2_qubit_circuit(i), validate=False) for i in samples] + return [random_clifford(num_qubits, seed=rng) for _ in range(size)] + + @deprecated_function() + @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: - return [random_clifford(num_qubits, seed=rng).to_circuit() for _ in range(size)] - if rng is None: rng = default_rng() - - if isinstance(rng, int): + elif isinstance(rng, int): 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] - else: - samples = rng.integers(11520, size=size) - return [self.clifford_2_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] + if num_qubits == 2: + samples = rng.integers(cls.NUM_CLIFFORD_2_QUBIT, size=size) + return [cls.clifford_2_qubit_circuit(i) for i in samples] + + return [random_clifford(num_qubits, seed=rng).to_circuit() for _ in range(size)] @classmethod @lru_cache(maxsize=24) - def clifford_1_qubit_circuit(cls, num): + def clifford_1_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str, ...]] = None): """Return the 1-qubit clifford circuit corresponding to `num` where `num` is between 0 and 23. """ - unpacked = cls._unpack_num(num, (2, 3, 4)) + unpacked = cls._unpack_num(num, cls.CLIFFORD_1_QUBIT_SIG) i, j, p = unpacked[0], unpacked[1], unpacked[2] qc = QuantumCircuit(1, name=f"Clifford-1Q({num})") if i == 1: @@ -156,63 +264,24 @@ def clifford_1_qubit_circuit(cls, num): if p == 3: qc.z(0) + if basis_gates: + qc = _synthesize_clifford_circuit(qc, basis_gates) + return qc @classmethod @lru_cache(maxsize=11520) - def clifford_2_qubit_circuit(cls, num): + def clifford_2_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str, ...]] = None): """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)): + if basis_gates: + layer_circ = _transformed_clifford_layer(layer, idx, basis_gates) + else: + layer_circ = _CLIFFORD_LAYER[layer][idx] + _circuit_compose(qc, layer_circ, qubits=(0, 1)) return qc @@ -229,16 +298,289 @@ 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 + +# Constant mapping from 1Q single Clifford gate to 1Q Clifford numerical identifier. +# This table must be generated using `data.generate_clifford_data.gen_cliff_single_1q_gate_map`, or, +# equivalently, correspond to the ordering implicitly defined by CliffUtils.clifford_1_qubit_circuit. +_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, +} +# Constant mapping from 2Q single Clifford gate to 2Q Clifford numerical identifier. +# This table must be generated using `data.generate_clifford_data.gen_cliff_single_2q_gate_map`, or, +# equivalently, correspond to the ordering defined by _layer_indices_from_num and _CLIFFORD_LAYER. +_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 a 1-qubit clifford integer.""" + 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 :const:`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 + + +######## +# 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 a 2-qubit clifford integer.""" + 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) + + +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) + lhs = _CLIFFORD_COMPOSE_2Q[lhs, rhs] + 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 + + +@lru_cache(maxsize=None) +def _transformed_clifford_layer( + layer: int, index: Integral, basis_gates: Tuple[str, ...] +) -> QuantumCircuit: + # Return the index-th quantum circuit of the layer translated with the basis_gates. + # The result is cached for speed. + return _synthesize_clifford_circuit(_CLIFFORD_LAYER[layer][index], basis_gates) + + +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 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..8da50467cb --- /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 +``~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. + +Note: Terra >= 0.22 is required to run this 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.""" + return np.packbits(cliff.tableau).tobytes() + + +_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 _CLIFF_1Q.items()} + + +def gen_clifford_inverse_1q(): + """Generate table data for integer 1Q Clifford inversion""" + invs = np.empty(NUM_CLIFFORD_1Q, dtype=int) + for i, cliff_i in _CLIFF_1Q.items(): + invs[i] = _TO_INT_1Q[_hash_cliff(cliff_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.empty((NUM_CLIFFORD_1Q, NUM_CLIFFORD_1Q), dtype=int) + for i, cliff_i in _CLIFF_1Q.items(): + for j, cliff_j in _CLIFF_1Q.items(): + cliff = cliff_i.compose(cliff_j) + products[i, j] = _TO_INT_1Q[_hash_cliff(cliff)] + assert all(sorted(products[i]) == np.arange(0, NUM_CLIFFORD_1Q)) + return products + + +_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 _CLIFF_2Q.items()} + + +def gen_clifford_inverse_2q(): + """Generate table data for integer 2Q Clifford inversion""" + invs = np.empty(NUM_CLIFFORD_2Q, dtype=int) + for i, cliff_i in _CLIFF_2Q.items(): + invs[i] = _TO_INT_2Q[_hash_cliff(cliff_i.adjoint())] + assert all(sorted(invs) == np.arange(0, NUM_CLIFFORD_2Q)) + return invs + + +def gen_clifford_compose_2q_gate(): + """Generate data for a 2Q Clifford composition table. + + Cliffords are represented as integers between 0 and 11519. Note that the full composition table + would require :math:`11520^2` elements and is therefore *NOT* generated, as that would take + more than 100MB. Instead, we sparsely populate the composition table only for RHS elements + from a specific set of basis gates defined by the values of ``_CLIFF_SINGLE_GATE_MAP_2Q``. + This is sufficient because when composing two arbitrary Cliffords, we can decompose the RHS + into these basis gates (which needs to be done anyways), and subsequently compute the product + in multiple steps using this sparse table. + """ + products = scipy.sparse.lil_matrix((NUM_CLIFFORD_2Q, NUM_CLIFFORD_2Q), dtype=int) + for lhs, cliff_lhs in _CLIFF_2Q.items(): + for rhs in _CLIFF_SINGLE_GATE_MAP_2Q.values(): + composed = cliff_lhs.compose(_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 + to be used as the value for ``_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 + to be used as the value for ``_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..b454fbe1c7 100644 --- a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py @@ -12,19 +12,23 @@ """ Interleaved RB Experiment class. """ -from typing import Union, Iterable, Optional, List, Sequence +import warnings +from typing import Union, Iterable, Optional, List, Sequence, Tuple from numpy.random import Generator from numpy.random.bit_generator import BitGenerator, SeedSequence -from qiskit import QuantumCircuit -from qiskit.circuit import Instruction -from qiskit.quantum_info import Clifford +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 .rb_experiment import StandardRB, SequenceElementType +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 num_from_1q_circuit, num_from_2q_circuit from .interleaved_rb_analysis import InterleavedRBAnalysis +from .rb_experiment import StandardRB, SequenceElementType class InterleavedRB(StandardRB): @@ -50,7 +54,7 @@ class InterleavedRB(StandardRB): def __init__( self, - interleaved_element: Union[QuantumCircuit, Instruction, Clifford], + interleaved_element: Union[QuantumCircuit, Gate, Delay, Clifford], qubits: Sequence[int], lengths: Iterable[int], backend: Optional[Backend] = None, @@ -62,12 +66,18 @@ def __init__( Args: interleaved_element: The element to interleave, - given either as a group element or as an instruction/circuit + 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 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` + is useful to obtain valid delays). + Parameterized circuits/instructions are not allowed. qubits: list of physical qubits for the experiment. lengths: A list of RB sequences lengths. backend: The backend to run the experiment on. - num_samples: Number of samples to generate for each - sequence length + num_samples: Number of samples to generate for each sequence length. seed: Optional, seed used to initialize ``numpy.random.default_rng``. when generating circuits. The ``default_rng`` will be initialized with this seed value everytime :meth:`circuits` is called. @@ -77,18 +87,51 @@ def __init__( Clifford samples to shorter sequences. Raises: - QiskitError: the interleaved_element is not convertible to Clifford object. + QiskitError: If the ``interleaved_element`` is invalid because: + * it has different number of qubits from the qubits argument + * it is not convertible to Clifford object + * it has an invalid delay (e.g. violating the timing constraints of the backend) """ + # Validations of interleaved_element + # - validate number of qubits of interleaved_element + if len(qubits) != interleaved_element.num_qubits: + raise QiskitError( + f"Mismatch in number of qubits between qubits ({len(qubits)})" + f" and interleaved element ({interleaved_element.num_qubits})." + ) + # - validate if interleaved_element is Clifford try: - self._interleaved_elem = Clifford(interleaved_element) + interleaved_clifford = Clifford(interleaved_element) except QiskitError as err: raise QiskitError( f"Interleaved element {interleaved_element.name} could not be converted to Clifford." ) from err - # Convert interleaved element to operation - self._interleaved_op = interleaved_element - if not isinstance(interleaved_element, Instruction): - self._interleaved_op = interleaved_element.to_instruction() + # - validate delays in interleaved_element + delay_ops = [] + if isinstance(interleaved_element, Delay): + delay_ops = [interleaved_element] + elif isinstance(interleaved_element, QuantumCircuit): + delay_ops = [delay.operation for delay in interleaved_element.get_instructions("delay")] + if delay_ops: + timing = BackendTiming(backend) + for delay_op in delay_ops: + if delay_op.unit != timing.delay_unit: + raise QiskitError( + f"Interleaved delay for backend {backend} must have time unit {timing.delay_unit}." + " Use BackendTiming to set valid duration and unit for delays." + ) + if timing.delay_unit == "dt": + valid_duration = timing.round_delay(samples=delay_op.duration) + if delay_op.duration != valid_duration: + raise QiskitError( + f"Interleaved delay duration {delay_op.duration}[dt] violates the timing" + f" constraints of the backend {backend}. It could be {valid_duration}[dt]." + " Use BackendTiming to set valid duration for delays." + ) + # Warnings + if isinstance(interleaved_element, QuantumCircuit) and interleaved_element.calibrations: + warnings.warn("Calibrations in interleaved circuit are ignored", UserWarning) + super().__init__( qubits, lengths, @@ -97,6 +140,15 @@ def __init__( seed=seed, full_sampling=full_sampling, ) + # Convert interleaved element to integer for speed in 1Q or 2Q case + if self.num_qubits == 1: + self._interleaved_cliff = num_from_1q_circuit(interleaved_clifford.to_circuit()) + elif self.num_qubits == 2: + self._interleaved_cliff = num_from_2q_circuit(interleaved_clifford.to_circuit()) + else: + self._interleaved_cliff = interleaved_clifford + self._interleaved_element = interleaved_element # Original interleaved element + self._interleaved_op = None # Transpiled interleaved element for speed self.analysis = InterleavedRBAnalysis() self.analysis.set_options(outcome="0" * self.num_qubits) @@ -105,13 +157,19 @@ def circuits(self) -> List[QuantumCircuit]: Returns: A list of :class:`QuantumCircuit`. + + Raises: + QiskitError: If the ``interleaved_element`` provided to the constructor + cannot be transpiled. """ + # Convert interleaved element to transpiled circuit operation and store it for speed + self.__set_up_interleaved_op() + # Build circuits of reference sequences reference_sequences = self._sample_sequences() reference_circuits = self._sequences_to_circuits(reference_sequences) for circ, seq in zip(reference_circuits, reference_sequences): circ.metadata = { - "experiment_type": self._type, "xval": len(seq), "group": "Clifford", "physical_qubits": self.physical_qubits, @@ -123,12 +181,11 @@ def circuits(self) -> List[QuantumCircuit]: new_seq = [] for elem in seq: new_seq.append(elem) - new_seq.append(self._interleaved_elem) + new_seq.append(self._interleaved_cliff) interleaved_sequences.append(new_seq) interleaved_circuits = self._sequences_to_circuits(interleaved_sequences) for circ, seq in zip(interleaved_circuits, reference_sequences): circ.metadata = { - "experiment_type": self._type, "xval": len(seq), # set length of the reference sequence "group": "Clifford", "physical_qubits": self.physical_qubits, @@ -136,8 +193,49 @@ def circuits(self) -> List[QuantumCircuit]: } return reference_circuits + interleaved_circuits - def _to_instruction(self, elem: SequenceElementType) -> Instruction: - if elem is self._interleaved_elem: + def _to_instruction( + self, elem: SequenceElementType, basis_gates: Optional[Tuple[str]] = None + ) -> Instruction: + if elem is self._interleaved_cliff: return self._interleaved_op - return super()._to_instruction(elem) + return super()._to_instruction(elem, basis_gates) + + def __set_up_interleaved_op(self) -> None: + # Convert interleaved element to transpiled circuit operation and store it for speed + self._interleaved_op = self._interleaved_element + basis_gates = self._get_basis_gates() + # Convert interleaved element to circuit + if isinstance(self._interleaved_op, Clifford): + self._interleaved_op = self._interleaved_op.to_circuit() + + if isinstance(self._interleaved_op, QuantumCircuit): + interleaved_circ = self._interleaved_op + elif isinstance(self._interleaved_op, Gate): + interleaved_circ = QuantumCircuit(self.num_qubits, name=self._interleaved_op.name) + interleaved_circ.append(self._interleaved_op, list(range(self.num_qubits))) + else: # Delay + interleaved_circ = [] + + if basis_gates and any(i.operation.name not in basis_gates for i in interleaved_circ): + # Transpile circuit with non-basis gates and remove idling qubits + try: + interleaved_circ = transpile( + interleaved_circ, self.backend, **vars(self.transpile_options) + ) + except TranspilerError as err: + raise QiskitError("Failed to transpile interleaved_element.") from err + interleaved_circ = _truncate_inactive_qubits( + interleaved_circ, active_qubits=interleaved_circ.qubits[: self.num_qubits] + ) + # Convert transpiled circuit to operation + if len(interleaved_circ) == 1: + self._interleaved_op = interleaved_circ.data[0].operation + else: + self._interleaved_op = interleaved_circ + + # Store interleaved operation as Instruction + if isinstance(self._interleaved_op, QuantumCircuit): + if not self._interleaved_op.name.startswith("Clifford"): + self._interleaved_op.name = f"Clifford-{self._interleaved_op.name}" + self._interleaved_op = self._interleaved_op.to_instruction() diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index 4dba2e5482..085aca4e4e 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -13,32 +13,41 @@ Standard RB Experiment class. """ import logging +import functools from collections import defaultdict from numbers import Integral -from typing import Union, Iterable, Optional, List, Sequence +from typing import Union, Iterable, Optional, List, Sequence, Tuple import numpy as np from numpy.random import Generator, default_rng from numpy.random.bit_generator import BitGenerator, SeedSequence -from qiskit.circuit import QuantumCircuit, Instruction +from qiskit.circuit import QuantumCircuit, Instruction, Barrier from qiskit.exceptions import QiskitError -from qiskit.providers.backend import Backend +from qiskit.providers import BackendV2Converter +from qiskit.providers.backend import Backend, BackendV1, BackendV2 +from qiskit.pulse.instruction_schedule_map import CalibrationPublisher from qiskit.quantum_info import Clifford from qiskit.quantum_info.random import random_clifford +from qiskit.transpiler import CouplingMap from qiskit_experiments.framework import BaseExperiment, Options from qiskit_experiments.framework.restless_mixin import RestlessMixin from .clifford_utils import ( CliffordUtils, + compose_1q, + compose_2q, + inverse_1q, + inverse_2q, _clifford_1q_int_to_instruction, _clifford_2q_int_to_instruction, + _transpile_clifford_circuit, ) from .rb_analysis import RBAnalysis LOG = logging.getLogger(__name__) -SequenceElementType = Union[Clifford, Integral] +SequenceElementType = Union[Clifford, Integral, QuantumCircuit] class StandardRB(BaseExperiment, RestlessMixin): @@ -136,6 +145,15 @@ def _default_experiment_options(cls) -> Options: return options + def _set_backend(self, backend: Backend): + """Set the backend V2 for RB experiments since RB experiments only support BackendV2 + except for simulators. If BackendV1 is provided, it is converted to V2 and stored. + """ + if isinstance(backend, BackendV1) and "simulator" not in backend.name(): + super()._set_backend(BackendV2Converter(backend, add_delay=True)) + else: + super()._set_backend(backend) + def circuits(self) -> List[QuantumCircuit]: """Return a list of RB circuits. @@ -149,7 +167,6 @@ def circuits(self) -> List[QuantumCircuit]: # Add metadata for each circuit for circ, seq in zip(circuits, sequences): circ.metadata = { - "experiment_type": self._type, "xval": len(seq), "group": "Clifford", "physical_qubits": self.physical_qubits, @@ -176,14 +193,72 @@ 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. + + - Return None if this experiment is an RB with 3 or more qubits. + - Return None if no basis gates are supplied via ``backend`` or ``transpile_options``. + - Return None if all 2q-gates supported on the physical qubits of the backend are one-way + directed (e.g. cx(0, 1) is supported but cx(1, 0) is not supported). + + In all those case when None are returned, basis transformation will be skipped in the + circuit generation step (i.e. :meth:`circuits`) and it will be done in the successive + transpilation step (i.e. :meth:`_transpiled_circuits`) that calls :func:`transpile`. + + Returns: + Sorted basis gate names. + """ + # 3 or more qubits case: Return None (skip basis transformation in circuit generation) + if self.num_qubits > 2: + return None + + # 1 qubit case: Return all basis gates (or None if no basis gates are supplied) + if self.num_qubits == 1: + basis_gates = self.transpile_options.get("basis_gates", None) + if not basis_gates and self.backend: + if isinstance(self.backend, BackendV2): + basis_gates = self.backend.operation_names + elif isinstance(self.backend, BackendV1): + basis_gates = self.backend.configuration().basis_gates + return tuple(sorted(basis_gates)) if basis_gates else None + + def is_bidirectional(coupling_map): + return len(coupling_map.reduce(self.physical_qubits).get_edges()) == 2 + + # 2 qubits case: Return all basis gates except for one-way directed 2q-gates. + # Return None if there is no bidirectional 2q-gates in basis gates. + if self.num_qubits == 2: + basis_gates = self.transpile_options.get("basis_gates", []) + if not basis_gates and self.backend: + if isinstance(self.backend, BackendV2) and self.backend.target: + has_bidirectional_2q_gates = False + for op_name in self.backend.target: + if self.backend.target.operation_from_name(op_name).num_qubits == 2: + if is_bidirectional(self.backend.target.build_coupling_map(op_name)): + has_bidirectional_2q_gates = True + else: + continue + basis_gates.append(op_name) + if not has_bidirectional_2q_gates: + basis_gates = None + elif isinstance(self.backend, BackendV1): + cmap = self.backend.configuration().coupling_map + if cmap is None or is_bidirectional(CouplingMap(cmap)): + basis_gates = self.backend.configuration().basis_gates + return tuple(sorted(basis_gates)) if basis_gates else None + + return None + def _sequences_to_circuits( self, sequences: List[Sequence[SequenceElementType]] ) -> List[QuantumCircuit]: - """Convert a RB sequence into circuit and append the inverse to the end. + """Convert an RB sequence into circuit and append the inverse to the end. Returns: A list of RB circuits. """ + basis_gates = self._get_basis_gates() + # Circuit generation circuits = [] for i, seq in enumerate(sequences): if ( @@ -192,42 +267,42 @@ def _sequences_to_circuits( ): prev_elem, prev_seq = self.__identity_clifford(), [] - qubits = list(range(self.num_qubits)) circ = QuantumCircuit(self.num_qubits) - circ.barrier(qubits) for elem in seq: - circ.append(self._to_instruction(elem), qubits) - circ.barrier(qubits) + circ.append(self._to_instruction(elem, basis_gates), circ.qubits) + circ.append(Barrier(self.num_qubits), circ.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) - circ.append(self._to_instruction(inv), qubits) + circ.append(self._to_instruction(inv, basis_gates), circ.qubits) circ.measure_all() # includes insertion of the barrier before measurement circuits.append(circ) return 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 + # Sample an RB sequence with the given length. + # 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) + return rng.integers(CliffordUtils.NUM_CLIFFORD_1_QUBIT, size=length) if self.num_qubits == 2: - return rng.integers(11520, size=length) - - return [random_clifford(self.num_qubits, rng) for _ in range(length)] - - def _to_instruction(self, elem: SequenceElementType) -> Instruction: - # TODO: basis transformation in 1Q (and 2Q) cases for speed + return rng.integers(CliffordUtils.NUM_CLIFFORD_2_QUBIT, size=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, basis_gates: Optional[Tuple[str, ...]] = None + ) -> Instruction: # Switching for speed up if isinstance(elem, Integral): if self.num_qubits == 1: - return _clifford_1q_int_to_instruction(elem) + return _clifford_1q_int_to_instruction(elem, basis_gates) if self.num_qubits == 2: - return _clifford_2q_int_to_instruction(elem) + return _clifford_2q_int_to_instruction(elem, basis_gates) + return elem.to_instruction() def __identity_clifford(self) -> SequenceElementType: @@ -235,37 +310,73 @@ def __identity_clifford(self) -> SequenceElementType: return 0 return Clifford(np.eye(2 * self.num_qubits)) - def __compose_clifford( - self, lop: SequenceElementType, rop: SequenceElementType + def __compose_clifford_seq( + self, base_elem: SequenceElementType, elements: Sequence[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) - 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 lop.compose(rop) + if self.num_qubits <= 2: + return functools.reduce( + compose_1q if self.num_qubits == 1 else compose_2q, elements, base_elem + ) + # 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 elements: + circ.compose(elem, inplace=True) + return base_elem.compose(Clifford.from_circuit(circ)) 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() - if self.num_qubits == 2: - return CliffordUtils.clifford_2_qubit(op).adjoint() + if self.num_qubits == 1: + return inverse_1q(op) + if self.num_qubits == 2: + return inverse_2q(op) + if isinstance(op, QuantumCircuit): + return Clifford.from_circuit(op).adjoint() return op.adjoint() def _transpiled_circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits, transpiled.""" - # TODO: Custom transpilation (without calling transpile()) for 1Q and 2Q cases - transpiled = super()._transpiled_circuits() + has_custom_transpile_option = ( + not set(vars(self.transpile_options)).issubset({"basis_gates", "optimization_level"}) + or self.transpile_options.get("optimization_level", 0) != 0 + ) + has_no_undirected_2q_basis = self._get_basis_gates() is None + if self.num_qubits > 2 or has_custom_transpile_option or has_no_undirected_2q_basis: + transpiled = super()._transpiled_circuits() + else: + transpiled = [ + _transpile_clifford_circuit(circ, physical_qubits=self.physical_qubits) + for circ in self.circuits() + ] + # Set custom calibrations provided in backend + if isinstance(self.backend, BackendV2): + qargs_patterns = [self.physical_qubits] # for self.num_qubits == 1 + if self.num_qubits == 2: + qargs_patterns = [ + (self.physical_qubits[0],), + (self.physical_qubits[1],), + self.physical_qubits, + (self.physical_qubits[1], self.physical_qubits[0]), + ] + + instructions = [] # (op_name, qargs) for each element where qargs means qubit tuple + for qargs in qargs_patterns: + for op_name in self.backend.target.operation_names_for_qargs(qargs): + instructions.append((op_name, qargs)) + + common_calibrations = defaultdict(dict) + for op_name, qargs in instructions: + inst_prop = self.backend.target[op_name].get(qargs, None) + if inst_prop is None: + continue + schedule = inst_prop.calibration + if schedule is None: + continue + publisher = schedule.metadata.get("publisher", CalibrationPublisher.QISKIT) + if publisher != CalibrationPublisher.BACKEND_PROVIDER: + common_calibrations[op_name][(qargs, tuple())] = schedule + + for circ in transpiled: + circ.calibrations = common_calibrations 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/releasenotes/notes/rb_using_transpiled_cliffords-cd1376000a2379c4.yaml b/releasenotes/notes/rb_using_transpiled_cliffords-cd1376000a2379c4.yaml new file mode 100644 index 0000000000..3f7df2e706 --- /dev/null +++ b/releasenotes/notes/rb_using_transpiled_cliffords-cd1376000a2379c4.yaml @@ -0,0 +1,8 @@ +--- +other: + - | + Improved the performance of circuit generation in 1Q/2Q RB experiments (about 10x speedup). + That is mainly achieved by the following two updates in their implementation: + - Custom transpilation of circuits (mapping circuits to physical qubits without using transpile), + - Integer-based Clifford operations (especially sparse lookup table with triplet decomposition + for 2Q Clifford circuits). diff --git a/releasenotes/notes/update-number-to-2q-clifford-mapping-c28f1f29b0205d57.yaml b/releasenotes/notes/update-number-to-2q-clifford-mapping-c28f1f29b0205d57.yaml new file mode 100644 index 0000000000..50cfc23470 --- /dev/null +++ b/releasenotes/notes/update-number-to-2q-clifford-mapping-c28f1f29b0205d57.yaml @@ -0,0 +1,23 @@ +--- +fixes: + - | + Fix a bug where :meth:`.CliffordUtils.random_cliffords` always returned a single Clifford + ignoring the ``size`` arguments. Now it returns a list of Cliffords with ``size``. + - | + Fix a bug where both :meth:`.CliffordUtils.random_cliffords` and + :meth:`.CliffordUtils.random_clifford_circuits` with integer ``seed`` + returned a list of the same circuits (sampled with a common seed). + Now it returns a list of circuits sampled with different seeds. +deprecations: + - | + Two helper methods of :meth:`.CliffordUtils.random_cliffords` and + :meth:`.CliffordUtils.random_clifford_circuits` have been deprecated. +other: + - | + :meth:`.CliffordUtils.clifford_2_qubit` (and :meth:`.CliffordUtils.clifford_2_qubit_circuit`) + changed its mapping between integers and 2Q Cliffords. + That means 2Q RB experiments may sample different set of circuits from before + even if exactly the same arguments are used for their construction. + - | + Removed unnecessary ``Barrier`` instructions in front of circuits generated by + :class:`.StandardRB` and :class:`.InterleavedRB`. 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 a42c16be39..78cf721cdb 100644 --- a/test/library/randomized_benchmarking/test_randomized_benchmarking.py +++ b/test/library/randomized_benchmarking/test_randomized_benchmarking.py @@ -11,24 +11,423 @@ # that they have been altered from the originals. """Test for randomized benchmarking experiments.""" - from test.base import QiskitExperimentsTestCase +import copy + import numpy as np from ddt import ddt, data, unpack -from qiskit.circuit import Delay, QuantumCircuit -from qiskit.circuit.library import SXGate, CXGate, TGate, XGate + +from qiskit.circuit import Delay, QuantumCircuit, Parameter +from qiskit.circuit.library import SXGate, CXGate, TGate, CZGate from qiskit.exceptions import QiskitError -from qiskit.quantum_info import Clifford +from qiskit.providers.fake_provider import FakeManila, FakeManilaV2, FakeWashington +from qiskit.pulse import Schedule, InstructionScheduleMap +from qiskit.quantum_info import Operator from qiskit_aer import AerSimulator from qiskit_aer.noise import NoiseModel, depolarizing_error - -from qiskit_experiments.library import randomized_benchmarking as rb from qiskit_experiments.database_service.exceptions import ExperimentEntryNotFound +from qiskit_experiments.framework.composite import ParallelExperiment +from qiskit_experiments.library import randomized_benchmarking as rb + + +class RBTestMixin: + """Mixin for RB tests.""" + + def assertAllIdentity(self, circuits): + """Test if all experiment circuits are identity.""" + for circ in circuits: + num_qubits = circ.num_qubits + qc_iden = QuantumCircuit(num_qubits) + circ.remove_final_measurements() + self.assertTrue(Operator(circ).equiv(Operator(qc_iden))) -class RBTestCase(QiskitExperimentsTestCase): - """Base test case for randomized benchmarking defining a common noise model.""" +@ddt +class TestStandardRB(QiskitExperimentsTestCase, RBTestMixin): + """Test for StandardRB without running the experiments.""" + + def setUp(self): + """Setup the tests.""" + super().setUp() + self.backend = FakeManilaV2() + + # ### Tests for configuration ### + @data( + {"qubits": [3, 3], "lengths": [1, 3, 5, 7, 9], "num_samples": 1, "seed": 100}, + {"qubits": [0, 1], "lengths": [1, 3, 5, -7, 9], "num_samples": 1, "seed": 100}, + {"qubits": [0, 1], "lengths": [1, 3, 5, 7, 9], "num_samples": -4, "seed": 100}, + {"qubits": [0, 1], "lengths": [1, 3, 5, 7, 9], "num_samples": 0, "seed": 100}, + {"qubits": [0, 1], "lengths": [1, 5, 5, 5, 9], "num_samples": 2, "seed": 100}, + ) + def test_invalid_configuration(self, configs): + """Test raise error when creating experiment with invalid configs.""" + self.assertRaises(QiskitError, rb.StandardRB, **configs) + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = rb.StandardRB(qubits=(0,), lengths=[10, 20, 30], seed=123) + loaded_exp = rb.StandardRB.from_config(exp.config()) + self.assertNotEqual(exp, loaded_exp) + self.assertTrue(self.json_equiv(exp, loaded_exp)) + + def test_roundtrip_serializable(self): + """Test round trip JSON serialization""" + exp = rb.StandardRB(qubits=(0,), lengths=[10, 20, 30], seed=123) + self.assertRoundTripSerializable(exp, self.json_equiv) + + def test_analysis_config(self): + """ "Test converting analysis to and from config works""" + analysis = rb.RBAnalysis() + loaded = rb.RBAnalysis.from_config(analysis.config()) + self.assertNotEqual(analysis, loaded) + self.assertEqual(analysis.config(), loaded.config()) + + # ### Tests for circuit generation ### + def test_return_same_circuit(self): + """Test if setting the same seed returns the same circuits.""" + exp1 = rb.StandardRB( + qubits=(0, 1), + lengths=[10, 20, 30], + seed=123, + backend=self.backend, + ) + + exp2 = rb.StandardRB( + qubits=(0, 1), + lengths=[10, 20, 30], + seed=123, + backend=self.backend, + ) + + circs1 = exp1.circuits() + circs2 = exp2.circuits() + + self.assertEqual(circs1[0].decompose(), circs2[0].decompose()) + self.assertEqual(circs1[1].decompose(), circs2[1].decompose()) + self.assertEqual(circs1[2].decompose(), circs2[2].decompose()) + + 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, + ) + exp2 = rb.StandardRB( + qubits=(0,), + lengths=[10, 20, 30], + seed=123, + backend=self.backend, + full_sampling=True, + ) + 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), + lengths=[10, 20, 30], + seed=123, + backend=self.backend, + full_sampling=False, + ) + + exp2 = rb.StandardRB( + qubits=(0, 1), + lengths=[10, 20, 30], + seed=123, + backend=self.backend, + full_sampling=True, + ) + + 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()) + + # ### Tests for transpiled circuit generation ### + def test_calibrations_via_transpile_options(self): + """Test if calibrations given as transpile_options show up in transpiled circuits.""" + qubits = (2,) + my_sched = Schedule(name="custom_sx_gate") + my_inst_map = InstructionScheduleMap() + my_inst_map.add(SXGate(), qubits, my_sched) + + exp = rb.StandardRB( + qubits=qubits, lengths=[3], num_samples=4, backend=self.backend, seed=123 + ) + exp.set_transpile_options(inst_map=my_inst_map) + transpiled = exp._transpiled_circuits() + for qc in transpiled: + self.assertTrue(qc.calibrations) + 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_calibrations_via_custom_backend(self): + """Test if calibrations given as custom backend show up in transpiled circuits.""" + qubits = (2,) + my_sched = Schedule(name="custom_sx_gate") + my_backend = copy.deepcopy(self.backend) + my_backend.target["sx"][qubits].calibration = my_sched + + exp = rb.StandardRB(qubits=qubits, lengths=[3], num_samples=4, backend=my_backend) + transpiled = exp._transpiled_circuits() + for qc in transpiled: + self.assertTrue(qc.calibrations) + 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): + """Test for InterleavedRB without running the experiments.""" + + def setUp(self): + """Setup the tests.""" + super().setUp() + self.backend = FakeManila() + self.backend_with_timing_constraint = FakeWashington() + + # ### Tests for configuration ### + def test_non_clifford_interleaved_element(self): + """Verifies trying to run interleaved RB with non Clifford element throws an exception""" + with self.assertRaises(QiskitError): + rb.InterleavedRB( + interleaved_element=TGate(), # T gate is not Clifford, this should fail + qubits=[0], + lengths=[1, 2, 3, 5, 8, 13], + ) + + @data([5, "dt"], [1e-7, "s"], [32, "ns"]) + @unpack + def test_interleaving_delay_with_invalid_duration(self, duration, unit): + """Raise if delay with invalid duration is given as interleaved_element""" + with self.assertRaises(QiskitError): + rb.InterleavedRB( + interleaved_element=Delay(duration, unit=unit), + qubits=[0], + lengths=[1, 2, 3], + backend=self.backend_with_timing_constraint, + ) + + 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, + ) + loaded_exp = rb.InterleavedRB.from_config(exp.config()) + self.assertNotEqual(exp, loaded_exp) + self.assertTrue(self.json_equiv(exp, loaded_exp)) + + def test_roundtrip_serializable(self): + """Test round trip JSON serialization""" + exp = rb.InterleavedRB( + interleaved_element=SXGate(), qubits=(0,), lengths=[10, 20, 30], seed=123 + ) + self.assertRoundTripSerializable(exp, self.json_equiv) + + def test_analysis_config(self): + """ "Test converting analysis to and from config works""" + analysis = rb.InterleavedRBAnalysis() + loaded = rb.InterleavedRBAnalysis.from_config(analysis.config()) + self.assertNotEqual(analysis, loaded) + self.assertEqual(analysis.config(), loaded.config()) + + # ### Tests for circuit generation ### + @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): + # clifford + self.assertEqual(c_std[std_idx], c_int[int_idx]) + # barrier + self.assertEqual(c_std[std_idx + 1][0].name, "barrier") + self.assertEqual(c_int[std_idx + 1][0].name, "barrier") + # for interleaved circuit: interleaved element + barrier + self.assertEqual(c_int[int_idx + 2][0].name, interleaved_element.name) + self.assertEqual(c_int[int_idx + 3][0].name, "barrier") + std_idx += 2 + int_idx += 4 + + 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(1.0e-7, 0, unit="s") + 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: + # 0: clifford, 1: barrier, 2: interleaved + actual = circuits[1][2].operation + self.assertEqual(interleaved_circ.count_ops(), actual.definition.count_ops()) + + def test_interleaving_delay(self): + """Test delay instruction can be interleaved.""" + # See qiskit-experiments/#727 for details + from qiskit_experiments.framework.backend_timing import BackendTiming + + timing = BackendTiming(self.backend) + exp = rb.InterleavedRB( + interleaved_element=Delay(timing.round_delay(time=1.0e-7)), + qubits=[0], + lengths=[1], + num_samples=1, + seed=1234, # This seed gives a 2-gate clifford + backend=self.backend, + ) + int_circs = exp.circuits()[1] + self.assertEqual(int_circs.count_ops().get("delay", 0), 1) + self.assertAllIdentity([int_circs]) + + def test_interleaving_circuit_with_delay(self): + """Test circuit with delay can be interleaved.""" + delay_qc = QuantumCircuit(2) + delay_qc.delay(160, [0]) + delay_qc.x(1) + + exp = rb.InterleavedRB( + interleaved_element=delay_qc, + qubits=[1, 2], + lengths=[1], + num_samples=1, + seed=1234, + backend=self.backend, + ) + int_circ = exp.circuits()[1] + self.assertAllIdentity([int_circ]) + + def test_interleaving_parameterized_circuit(self): + """Fail if parameterized circuit is interleaved but after assigned it may be interleaved.""" + qubits = (2,) + theta = Parameter("theta") + phi = Parameter("phi") + lam = Parameter("lambda") + cliff_circ_with_param = QuantumCircuit(1) + cliff_circ_with_param.rz(theta, 0) + cliff_circ_with_param.sx(0) + cliff_circ_with_param.rz(phi, 0) + cliff_circ_with_param.sx(0) + cliff_circ_with_param.rz(lam, 0) + + with self.assertRaises(QiskitError): + rb.InterleavedRB( + interleaved_element=cliff_circ_with_param, + qubits=qubits, + lengths=[3], + num_samples=4, + backend=self.backend, + ) + + # # TODO: Enable after Clifford supports creation from circuits with rz + # # parameters must be assigned before initializing InterleavedRB + # param_map = {theta: np.pi / 2, phi: -np.pi / 2, lam: np.pi / 2} + # cliff_circ_with_param.assign_parameters(param_map, inplace=True) + # + # exp = rb.InterleavedRB( + # interleaved_element=cliff_circ_with_param, + # qubits=qubits, + # lengths=[3], + # num_samples=4, + # backend=self.backend, + # ) + # circuits = exp.circuits() + # for qc in circuits: + # self.assertEqual(qc.num_parameters, 0) + + # ### Tests for transpiled circuit generation ### + def test_interleaved_circuit_is_decomposed(self): + """Test if interleaved circuit is decomposed in transpiled circuits.""" + delay_qc = QuantumCircuit(2) + delay_qc.delay(160, [0]) + delay_qc.x(1) + + exp = rb.InterleavedRB( + interleaved_element=delay_qc, + qubits=[1, 2], + lengths=[3], + num_samples=1, + seed=1234, + backend=self.backend, + ) + transpiled = exp._transpiled_circuits() + for qc in transpiled: + 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.""" def setUp(self): """Setup the tests.""" @@ -38,19 +437,22 @@ def setUp(self): self.p1q = 0.02 self.p2q = 0.10 self.pvz = 0.0 + self.pcz = 0.15 # basis gates - self.basis_gates = ["sx", "rz", "cx"] + self.basis_gates = ["rz", "sx", "cx"] # setup noise model sx_error = depolarizing_error(self.p1q, 1) rz_error = depolarizing_error(self.pvz, 1) cx_error = depolarizing_error(self.p2q, 2) + cz_error = depolarizing_error(self.pcz, 2) noise_model = NoiseModel() noise_model.add_all_qubit_quantum_error(sx_error, "sx") noise_model.add_all_qubit_quantum_error(rz_error, "rz") noise_model.add_all_qubit_quantum_error(cx_error, "cx") + noise_model.add_all_qubit_quantum_error(cz_error, "cz") self.noise_model = noise_model @@ -63,22 +465,9 @@ def setUp(self): # Aer simulator self.backend = AerSimulator(noise_model=noise_model, seed_simulator=123) - 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)) - - circ.remove_final_measurements() - self.assertEqual( - Clifford(circ), iden, f"Circuit {circ.name} doesn't result in the identity matrix." - ) - - -@ddt -class TestStandardRB(RBTestCase): - """Test for standard RB.""" +class TestRunStandardRB(RBRunTestCase): + """Test for running StandardRB.""" def test_single_qubit(self): """Test single qubit RB.""" @@ -108,7 +497,7 @@ def test_single_qubit(self): self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.1 * epc_expected) def test_two_qubit(self): - """Test two qubit RB.""" + """Test two qubit RB. Use default basis gates.""" exp = rb.StandardRB( qubits=(0, 1), lengths=list(range(1, 30, 3)), @@ -116,7 +505,8 @@ def test_two_qubit(self): backend=self.backend, ) exp.analysis.set_options(gate_error_ratio=None) - exp.set_transpile_options(**self.transpiler_options) + transpiler_options = {"optimization_level": 1} + exp.set_transpile_options(**transpiler_options) self.assertAllIdentity(exp.circuits()) expdata = exp.run() @@ -127,11 +517,34 @@ def test_two_qubit(self): # average number of CX gate per Clifford is 1.5. # Since this is two qubit RB, the dep-parameter is factored by 3/4. epc = expdata.analysis_results("EPC") - # Allow for 50 percent tolerance since we ignore 1q gate contribution epc_expected = 1 - (1 - 3 / 4 * self.p2q) ** 1.5 self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.5 * epc_expected) + def test_three_qubit(self): + """Test two qubit RB. Use default basis gates.""" + exp = rb.StandardRB( + qubits=(0, 1, 2), + lengths=list(range(1, 30, 3)), + seed=123, + backend=self.backend, + ) + exp.analysis.set_options(gate_error_ratio=None) + exp.set_transpile_options(**self.transpiler_options) + self.assertAllIdentity(exp.circuits()) + + expdata = exp.run() + self.assertExperimentDone(expdata) + + # Given CX error is dominant and 1q error can be negligible. + # Arbitrary SU(8) can be decomposed with [0,...,7] CX gates, the expected + # average number of CX gate per Clifford is 3.5. + # Since this is three qubit RB, the dep-parameter is factored by 7/8. + epc = expdata.analysis_results("EPC") + # Allow for 50 percent tolerance since we ignore 1q gate contribution + epc_expected = 1 - (1 - 7 / 8 * self.p2q) ** 3.5 + self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.5 * epc_expected) + def test_add_more_circuit_yields_lower_variance(self): """Test variance reduction with larger number of sampling.""" exp1 = rb.StandardRB( @@ -172,104 +585,26 @@ def test_poor_experiment_result(self): from qiskit.providers.fake_provider import FakeVigoV2 backend = FakeVigoV2() + # TODO: this test no longer makes sense (yields small reduced_chisq) + # after fixing how to call fake backend v2 (by adding the next line) + # Need to call target before running fake backend v2 to load correct data + self.assertLess(backend.target["sx"][(0,)].error, 0.001) + exp = rb.StandardRB( qubits=(0,), - lengths=[100, 200, 300, 400], + lengths=[100, 200, 300], seed=123, backend=backend, num_samples=5, ) exp.set_transpile_options(basis_gates=["x", "sx", "rz"], optimization_level=1) - # Simulator seed must be fixed. This can be set via run option with FakeBackend. - # pylint: disable=no-member - exp.set_run_options(seed_simulator=456) + expdata = exp.run() self.assertExperimentDone(expdata) - overview = expdata.analysis_results(0).value # This yields bad fit due to poor data points, but still fit is not completely off. self.assertLess(overview.reduced_chisq, 10) - def test_return_same_circuit(self): - """Test if setting the same seed returns the same circuits.""" - exp1 = rb.StandardRB( - qubits=(0, 1), - lengths=[10, 20, 30], - seed=123, - backend=self.backend, - ) - - exp2 = rb.StandardRB( - qubits=(0, 1), - lengths=[10, 20, 30], - seed=123, - backend=self.backend, - ) - - circs1 = exp1.circuits() - circs2 = exp2.circuits() - - self.assertEqual(circs1[0].decompose(), circs2[0].decompose()) - self.assertEqual(circs1[1].decompose(), circs2[1].decompose()) - self.assertEqual(circs1[2].decompose(), circs2[2].decompose()) - - def test_full_sampling(self): - """Test if full sampling generates different circuits.""" - exp1 = rb.StandardRB( - qubits=(0, 1), - lengths=[10, 20, 30], - seed=123, - backend=self.backend, - full_sampling=False, - ) - - exp2 = rb.StandardRB( - qubits=(0, 1), - lengths=[10, 20, 30], - seed=123, - backend=self.backend, - full_sampling=True, - ) - - 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()) - - @data( - {"qubits": [3, 3], "lengths": [1, 3, 5, 7, 9], "num_samples": 1, "seed": 100}, - {"qubits": [0, 1], "lengths": [1, 3, 5, -7, 9], "num_samples": 1, "seed": 100}, - {"qubits": [0, 1], "lengths": [1, 3, 5, 7, 9], "num_samples": -4, "seed": 100}, - {"qubits": [0, 1], "lengths": [1, 3, 5, 7, 9], "num_samples": 0, "seed": 100}, - {"qubits": [0, 1], "lengths": [1, 5, 5, 5, 9], "num_samples": 2, "seed": 100}, - ) - def test_invalid_configuration(self, configs): - """Test raise error when creating experiment with invalid configs.""" - self.assertRaises(QiskitError, rb.StandardRB, **configs) - - def test_experiment_config(self): - """Test converting to and from config works""" - exp = rb.StandardRB(qubits=(0,), lengths=[10, 20, 30], seed=123) - loaded_exp = rb.StandardRB.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertTrue(self.json_equiv(exp, loaded_exp)) - - def test_roundtrip_serializable(self): - """Test round trip JSON serialization""" - exp = rb.StandardRB(qubits=(0,), lengths=[10, 20, 30], seed=123) - self.assertRoundTripSerializable(exp, self.json_equiv) - - def test_analysis_config(self): - """ "Test converting analysis to and from config works""" - analysis = rb.RBAnalysis() - loaded = rb.RBAnalysis.from_config(analysis.config()) - self.assertNotEqual(analysis, loaded) - self.assertEqual(analysis.config(), loaded.config()) - def test_expdata_serialization(self): """Test serializing experiment data works.""" exp = rb.StandardRB( @@ -284,41 +619,80 @@ 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) + + def test_two_qubit_parallel(self): + """Test two qubit RB in parallel.""" + qubit_pairs = [[0, 1], [2, 3]] + lengths = list(range(5, 100, 5)) + exps = [] + for pair in qubit_pairs: + exp = rb.StandardRB(qubits=pair, 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 - 3 / 4 * self.p2q) ** 1.5 + for i in range(2): + epc = par_expdata.child_data(i).analysis_results("EPC") + # Allow for 20 percent tolerance since we ignore 1q gate contribution + self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.2 * epc_expected) -@ddt -class TestInterleavedRB(RBTestCase): - """Test for interleaved RB.""" + def test_two_qubit_with_cz(self): + """Test two qubit RB.""" + transpiler_options = { + "basis_gates": ["sx", "rz", "cz"], + "optimization_level": 1, + } - @data([XGate(), [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 + exp = rb.StandardRB( + qubits=(0, 1), + lengths=list(range(1, 50, 5)), + seed=123, + backend=self.backend, ) + exp.analysis.set_options(gate_error_ratio=None) + exp.set_transpile_options(**transpiler_options) + + expdata = exp.run() + self.assertAllIdentity(exp.circuits()) + self.assertExperimentDone(expdata) + + # Given CX error is dominant and 1q error can be negligible. + # Arbitrary SU(4) can be decomposed with (0, 1, 2, 3) CZ gates, the expected + # average number of CZ gate per Clifford is 1.5. + # Since this is two qubit RB, the dep-parameter is factored by 3/4. + epc = expdata.analysis_results("EPC") + + # Allow for 50 percent tolerance since we ignore 1q gate contribution + epc_expected = 1 - (1 - 3 / 4 * self.pcz) ** 1.5 + self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.5 * epc_expected) - 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 + +class TestRunInterleavedRB(RBRunTestCase): + """Test for running InterleavedRB.""" def test_single_qubit(self): """Test single qubit IRB.""" @@ -360,74 +734,29 @@ def test_two_qubit(self): epc_expected = 3 / 4 * self.p2q self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.1 * epc_expected) - def test_non_clifford_interleaved_element(self): - """Verifies trying to run interleaved RB with non Clifford element throws an exception""" - qubits = 1 - lengths = [1, 4, 6, 9, 13, 16] - interleaved_element = TGate() # T gate is not Clifford, this should fail - self.assertRaises( - QiskitError, - rb.InterleavedRB, - interleaved_element=interleaved_element, - qubits=qubits, - lengths=lengths, - ) - - def test_interleaving_delay(self): - """Test delay instruction can be interleaved.""" - # See qiskit-experiments/#727 for details - interleaved_element = Delay(10, unit="us") - exp = rb.InterleavedRB( - interleaved_element, - qubits=[0], - lengths=[1], - num_samples=1, - ) - # Not raises an error - _, int_circ = exp.circuits() - - # barrier, clifford, barrier, "delay", barrier, ... - self.assertEqual(int_circ.data[3][0], interleaved_element) - - def test_interleaving_circuit_with_delay(self): - """Test circuit with delay can be interleaved.""" - delay_qc = QuantumCircuit(2) - delay_qc.delay(10, [0], unit="us") - delay_qc.x(1) - - exp = rb.InterleavedRB( - interleaved_element=delay_qc, qubits=[1, 2], lengths=[1], seed=123, num_samples=1 - ) - _, int_circ = exp.circuits() - - qc = QuantumCircuit(2) - qc.x(1) - expected_inversion = Clifford(int_circ.data[1][0]).compose(qc).adjoint() - # barrier, clifford, barrier, "interleaved circuit", barrier, inversion, ... - self.assertEqual(expected_inversion, Clifford(int_circ.data[5][0])) - - def test_experiment_config(self): - """Test converting to and from config works""" + def test_two_qubit_with_cz(self): + """Test two qubit IRB.""" + transpiler_options = { + "basis_gates": ["sx", "rz", "cz"], + "optimization_level": 1, + } exp = rb.InterleavedRB( - interleaved_element=SXGate(), qubits=(0,), lengths=[10, 20, 30], seed=123 + interleaved_element=CZGate(), + qubits=(0, 1), + lengths=list(range(1, 30, 3)), + seed=1234, + backend=self.backend, ) - loaded_exp = rb.InterleavedRB.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertTrue(self.json_equiv(exp, loaded_exp)) + exp.set_transpile_options(**transpiler_options) + self.assertAllIdentity(exp.circuits()) - def test_roundtrip_serializable(self): - """Test round trip JSON serialization""" - exp = rb.InterleavedRB( - interleaved_element=SXGate(), qubits=(0,), lengths=[10, 20, 30], seed=123 - ) - self.assertRoundTripSerializable(exp, self.json_equiv) + expdata = exp.run() + self.assertExperimentDone(expdata) - def test_analysis_config(self): - """ "Test converting analysis to and from config works""" - analysis = rb.InterleavedRBAnalysis() - loaded = rb.InterleavedRBAnalysis.from_config(analysis.config()) - self.assertNotEqual(analysis, loaded) - self.assertEqual(analysis.config(), loaded.config()) + # Since this is interleaved, we can directly compare values, i.e. n_gpc = 1 + epc = expdata.analysis_results("EPC") + epc_expected = 3 / 4 * self.pcz + self.assertAlmostEqual(epc.value.n, epc_expected, delta=3 * epc.value.std_dev) def test_expdata_serialization(self): """Test serializing experiment data works.""" @@ -462,10 +791,14 @@ def setUp(self): # Setup noise model, including more gate for complicated EPG computation # Note that 1Q channel error is amplified to check 1q channel correction mechanism - x_error = depolarizing_error(0.04, 1) - h_error = depolarizing_error(0.02, 1) - s_error = depolarizing_error(0.00, 1) - cx_error = depolarizing_error(0.08, 2) + self.p_x = 0.04 + self.p_h = 0.02 + self.p_s = 0.0 + self.p_cx = 0.09 + x_error = depolarizing_error(self.p_x, 1) + h_error = depolarizing_error(self.p_h, 1) + s_error = depolarizing_error(self.p_s, 1) + cx_error = depolarizing_error(self.p_cx, 2) noise_model = NoiseModel() noise_model.add_all_qubit_quantum_error(x_error, "x") @@ -529,8 +862,8 @@ def test_default_epg_ratio(self): # H and X gate EPG are assumed to be the same, so this underestimate X and overestimate H self.assertEqual(h_epg.value.n, x_epg.value.n) - self.assertLess(x_epg.value.n, 0.04 * 0.5) - self.assertGreater(h_epg.value.n, 0.02 * 0.5) + self.assertLess(x_epg.value.n, self.p_x * 0.5) + self.assertGreater(h_epg.value.n, self.p_h * 0.5) def test_no_epg(self): """Calculate no EPGs.""" @@ -558,8 +891,8 @@ def test_with_custom_epg_ratio(self): h_epg = result.analysis_results("EPG_h") x_epg = result.analysis_results("EPG_x") - self.assertAlmostEqual(x_epg.value.n, 0.04 * 0.5, delta=0.005) - self.assertAlmostEqual(h_epg.value.n, 0.02 * 0.5, delta=0.005) + self.assertAlmostEqual(x_epg.value.n, self.p_x * 0.5, delta=0.005) + self.assertAlmostEqual(h_epg.value.n, self.p_h * 0.5, delta=0.005) def test_2q_epg(self): """Compute 2Q EPG without correction. @@ -574,7 +907,7 @@ def test_2q_epg(self): cx_epg = result.analysis_results("EPG_cx") - self.assertGreater(cx_epg.value.n, 0.08 * 0.75) + self.assertGreater(cx_epg.value.n, self.p_cx * 0.75) def test_2q_epg_with_correction(self): """Check that 2Q EPG with 1Q depolarization correction gives a better (smaller) result than @@ -605,7 +938,7 @@ def test_2q_epg_with_correction(self): result_2qrb = analysis_2qrb.run(self.expdata_2qrb) self.assertExperimentDone(result_2qrb) cx_epg_corrected = result_2qrb.analysis_results("EPG_cx") - self.assertLess( - np.abs(cx_epg_corrected.value.n - 0.08 * 0.75), np.abs(cx_epg_raw.value.n - 0.08 * 0.75) + np.abs(cx_epg_corrected.value.n - self.p_cx * 0.75), + np.abs(cx_epg_raw.value.n - self.p_cx * 0.75), ) diff --git a/test/library/randomized_benchmarking/test_rb_utils.py b/test/library/randomized_benchmarking/test_rb_utils.py index c9163e287d..671e629967 100644 --- a/test/library/randomized_benchmarking/test_rb_utils.py +++ b/test/library/randomized_benchmarking/test_rb_utils.py @@ -31,7 +31,6 @@ CZGate, SwapGate, ) -from qiskit.quantum_info import Clifford import qiskit_experiments.library.randomized_benchmarking as rb from qiskit_experiments.framework import AnalysisResultData @@ -174,843 +173,3 @@ def test_coherence_limit(self): self.assertAlmostEqual(oneq_coherence_err, 0.00049975, 6, "Error: 1Q Coherence Limit") self.assertAlmostEqual(twoq_coherence_err, 0.00597, 5, "Error: 2Q Coherence Limit") - - 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] - utils = rb.CliffordUtils() - for n in range(24): - clifford = utils.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, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 576, - 577, - 578, - 579, - 580, - 581, - 582, - 583, - 584, - 585, - 586, - 587, - 588, - 589, - 590, - 591, - 592, - 593, - 594, - 595, - 596, - 597, - 598, - 599, - 600, - 601, - 602, - 603, - 604, - 605, - 606, - 607, - 608, - 609, - 610, - 611, - 612, - 613, - 614, - 615, - 616, - 617, - 618, - 619, - 620, - 621, - 622, - 623, - 624, - 625, - 626, - 627, - 628, - 629, - 630, - 631, - 632, - 633, - 634, - 635, - 636, - 637, - 638, - 639, - 640, - 641, - 642, - 643, - 644, - 645, - 646, - 647, - 648, - 649, - 650, - 651, - 652, - 653, - 654, - 655, - 656, - 657, - 658, - 659, - 660, - 661, - 662, - 663, - 664, - 665, - 666, - 667, - 668, - 669, - 670, - 671, - 672, - 673, - 674, - 675, - 676, - 677, - 678, - 679, - 680, - 681, - 682, - 683, - 684, - 685, - 686, - 687, - 688, - 689, - 690, - 691, - 692, - 693, - 694, - 695, - 696, - 697, - 698, - 699, - 700, - 701, - 702, - 703, - 704, - 705, - 706, - 707, - 708, - 709, - 710, - 711, - 712, - 713, - 714, - 715, - 716, - 717, - 718, - 719, - 720, - 721, - 722, - 723, - 724, - 725, - 726, - 727, - 728, - 729, - 730, - 731, - 732, - 733, - 734, - 735, - 736, - 737, - 738, - 739, - 740, - 741, - 742, - 743, - 744, - 745, - 746, - 747, - 748, - 749, - 750, - 751, - 752, - 753, - 754, - 755, - 756, - 757, - 758, - 759, - 760, - 761, - 762, - 763, - 764, - 765, - 766, - 767, - 768, - 769, - 770, - 771, - 772, - 773, - 774, - 775, - 776, - 777, - 778, - 779, - 780, - 781, - 782, - 783, - 784, - 785, - 786, - 787, - 788, - 789, - 790, - 791, - 792, - 793, - 794, - 795, - 796, - 797, - 798, - 799, - 800, - 801, - 802, - 803, - 804, - 805, - 806, - 807, - 808, - 809, - 810, - 811, - 812, - 813, - 814, - 815, - 816, - 817, - 818, - 819, - 820, - 821, - 822, - 823, - 824, - 825, - 826, - 827, - 828, - 829, - 830, - 831, - 832, - 833, - 834, - 835, - 836, - 837, - 838, - 839, - 840, - 841, - 842, - 843, - 844, - 845, - 846, - 847, - 848, - 849, - 850, - 851, - 852, - 853, - 854, - 855, - 856, - 857, - 858, - 859, - 860, - 861, - 862, - 863, - 864, - 865, - 866, - 867, - 868, - 869, - 870, - 871, - 872, - 873, - 874, - 875, - 876, - 877, - 878, - 879, - 880, - 881, - 882, - 883, - 884, - 885, - 886, - 887, - 888, - 889, - 890, - 891, - 892, - 893, - 894, - 895, - 896, - 897, - 898, - 899, - 5760, - 5761, - 5762, - 5763, - 5764, - 5765, - 5766, - 5767, - 5768, - 5769, - 5770, - 5771, - 5772, - 5773, - 5774, - 5775, - 5776, - 5777, - 5778, - 5779, - 5780, - 5781, - 5782, - 5783, - 5784, - 5785, - 5786, - 5787, - 5788, - 5789, - 5790, - 5791, - 5792, - 5793, - 5794, - 5795, - 5796, - 5797, - 5798, - 5799, - 5800, - 5801, - 5802, - 5803, - 5804, - 5805, - 5806, - 5807, - 5808, - 5809, - 5810, - 5811, - 5812, - 5813, - 5814, - 5815, - 5816, - 5817, - 5818, - 5819, - 5820, - 5821, - 5822, - 5823, - 5824, - 5825, - 5826, - 5827, - 5828, - 5829, - 5830, - 5831, - 5832, - 5833, - 5834, - 5835, - 5836, - 5837, - 5838, - 5839, - 5840, - 5841, - 5842, - 5843, - 5844, - 5845, - 5846, - 5847, - 5848, - 5849, - 5850, - 5851, - 5852, - 5853, - 5854, - 5855, - 5856, - 5857, - 5858, - 5859, - 5860, - 5861, - 5862, - 5863, - 5864, - 5865, - 5866, - 5867, - 5868, - 5869, - 5870, - 5871, - 5872, - 5873, - 5874, - 5875, - 5876, - 5877, - 5878, - 5879, - 5880, - 5881, - 5882, - 5883, - 5884, - 5885, - 5886, - 5887, - 5888, - 5889, - 5890, - 5891, - 5892, - 5893, - 5894, - 5895, - 5896, - 5897, - 5898, - 5899, - 5900, - 5901, - 5902, - 5903, - 5904, - 5905, - 5906, - 5907, - 5908, - 5909, - 5910, - 5911, - 5912, - 5913, - 5914, - 5915, - 5916, - 5917, - 5918, - 5919, - 5920, - 5921, - 5922, - 5923, - 5924, - 5925, - 5926, - 5927, - 5928, - 5929, - 5930, - 5931, - 5932, - 5933, - 5934, - 5935, - 5936, - 5937, - 5938, - 5939, - 5940, - 5941, - 5942, - 5943, - 5944, - 5945, - 5946, - 5947, - 5948, - 5949, - 5950, - 5951, - 5952, - 5953, - 5954, - 5955, - 5956, - 5957, - 5958, - 5959, - 5960, - 5961, - 5962, - 5963, - 5964, - 5965, - 5966, - 5967, - 5968, - 5969, - 5970, - 5971, - 5972, - 5973, - 5974, - 5975, - 5976, - 5977, - 5978, - 5979, - 5980, - 5981, - 5982, - 5983, - 5984, - 5985, - 5986, - 5987, - 5988, - 5989, - 5990, - 5991, - 5992, - 5993, - 5994, - 5995, - 5996, - 5997, - 5998, - 5999, - 6000, - 6001, - 6002, - 6003, - 6004, - 6005, - 6006, - 6007, - 6008, - 6009, - 6010, - 6011, - 6012, - 6013, - 6014, - 6015, - 6016, - 6017, - 6018, - 6019, - 6020, - 6021, - 6022, - 6023, - 6024, - 6025, - 6026, - 6027, - 6028, - 6029, - 6030, - 6031, - 6032, - 6033, - 6034, - 6035, - 6036, - 6037, - 6038, - 6039, - 6040, - 6041, - 6042, - 6043, - 6044, - 6045, - 6046, - 6047, - 6048, - 6049, - 6050, - 6051, - 6052, - 6053, - 6054, - 6055, - 6056, - 6057, - 6058, - 6059, - 6060, - 6061, - 6062, - 6063, - 6064, - 6065, - 6066, - 6067, - 6068, - 6069, - 6070, - 6071, - 6072, - 6073, - 6074, - 6075, - 6076, - 6077, - 6078, - 6079, - 6080, - 6081, - 6082, - 6083, - 10944, - 10945, - 10946, - 10947, - 10948, - 10949, - 10950, - 10951, - 10952, - 10953, - 10954, - 10955, - 10956, - 10957, - 10958, - 10959, - 10960, - 10961, - 10962, - 10963, - 10964, - 10965, - 10966, - 10967, - 10968, - 10969, - 10970, - 10971, - 10972, - 10973, - 10974, - 10975, - 10976, - 10977, - 10978, - 10979, - ] - cliffords = [] - for n in pauli_free_elements: - clifford = utils.clifford_2_qubit(n) - phase = clifford.table.phase - for i in range(4): - self.assertFalse(phase[i]) - for other_clifford in cliffords: - self.assertNotEqual(clifford, other_clifford) - cliffords.append(clifford) - - pauli_check_elements_list = [ - [0, 36, 72, 108, 144, 180, 216, 252, 288, 324, 360, 396, 432, 468, 504, 540], - [ - 576, - 900, - 1224, - 1548, - 1872, - 2196, - 2520, - 2844, - 3168, - 3492, - 3816, - 4140, - 4464, - 4788, - 5112, - 5436, - ], - [ - 5760, - 6084, - 6408, - 6732, - 7056, - 7380, - 7704, - 8028, - 8352, - 8676, - 9000, - 9324, - 9648, - 9972, - 10296, - 10620, - ], - [ - 10944, - 10980, - 11016, - 11052, - 11088, - 11124, - 11160, - 11196, - 11232, - 11268, - 11304, - 11340, - 11376, - 11412, - 11448, - 11484, - ], - ] - for pauli_check_elements in pauli_check_elements_list: - phases = [] - table = None - for n in pauli_check_elements: - clifford = utils.clifford_2_qubit(n) - if table is None: - table = clifford.table.array - else: - self.assertTrue(np.all(table == clifford.table.array)) - phase = tuple(clifford.table.phase) - for other_phase in phases: - self.assertNotEqual(phase, other_phase) - phases.append(phase)