diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py index e5f761cb55..08441df5cf 100644 --- a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py +++ b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py @@ -13,22 +13,23 @@ Utilities for using the Clifford group in randomized benchmarking """ -from typing import List, Tuple, Optional, Union +import itertools from functools import lru_cache -from numbers import Integral from math import isclose -import itertools +from numbers import Integral +from typing import List +from typing import Optional, Union, Tuple, Sequence + import numpy as np from numpy.random import Generator, default_rng -from qiskit import QuantumCircuit, QuantumRegister from qiskit.circuit import Gate, Instruction +from qiskit.circuit import QuantumCircuit, QuantumRegister, CircuitInstruction, Qubit from qiskit.circuit.library import SdgGate, HGate, SGate from qiskit.compiler import transpile -from qiskit.providers.backend import Backend from qiskit.exceptions import QiskitError +from qiskit.providers.backend import Backend from qiskit.quantum_info import Clifford, random_clifford - from .clifford_data import ( CLIFF_SINGLE_GATE_MAP_1Q, CLIFF_SINGLE_GATE_MAP_2Q, @@ -41,14 +42,108 @@ ) +# Transpilation utilities +def _transpile_clifford_circuit(circuit: QuantumCircuit, layout: Sequence[int]) -> QuantumCircuit: + return _apply_qubit_layout(_decompose_clifford_ops(circuit), layout=layout) + + +def _decompose_clifford_ops(circuit: QuantumCircuit) -> QuantumCircuit: + # Simplified QuantumCircuit.decompose, which decomposes only Clifford ops + 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, layout: Sequence[int]) -> QuantumCircuit: + res = QuantumCircuit(1 + max(layout), 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=layout) + 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 update and copy of operations + 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 + for gate, cals in other.calibrations.items(): + self._calibrations[gate].update(cals) + return self + + +def _truncate_inactive_qubits( + circ: QuantumCircuit, active_qubits: Sequence[Qubit] +) -> QuantumCircuit: + new_data = [] + for inst in circ: + if all(q in active_qubits for q in inst.qubits): + new_data.append(inst) + + res = QuantumCircuit(active_qubits, name=circ.name) + res._calibrations = circ.calibrations + res._data = new_data + res._metadata = circ.metadata + return res + + +def _transform_clifford_circuit(circuit: QuantumCircuit, basis_gates: Tuple[str]) -> QuantumCircuit: + 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: + utils = __get_clifford_utils_2q(basis_gates) + return utils.transpiled_cliff_from_layer_nums( + utils.layer_indices_from_num(num) + ).to_instruction() + # return CliffordUtils.clifford_2_qubit_circuit(num, basis_gates).to_instruction() + + +@lru_cache(maxsize=None) +def __get_clifford_utils_2q(basis_gates: Optional[Tuple[str]]): + return CliffordUtils(2, basis_gates) # The classes VGate and WGate are not actually used in the code - we leave them here to give @@ -173,7 +268,7 @@ def random_clifford_circuits( @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. """ @@ -193,11 +288,14 @@ def clifford_1_qubit_circuit(cls, num): if p == 3: qc.z(0) + if basis_gates: + qc = _transform_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. """ @@ -251,6 +349,9 @@ def clifford_2_qubit_circuit(cls, num): if p1 == 3: qc.z(1) + if basis_gates: + qc = _transform_clifford_circuit(qc, basis_gates) + return qc @staticmethod @@ -555,6 +656,7 @@ def transpiled_cliff_from_layer_nums(self, triplet: Tuple) -> QuantumCircuit: qc = q0.copy() qc.compose(q1, inplace=True) qc.compose(q2, inplace=True) + qc.name = f"Clifford-2Q({self.num_from_layer_indices(triplet)})" return qc @staticmethod diff --git a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py index 5c61e5c442..2b66c035d0 100644 --- a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py @@ -12,20 +12,20 @@ """ Interleaved RB Experiment class. """ -from typing import Union, Iterable, Optional, List, Sequence +from typing import Union, Iterable, Optional, List, Sequence, Tuple -from numpy.random import Generator, default_rng +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 qiskit.compiler import transpile - -from .rb_experiment import StandardRB, SequenceElementType +from qiskit.quantum_info import Clifford +from qiskit.transpiler.exceptions import TranspilerError +from .clifford_utils import CliffordUtils, _truncate_inactive_qubits from .interleaved_rb_analysis import InterleavedRBAnalysis +from .rb_experiment import StandardRB, SequenceElementType class InterleavedRB(StandardRB): @@ -51,7 +51,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, @@ -63,7 +63,9 @@ 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. + Only when the element contains any non-basis gates, + it will be transpiled with ``transpiled_options`` of this experiment. qubits: list of physical qubits for the experiment. lengths: A list of RB sequences lengths. backend: The backend to run the experiment on. @@ -78,18 +80,19 @@ def __init__( Clifford samples to shorter sequences. Raises: - QiskitError: the interleaved_element is not convertible to Clifford object. + QiskitError: the interleaved_element is invalid (e.g. not convertible to Clifford object). """ + 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})." + ) try: self._interleaved_elem = 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() super().__init__( qubits, lengths, @@ -98,7 +101,14 @@ def __init__( seed=seed, full_sampling=full_sampling, ) - self._transpiled_interleaved_elem = None + # Convert interleaved element to integer for speed + if self.num_qubits <= 2: + interleaved_circ = self._interleaved_elem.to_circuit() + utils = CliffordUtils( + self.num_qubits, basis_gates=self._get_basis_gates() + ) # TODO: cleanup + self._interleaved_elem = utils.compose_num_with_clifford(0, interleaved_circ) + self._interleaved_op = interleaved_element self.analysis = InterleavedRBAnalysis() self.analysis.set_options(outcome="0" * self.num_qubits) @@ -108,60 +118,57 @@ def circuits(self) -> List[QuantumCircuit]: Returns: A list of :class:`QuantumCircuit`. + Raises: + QiskitError: if fail to transpile interleaved_element. """ - if self.num_qubits > 2: - return super().circuits() + basis_gates = self._get_basis_gates() + self._cliff_utils = CliffordUtils(self.num_qubits, basis_gates=basis_gates) # TODO: cleanup - self._set_basis_gates() - self._initialize_clifford_utils() - rng = default_rng(seed=self.experiment_options.seed) - circuits = [] - for _ in range(self.experiment_options.num_samples): - self._set_transpiled_interleaved_element() - std_circuits, int_circuits = self._build_rb_circuits( - self.experiment_options.lengths, - rng, - ) - circuits += std_circuits - circuits += int_circuits - return circuits - - def _set_transpiled_interleaved_element(self): - """ - Create the transpiled interleaved element. If it is a single gate, - create a circuit comprising this gate. - """ + # Convert interleaved element to transpiled circuit operations and store them for speed + # Convert interleaved element to circuit if isinstance(self._interleaved_op, QuantumCircuit): - qc_interleaved = self._interleaved_op - else: - qc_interleaved = QuantumCircuit(self.num_qubits, self.num_qubits) - qubits = list(range(self.num_qubits)) - qc_interleaved.append(self._interleaved_op, qubits) - self._transpiled_interleaved_elem = qc_interleaved - - if hasattr(self.transpile_options, "basis_gates"): - basis_gates = self.transpile_options.basis_gates - else: - basis_gates = None - self._transpiled_interleaved_elem = transpile( - circuits=qc_interleaved, - optimization_level=1, - basis_gates=basis_gates, - backend=self._backend, - ) - - def _sample_circuits(self) -> List[QuantumCircuit]: - """Return a list of RB circuits. + interleaved_circ = self._interleaved_op + elif isinstance(self._interleaved_op, Clifford): + interleaved_circ = self._interleaved_op.to_circuit() + else: # Instruction + interleaved_circ = QuantumCircuit(self.num_qubits, name=self._interleaved_op.name) + interleaved_circ.append(self._interleaved_op, list(range(self.num_qubits))) + interleaved_circ.name = f"Clifford-{interleaved_circ.name}" + 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. Check if transpile_options is correct." + " Note that using delays in dt unit satisfying timing constraints is faster" + " than transpiling with scheduling_method." + ) 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 + # assert isinstance(self._interleaved_op, (Instruction, QuantumCircuit) + if not isinstance(self._interleaved_op, Instruction): + self._interleaved_op = self._interleaved_op.to_instruction() - Returns: - A list of :class:`QuantumCircuit`. - """ # Build circuits of reference sequences reference_sequences = self._sample_sequences() reference_circuits = self._sequences_to_circuits(reference_sequences) - for circ in reference_circuits: - circ.metadata["interleaved"] = False - + 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, + "interleaved": False, + } # Build circuits of interleaved sequences interleaved_sequences = [] for seq in reference_sequences: @@ -171,204 +178,20 @@ def _sample_circuits(self) -> List[QuantumCircuit]: new_seq.append(self._interleaved_elem) interleaved_sequences.append(new_seq) interleaved_circuits = self._sequences_to_circuits(interleaved_sequences) - for circ in interleaved_circuits: - circ.metadata["interleaved"] = True + 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, + "interleaved": True, + } return reference_circuits + interleaved_circuits - def _to_instruction(self, elem: SequenceElementType) -> Instruction: + def _to_instruction( + self, elem: SequenceElementType, basis_gates: Optional[Tuple[str]] = None + ) -> Instruction: if elem is self._interleaved_elem: return self._interleaved_op - return super()._to_instruction(elem) - - def _build_rb_circuits(self, lengths: List[int], rng: Generator) -> List[QuantumCircuit]: - """ - build_rb_circuits - Args: - lengths: A list of RB sequence lengths. We create random circuits - where the number of cliffords in each is defined in 'lengths'. - rng: Generator object for random number generation. - If None, default_rng will be used. - - Returns: - The transpiled RB circuits. - - Additional information: - To create the RB circuit, we use a mapping between Cliffords and integers - defined in the file clifford_data.py. The operations compose and inverse are much faster - when performed on the integers rather than on the Cliffords themselves. - """ - if self._full_sampling: - return self._build_rb_circuits_full_sampling(lengths, rng) - max_qubit = max(self.physical_qubits) + 1 - all_rb_circuits = [] - all_rb_interleaved_circuits = [] - - # When full_sampling==False, each circuit is the prefix of the next circuit (without the - # inverse Clifford at the end of the circuit. The variable 'circ' will contain - # the growing circuit. - # When each circuit reaches its length, we copy it to rb_circ, append the inverse, - # and add it to the list of circuits. - n = self.num_qubits - qubits = list(range(n)) - clbits = list(range(n)) - - interleaved_circ = QuantumCircuit(max_qubit, n) - interleaved_circ.barrier(qubits) - # We transpile the empty circuit to match the backend qubits - interleaved_circ = transpile( - circuits=interleaved_circ, - optimization_level=1, - basis_gates=self.transpile_options.basis_gates, - backend=self._backend, - ) - - circ = QuantumCircuit(max_qubit, n) - circ.barrier(qubits) - # We transpile the empty circuit to match the backend qubits - circ = transpile( - circuits=circ, - optimization_level=1, - basis_gates=self.transpile_options.basis_gates, - backend=self._backend, - ) - # composed_cliff_num is the number representing the composition of all the Cliffords up to now - # composed_interleaved_num is the same for an interleaved circuit - composed_cliff_num = 0 # 0 is the Clifford that is Id - composed_interleaved_num = 0 - prev_length = 0 - - for length in lengths: - for i in range(prev_length, length): - circ, next_circ, composed_cliff_num = self._add_random_cliff_to_circ( - circ, composed_cliff_num, qubits, rng - ) - interleaved_circ, composed_interleaved_num = self._add_cliff_to_circ( - interleaved_circ, next_circ, composed_interleaved_num, qubits - ) - - # The interleaved element is appended after every Clifford - interleaved_circ, composed_interleaved_num = self._add_cliff_to_circ( - interleaved_circ, - self._transpiled_interleaved_elem, - composed_interleaved_num, - qubits, - ) - if i == length - 1: - rb_circ = circ.copy() # circ is used as the prefix of the next circuit - rb_circ = self._add_inverse_to_circ(rb_circ, composed_cliff_num, qubits, clbits) - - rb_circ.metadata = { - "experiment_type": "rb", - "xval": length, - "group": "Clifford", - "physical_qubits": self.physical_qubits, - "interleaved": False, - } - all_rb_circuits.append(rb_circ) - - # interleaved_circ is used as the prefix of the next circuit - rb_interleaved_circ = interleaved_circ.copy() - rb_interleaved_circ = self._add_inverse_to_circ( - rb_interleaved_circ, composed_interleaved_num, qubits, clbits - ) - rb_interleaved_circ.metadata = { - "experiment_type": "rb", - "xval": length, - "group": "Clifford", - "physical_qubits": self.physical_qubits, - "interleaved": True, - } - all_rb_interleaved_circuits.append(rb_interleaved_circ) - - prev_length = i + 1 - return all_rb_circuits, all_rb_interleaved_circuits - - def _build_rb_circuits_full_sampling( - self, lengths: List[int], rng: Generator - ) -> List[QuantumCircuit]: - """ - _build_rb_circuits_full_sampling - Args: - lengths: A list of RB sequence lengths. We create random circuits - where the number of cliffords in each is defined in ''lengths'. - rng: Generator object for random number generation. - If None, default_rng will be used. - interleaved_element: the interleaved element as a QuantumCircuit. - - Returns: - The transpiled RB circuits. - - Additional information: - This is similar to _build_rb_circuits for the case of full_sampling. - """ - all_rb_circuits = [] - all_rb_interleaved_circuits = [] - - n = self.num_qubits - qubits = list(range(n)) - clbits = list(range(n)) - max_qubit = max(self.physical_qubits) + 1 - for length in lengths: - # We define the circuit size here, for the layout that will - # be created later - rb_circ = QuantumCircuit(max_qubit, n) - rb_circ.barrier(qubits) - # We transpile the empty circuit to match the backend qubits - rb_circ = transpile( - circuits=rb_circ, - optimization_level=1, - basis_gates=self.transpile_options.basis_gates, - backend=self._backend, - ) - rb_interleaved_circ = QuantumCircuit(max_qubit, n) - rb_interleaved_circ.barrier(qubits) - rb_interleaved_circ = transpile( - circuits=rb_interleaved_circ, - optimization_level=1, - basis_gates=self.transpile_options.basis_gates, - backend=self._backend, - ) - # composed_cliff_num is the number representing the composition of - # all the Cliffords up to now - # composed_interleaved_num is the same for an interleaved circuit - composed_cliff_num = 0 - composed_interleaved_num = 0 - # For full_sampling, we create each circuit independently. - for _ in range(length): - rb_circ, next_circ, composed_cliff_num = self._add_random_cliff_to_circ( - rb_circ, composed_cliff_num, qubits, rng - ) - rb_interleaved_circ, composed_interleaved_num = self._add_cliff_to_circ( - rb_interleaved_circ, next_circ, composed_interleaved_num, qubits - ) - # The interleaved element is appended after every Clifford and its barrier - rb_interleaved_circ, composed_interleaved_num = self._add_cliff_to_circ( - rb_interleaved_circ, - self._transpiled_interleaved_elem, - composed_interleaved_num, - qubits, - ) - - rb_circ = self._add_inverse_to_circ(rb_circ, composed_cliff_num, qubits, clbits) - rb_circ.metadata = { - "experiment_type": "rb", - "xval": length, - "group": "Clifford", - "physical_qubits": self.physical_qubits, - "interleaved": False, - } - - rb_interleaved_circ = self._add_inverse_to_circ( - rb_interleaved_circ, composed_interleaved_num, qubits, clbits - ) - rb_interleaved_circ.metadata = { - "experiment_type": "rb", - "xval": length, - "group": "Clifford", - "physical_qubits": self.physical_qubits, - "interleaved": True, - } - all_rb_circuits.append(rb_circ) - all_rb_interleaved_circuits.append(rb_interleaved_circ) - return all_rb_circuits, all_rb_interleaved_circuits + return super()._to_instruction(elem, basis_gates) diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index eb915182a6..83e711bf9a 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -12,34 +12,34 @@ """ Standard RB Experiment class. """ - import logging 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 import QuantumCircuit, ClassicalRegister, QiskitError -from qiskit.circuit import Clbit -from qiskit.circuit import Instruction -from qiskit.providers.backend import Backend -from qiskit.compiler import transpile -from qiskit.quantum_info import Clifford, random_clifford - +from qiskit.circuit import QuantumCircuit, Instruction, Barrier +from qiskit.exceptions import QiskitError +from qiskit.providers.backend import Backend, BackendV2 +from qiskit.quantum_info import Clifford +from qiskit.quantum_info.random import random_clifford from qiskit_experiments.framework import BaseExperiment, Options from qiskit_experiments.framework.restless_mixin import RestlessMixin from .clifford_utils import ( CliffordUtils, _clifford_1q_int_to_instruction, _clifford_2q_int_to_instruction, + _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): @@ -67,9 +67,6 @@ class StandardRB(BaseExperiment, RestlessMixin): """ - default_basis_gates = {"rz", "sx", "cx"} - _clifford_utils = None - def __init__( self, qubits: Sequence[int], @@ -118,8 +115,7 @@ def __init__( ) self.analysis.set_options(outcome="0" * self.num_qubits) - # Set fixed options - self._full_sampling = full_sampling + self._cliff_utils = None # TODO: cleanup @classmethod def _default_experiment_options(cls) -> Options: @@ -148,27 +144,23 @@ def circuits(self) -> List[QuantumCircuit]: Returns: A list of :class:`QuantumCircuit`. - """ - self._set_basis_gates() - self._initialize_clifford_utils() - rng = default_rng(seed=self.experiment_options.seed) - circuits = [] - - if self.num_qubits in [1, 2]: - for _ in range(self.experiment_options.num_samples): - rb_circuits = self._build_rb_circuits(self.experiment_options.lengths, rng) - circuits += rb_circuits - else: - for _ in range(self.experiment_options.num_samples): - circuits += self._sample_circuits() - - return circuits - - # The following methods are used for RB with more than 2 qubits - def _sample_circuits(self): + self._cliff_utils = CliffordUtils( + self.num_qubits, basis_gates=self._get_basis_gates() + ) # TODO: cleanup + # Sample random Clifford sequences sequences = self._sample_sequences() - return self._sequences_to_circuits(sequences) + # Convert each sequence into circuit and append the inverse to the end. + circuits = self._sequences_to_circuits(sequences) + # 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, + } + return circuits def _sample_sequences(self) -> List[Sequence[SequenceElementType]]: """Sample RB sequences @@ -190,14 +182,35 @@ 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. + + Returns: + Sorted basis gate names. + """ + # Basis gates to use in basis transformation during circuit generation for 1Q/2Q cases + 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 + else: + basis_gates = self.backend.configuration().basis_gates + + if basis_gates: + basis_gates = tuple(sorted(basis_gates)) + + return basis_gates + def _sequences_to_circuits( self, sequences: List[Sequence[SequenceElementType]] ) -> List[QuantumCircuit]: - """Convert an RB sequence into circuit and append the inverse to the end. + """Convert a 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 ( @@ -206,49 +219,43 @@ 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) + circ.append(Barrier(self.num_qubits), circ.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 - # Add metadata - circ.metadata = { - "experiment_type": self._type, - "xval": len(seq), - "group": "Clifford", - "physical_qubits": self.physical_qubits, - } 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 + # Return integer instead of Clifford object for 1 or 2 qubits case for speed if self.num_qubits == 1: return rng.integers(24, size=length) if self.num_qubits == 2: return rng.integers(11520, size=length) + # Return 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)] - 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 + 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: @@ -256,234 +263,57 @@ def __identity_clifford(self) -> SequenceElementType: return 0 return Clifford(np.eye(2 * self.num_qubits)) + def __compose_clifford_seq( + self, org: SequenceElementType, seq: Sequence[SequenceElementType] + ) -> SequenceElementType: + if self.num_qubits <= 2: + new = org + for elem in seq: + new = self.__compose_clifford(new, elem) + return new + # 3 or more qubits: compose Clifford from circuits for speed + # TODO: Revisit after terra#7269, #7483, #8585 + circ = QuantumCircuit(self.num_qubits) + for elem in seq: + circ.compose(elem, inplace=True) + return org.compose(Clifford.from_circuit(circ)) + def __compose_clifford( self, lop: SequenceElementType, rop: SequenceElementType ) -> SequenceElementType: - # TODO: Speed up 1Q (and 2Q) cases using integer clifford composition - # Integer clifford composition is 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) + if self.num_qubits <= 2: + utils = self._cliff_utils + return utils.compose_num_with_clifford(lop, utils.create_cliff_from_num(rop)) + return lop.compose(rop) def __adjoint_clifford(self, op: SequenceElementType) -> SequenceElementType: - # TODO: Speed up 1Q and 2Q cases using integer clifford inversion - # Integer clifford inversion has not yet supported - if isinstance(op, Integral): - if self.num_qubits == 1: - return CliffordUtils.clifford_1_qubit(op).adjoint() - if self.num_qubits == 2: - return CliffordUtils.clifford_2_qubit(op).adjoint() - return op.adjoint() - - # The following methods are used for RB with 1 or 2 qubits - def _build_rb_circuits(self, lengths: List[int], rng: Generator) -> List[QuantumCircuit]: - """ - build_rb_circuits - Args: - lengths: A list of RB sequence lengths. We create random circuits - where the number of cliffords in each is defined in 'lengths'. - rng: Generator object for random number generation. - If None, default_rng will be used. - - Returns: - The transpiled RB circuits. - - Additional information: - To create the RB circuit, we use a mapping between Cliffords and integers - defined in the file clifford_data.py. The operations compose and inverse are much faster - when performed on the integers rather than on the Cliffords themselves. - """ - if self._full_sampling: - return self._build_rb_circuits_full_sampling(lengths, rng) - max_qubit = max(self.physical_qubits) + 1 - all_rb_circuits = [] - - # When full_sampling==False, each circuit is the prefix of the next circuit (without the - # inverse Clifford at the end of the circuit. The variable 'circ' will contain - # the growing circuit. - # When each circuit reaches its length, we copy it to rb_circ, append the inverse, - # and add it to the list of circuits. - n = self.num_qubits - qubits = list(range(n)) - clbits = list(range(n)) - circ = QuantumCircuit(max_qubit, n) - circ.barrier(qubits) - # We transpile the empty circuit to match the backend qubits - circ = transpile( - circuits=circ, - optimization_level=1, - basis_gates=self.transpile_options.basis_gates, - backend=self._backend, - ) - - # composed_cliff_num is the number representing the composition of all the Cliffords up to now - composed_cliff_num = 0 # 0 is the Clifford that is Id - prev_length = 0 - - for length in lengths: - for i in range(prev_length, length): - circ, _, composed_cliff_num = self._add_random_cliff_to_circ( - circ, composed_cliff_num, qubits, rng - ) - - if i == length - 1: - rb_circ = circ.copy() # circ is used as the prefix of the next circuit - rb_circ = self._add_inverse_to_circ(rb_circ, composed_cliff_num, qubits, clbits) - - rb_circ.metadata = { - "experiment_type": "rb", - "xval": length, - "group": "Clifford", - "physical_qubits": self.physical_qubits, - } - all_rb_circuits.append(rb_circ) - prev_length = i + 1 - return all_rb_circuits - - def _build_rb_circuits_full_sampling( - self, lengths: List[int], rng: Generator - ) -> List[QuantumCircuit]: - """ - _build_rb_circuits_full_sampling - Args: - lengths: A list of RB sequence lengths. We create random circuits - where the number of cliffords in each is defined in 'lengths'. - rng: Generator object for random number generation. - If None, default_rng will be used. - - Returns: - The transpiled RB circuits. - - Additional information: - This is similar to _build_rb_circuits for the case of full_sampling. - """ - all_rb_circuits = [] - n = self.num_qubits - qubits = list(range(n)) - clbits = list(range(n)) - max_qubit = max(self.physical_qubits) + 1 - for length in lengths: - # We define the circuit size here, for the layout that will - # be created later - rb_circ = QuantumCircuit(max_qubit, n) - rb_circ.barrier(qubits) - # We transpile the empty circuit to match the backend qubits - rb_circ = transpile( - circuits=rb_circ, - optimization_level=1, - basis_gates=self.transpile_options.basis_gates, - backend=self._backend, - ) - - # composed_cliff_num is the number representing the composition of - # all the Cliffords up to now - composed_cliff_num = 0 - - # For full_sampling, we create each circuit independently. - for _ in range(length): - # choose random clifford - rb_circ, _, composed_cliff_num = self._add_random_cliff_to_circ( - rb_circ, composed_cliff_num, qubits, rng - ) - - rb_circ = self._add_inverse_to_circ(rb_circ, composed_cliff_num, qubits, clbits) - - rb_circ.metadata = { - "experiment_type": "rb", - "xval": length, - "group": "Clifford", - "physical_qubits": self.physical_qubits, - "interleaved": False, - } - - all_rb_circuits.append(rb_circ) - return all_rb_circuits + if self.num_qubits <= 2: + return self._cliff_utils.inverse_cliff(op) - def _add_random_cliff_to_circ(self, circ, composed_cliff_num, qubits, rng): - next_circ = StandardRB._clifford_utils.create_random_clifford(rng) - circ, composed_cliff_num = self._add_cliff_to_circ( - circ, next_circ, composed_cliff_num, qubits - ) - return circ, next_circ, composed_cliff_num + if isinstance(op, QuantumCircuit): + return Clifford.from_circuit(op).adjoint() - def _add_cliff_to_circ( - self, - circ: QuantumCircuit, - next_circ: QuantumCircuit, - composed_cliff_num: int, - qubits: List[int], - ): - """Append a Clifford to the end of a circuit. Return both the updated circuit and the updated - number representing the circuit""" - circ.compose(next_circ, inplace=True) - composed_cliff_num = StandardRB._clifford_utils.compose_num_with_clifford( - composed_num=composed_cliff_num, - qc=next_circ, - ) - circ.barrier(qubits) - return circ, composed_cliff_num - - def _add_inverse_to_circ(self, rb_circ, composed_num, qubits, clbits): - """Append the inverse of a circuit to the end of the circuit""" - inverse_cliff = StandardRB._clifford_utils.inverse_cliff(composed_num) - rb_circ.compose(inverse_cliff, inplace=True) - rb_circ.measure(qubits, clbits) - return rb_circ - - # This method does a quick layout to avoid calling 'transpile()' which is - # very costly in performance - # We simply copy the circuit to a new circuit where we define the mapping - # of the qubit to the single physical qubit that was requested by the user - # This is a hack, and would be better if transpile() implemented it. - # Something similar is done in ParallelExperiment._combined_circuits - def _layout_for_rb(self): - transpiled = [] - qargs_map = ( - {0: self.physical_qubits[0]} - if self.num_qubits == 1 - else {0: self.physical_qubits[0], 1: self.physical_qubits[1]} - ) - for circ in self.circuits(): - new_circ = QuantumCircuit( - *circ.qregs, - name=circ.name, - global_phase=circ.global_phase, - metadata=circ.metadata.copy(), - ) - clbits = circ.num_clbits - if clbits: - creg = ClassicalRegister(clbits) - new_cargs = [Clbit(creg, i) for i in range(clbits)] - new_circ.add_register(creg) - - for inst, qargs, cargs in circ.data: - mapped_cargs = [new_cargs[circ.find_bit(clbit).index] for clbit in cargs] - mapped_qargs = [circ.qubits[qargs_map[circ.find_bit(i).index]] for i in qargs] - new_circ.data.append((inst, mapped_qargs, mapped_cargs)) - # Add the calibrations - for gate, cals in circ.calibrations.items(): - for key, sched in cals.items(): - new_circ.add_calibration(gate, qubits=key[0], schedule=sched, params=key[1]) - - transpiled.append(new_circ) - return transpiled + return op.adjoint() def _transpiled_circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits, transpiled.""" - if self.num_qubits in [1, 2]: - transpiled = self._layout_for_rb() + has_custom_transpile_option = ( + any( + opt not in {"basis_gates", "optimization_level"} + for opt in vars(self.transpile_options) + ) + or self.transpile_options.get("optimization_level", 0) != 0 + ) + if self.num_qubits <= 2 and not has_custom_transpile_option: + transpiled = [ + _transpile_clifford_circuit(circ, layout=self.physical_qubits) + for circ in self.circuits() + ] else: transpiled = super()._transpiled_circuits() - if self.analysis.options.get("gate_error_ratio", None) is None: + if self.analysis.options.get("gate_error_ratio", None) is None: # Gate errors are not computed, then counting ops is not necessary. return transpiled @@ -503,6 +333,7 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]: formatted_key = tuple(sorted(qinds)), inst.name count_ops_result[formatted_key] += 1 circ.metadata["count_ops"] = tuple(count_ops_result.items()) + return transpiled def _metadata(self): @@ -514,21 +345,3 @@ def _metadata(self): metadata[run_opt] = getattr(self.run_options, run_opt) return metadata - - def _initialize_clifford_utils(self): - if StandardRB._clifford_utils is None or not ( - StandardRB._clifford_utils.num_qubits == self.num_qubits - and StandardRB._clifford_utils.basis_gates == self.transpile_options.basis_gates - and StandardRB._clifford_utils._backend == self._backend - ): - StandardRB._clifford_utils = CliffordUtils( - self.num_qubits, self.transpile_options.basis_gates, backend=self._backend - ) - - def _set_basis_gates(self): - if not hasattr(self.transpile_options, "basis_gates"): - if not self.backend is None and self.backend.configuration().basis_gates: - self.set_transpile_options(basis_gates=self.backend.configuration().basis_gates) - else: - basis_gates_option = {"basis_gates": StandardRB.default_basis_gates} - self.transpile_options.update_options(**basis_gates_option) diff --git a/test/library/randomized_benchmarking/test_randomized_benchmarking.py b/test/library/randomized_benchmarking/test_randomized_benchmarking.py index 6e7b8cfa8c..f96047e57c 100644 --- a/test/library/randomized_benchmarking/test_randomized_benchmarking.py +++ b/test/library/randomized_benchmarking/test_randomized_benchmarking.py @@ -11,28 +11,299 @@ # that they have been altered from the originals. """Test for randomized benchmarking experiments.""" - from test.base import QiskitExperimentsTestCase -import random -from ddt import ddt, data, unpack import numpy as np +from ddt import ddt, data, unpack from qiskit.circuit import Delay, QuantumCircuit from qiskit.circuit.library import SXGate, CXGate, TGate, CZGate from qiskit.exceptions import QiskitError +from qiskit.providers.fake_provider import FakeManila, FakeWashington 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.library.randomized_benchmarking import CliffordUtils -from qiskit_experiments.framework.composite import ParallelExperiment 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))) + + +@ddt +class TestStandardRB(QiskitExperimentsTestCase, RBTestMixin): + """Test for StandardRB without running the experiments.""" + + def setUp(self): + """Setup the tests.""" + super().setUp() + self.backend = FakeManila() + + # ### 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, + ) + + exp1.set_transpile_options(basis_gates=["rz", "sx", "cx"], optimization_level=1) + exp2.set_transpile_options(basis_gates=["rz", "sx", "cx"], optimization_level=1) + 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, + ) + exp1.set_transpile_options(basis_gates=["rz", "sx", "cx"], optimization_level=1) + exp2 = rb.StandardRB( + qubits=(0,), + lengths=[10, 20, 30], + seed=123, + backend=self.backend, + full_sampling=True, + ) + exp2.set_transpile_options(basis_gates=["rz", "sx", "cx"], optimization_level=1) + 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, + ) + exp1.set_transpile_options(basis_gates=["rz", "sx", "cx"], optimization_level=1) + + exp2 = rb.StandardRB( + qubits=(0, 1), + lengths=[10, 20, 30], + seed=123, + backend=self.backend, + full_sampling=True, + ) + exp2.set_transpile_options(basis_gates=["rz", "sx", "cx"], optimization_level=1) + + 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()) + + +@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"], [3.2e-7, "s"]) + @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], + ) + + 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): + # 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 + + def test_preserve_interleaved_circuit_element(self): + """Interleaved RB should not change a given interleaved circuit during RB circuit generation.""" + interleaved_circ = QuantumCircuit(2, name="bell_with_delay") + interleaved_circ.h(0) + interleaved_circ.delay(160, 0) + interleaved_circ.cx(0, 1) + + exp = rb.InterleavedRB( + interleaved_element=interleaved_circ, qubits=[2, 1], lengths=[1], num_samples=1 + ) + circuits = exp.circuits() + # Get the first interleaved operation in the interleaved RB sequence: + # 0: barrier, 1: clifford, 2: barrier, 3: interleaved + actual = circuits[1][3].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 + exp = rb.InterleavedRB( + interleaved_element=Delay(100), # TODO: Use BackendTiming + qubits=[0], + lengths=[1], + num_samples=1, + seed=1234, # This seed gives a 2-gate clifford + backend=self.backend, + ) + int_circs = exp.circuits()[1] + # barrier, 2-gate clifford, barrier, "delay", barrier, ... + self.assertEqual(int_circs.data[3][0].name, "delay") + 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]) -class RBTestCase(QiskitExperimentsTestCase): - """Base test case for randomized benchmarking defining a common noise model.""" +class RBRunTestCase(QiskitExperimentsTestCase, RBTestMixin): + """Base test case for running RB experiments defining a common noise model.""" def setUp(self): """Setup the tests.""" @@ -70,18 +341,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 - qc_iden = QuantumCircuit(num_qubits) - circ.remove_final_measurements() - assert Operator(circ).equiv(Operator(qc_iden)) - -@ddt -class TestStandardRB(RBTestCase): - """Test for standard RB.""" +class TestRunStandardRB(RBRunTestCase): + """Test for running StandardRB.""" def test_single_qubit(self): """Test single qubit RB.""" @@ -199,17 +461,19 @@ 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) @@ -217,117 +481,6 @@ def test_poor_experiment_result(self): # 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, - ) - - exp1.set_transpile_options(**self.transpiler_options) - exp2.set_transpile_options(**self.transpiler_options) - 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, - ) - exp1.set_transpile_options(**self.transpiler_options) - exp2 = rb.StandardRB( - qubits=(0,), - lengths=[10, 20, 30], - seed=123, - backend=self.backend, - full_sampling=True, - ) - exp2.set_transpile_options(**self.transpiler_options) - circs1 = exp1.circuits() - circs2 = exp2.circuits() - - self.assertEqual(circs1[0].decompose(), circs2[0].decompose()) - - # fully sampled circuits are regenerated while other is just built on top of previous length - self.assertNotEqual(circs1[1].decompose(), circs2[1].decompose()) - self.assertNotEqual(circs1[2].decompose(), circs2[2].decompose()) - - def test_full_sampling_2_qubits(self): - """Test if full sampling generates different circuits.""" - exp1 = rb.StandardRB( - qubits=(0, 1), - lengths=[10, 20, 30], - seed=123, - backend=self.backend, - full_sampling=False, - ) - exp1.set_transpile_options(**self.transpiler_options) - - exp2 = rb.StandardRB( - qubits=(0, 1), - lengths=[10, 20, 30], - seed=123, - backend=self.backend, - full_sampling=True, - ) - exp2.set_transpile_options(**self.transpiler_options) - - circs1 = exp1.circuits() - circs2 = exp2.circuits() - - self.assertEqual(circs1[0].decompose(), circs2[0].decompose()) - - # fully sampled circuits are regenerated while other is just built on top of previous length - self.assertNotEqual(circs1[1].decompose(), circs2[1].decompose()) - self.assertNotEqual(circs1[2].decompose(), circs2[2].decompose()) - - @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( @@ -414,86 +567,28 @@ def test_two_qubit_with_cz(self): self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.5 * epc_expected) -@ddt -class TestInterleavedRB(RBTestCase): - """Test for interleaved RB.""" - - @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. - """ - full_sampling = [True, False] - for val in full_sampling: - exp = rb.InterleavedRB( - interleaved_element=interleaved_element, - qubits=qubits, - lengths=[length], - num_samples=1, - full_sampling=val, - ) - exp.set_transpile_options(**self.transpiler_options) - circuits = exp.circuits() - c_std = circuits[0] - c_int = circuits[1] - if c_std.metadata["interleaved"]: - c_std, c_int = c_int, c_std - num_cliffords = c_std.metadata["xval"] - std_idx = 0 - int_idx = 0 - for _ in range(num_cliffords): - # barrier - self.assertEqual(c_std[std_idx][0].name, "barrier") - self.assertEqual(c_int[int_idx][0].name, "barrier") - # clifford - std_idx += 1 - int_idx += 1 - while c_std[std_idx][0].name != "barrier": - self.assertEqual(c_std[std_idx], c_int[int_idx]) - std_idx += 1 - int_idx += 1 - # for interleaved circuit: barrier + interleaved element - self.assertEqual(c_int[int_idx][0].name, "barrier") - int_idx += 1 - self.assertEqual(c_int[int_idx][0].name, interleaved_element.name) - int_idx += 1 +class TestRunInterleavedRB(RBRunTestCase): + """Test for running InterleavedRB.""" def test_single_qubit(self): - """Test single qubit IRB, once with an interleaved gate, once with an interleaved - Clifford circuit. - """ - interleaved_gate = SXGate() - random.seed(123) - num = random.randint(0, 23) - interleaved_clifford = CliffordUtils.clifford_1_qubit_circuit(num) - # The circuit created for interleaved_clifford is: - # qc = QuantumCircuit(1) - # qc.rz(np.pi/2, 0) - # qc.sx(0) - # qc.rz(np.pi/2, 0) - # Since there is a single sx per interleaved_element, - # therefore epc_expected is the same as for when interleaved_element = SXGate() - for interleaved_element in [interleaved_gate, interleaved_clifford]: - exp = rb.InterleavedRB( - interleaved_element=interleaved_element, - qubits=(0,), - lengths=list(range(1, 300, 30)), - seed=123, - backend=self.backend, - ) - exp.set_transpile_options(**self.transpiler_options) - - self.assertAllIdentity(exp.circuits()) + """Test single qubit IRB.""" + exp = rb.InterleavedRB( + interleaved_element=SXGate(), + qubits=(0,), + lengths=list(range(1, 300, 30)), + seed=123, + backend=self.backend, + ) + exp.set_transpile_options(**self.transpiler_options) + self.assertAllIdentity(exp.circuits()) - expdata = exp.run() - self.assertExperimentDone(expdata) + expdata = exp.run() + self.assertExperimentDone(expdata) - # Since this is interleaved, we can directly compare values, i.e. n_gpc = 1 - epc = expdata.analysis_results("EPC") - epc_expected = 1 / 2 * self.p1q - self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.1 * epc_expected) + # Since this is interleaved, we can directly compare values, i.e. n_gpc = 1 + epc = expdata.analysis_results("EPC") + epc_expected = 1 / 2 * self.p1q + self.assertAlmostEqual(epc.value.n, epc_expected, delta=0.1 * epc_expected) def test_two_qubit(self): """Test two qubit IRB.""" @@ -525,7 +620,7 @@ def test_two_qubit_with_cz(self): interleaved_element=CZGate(), qubits=(0, 1), lengths=list(range(1, 30, 3)), - seed=123, + seed=1234, backend=self.backend, ) exp.set_transpile_options(**transpiler_options) @@ -539,85 +634,6 @@ def test_two_qubit_with_cz(self): epc_expected = 3 / 4 * self.pcz 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 = [0] - 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, - seed=1234, # This seed gives a 2-gate clifford - ) - exp.set_transpile_options(**self.transpiler_options) - - int_circs = exp.circuits()[1] - - # barrier, 2-gate clifford, barrier, "delay", barrier, ... - self.assertEqual(int_circs.data[4][0].name, interleaved_element.name) - - # Transpiled delay duration is represented in seconds, so must convert from us - self.assertEqual(int_circs.data[4][0].unit, "s") - self.assertAlmostEqual(int_circs.data[4][0].params[0], interleaved_element.params[0] * 1e-6) - self.assertAllIdentity([int_circs]) - - def test_interleaving_circuit_with_delay(self): - """Test circuit with delay can be interleaved.""" - 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, - ) - exp.set_transpile_options(**self.transpiler_options) - int_circ = exp.circuits()[1] - self.assertAllIdentity([int_circ]) - - 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()) - def test_expdata_serialization(self): """Test serializing experiment data works.""" exp = rb.InterleavedRB(