diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index 2afd94abbf..a045f334f9 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -126,6 +126,7 @@ def run( # Generate and transpile circuits circuits = transpile(self.circuits(backend), backend, **self.transpile_options.__dict__) + self._postprocess_transpiled_circuits(circuits, backend, **run_options) if isinstance(backend, LegacyBackend): qobj = assemble(circuits, backend=backend, **run_opts) @@ -308,6 +309,10 @@ def set_analysis_options(self, **fields): """ self._analysis_options.update_options(**fields) + def _postprocess_transpiled_circuits(self, circuits, backend, **run_options): + """Additional post-processing of transpiled circuits before running on backend""" + pass + def _metadata(self) -> Dict[str, any]: """Return experiment metadata for ExperimentData. diff --git a/qiskit_experiments/randomized_benchmarking/__init__.py b/qiskit_experiments/randomized_benchmarking/__init__.py index 171dab52db..15e1c6c02f 100644 --- a/qiskit_experiments/randomized_benchmarking/__init__.py +++ b/qiskit_experiments/randomized_benchmarking/__init__.py @@ -39,3 +39,4 @@ from .interleaved_rb_experiment import InterleavedRBExperiment from .rb_analysis import RBAnalysis from .interleaved_rb_analysis import InterleavedRBAnalysis +from .rb_utils import RBUtils diff --git a/qiskit_experiments/randomized_benchmarking/rb_analysis.py b/qiskit_experiments/randomized_benchmarking/rb_analysis.py index f42f7359da..2c6ed3e18a 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_analysis.py +++ b/qiskit_experiments/randomized_benchmarking/rb_analysis.py @@ -26,7 +26,9 @@ get_opt_value, get_opt_error, ) + from qiskit_experiments.analysis.data_processing import multi_mean_xy_data +from .rb_utils import RBUtils class RBAnalysis(CurveAnalysis): @@ -85,6 +87,9 @@ def _default_options(cls): default_options.xlabel = "Clifford Length" default_options.ylabel = "P(0)" default_options.fit_reports = {"alpha": "\u03B1", "EPC": "EPC"} + default_options.error_dict = None + default_options.epg_1_qubit = None + default_options.gate_error_ratio = None return default_options @@ -148,6 +153,16 @@ def _format_data(self, data: CurveData) -> CurveData: data_index=mean_data_index, ) + def _run_analysis(self, experiment_data, **options): + """Run analysis on circuit data.""" + error_dict = options["error_dict"] + qubits = experiment_data.metadata()["physical_qubits"] + if not error_dict: + options["error_dict"] = RBUtils.get_error_dict_from_backend( + experiment_data.backend, qubits + ) + return super()._run_analysis(experiment_data, **options) + def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisResult: """Calculate EPC.""" alpha = get_opt_value(analysis_result, "alpha") @@ -157,4 +172,33 @@ def _post_analysis(self, analysis_result: CurveAnalysisResult) -> CurveAnalysisR analysis_result["EPC"] = scale * (1 - alpha) analysis_result["EPC_err"] = scale * alpha_err / alpha + # Add EPG data + count_ops = [] + for meta in self._data(label="raw_data").metadata: + count_ops += meta["count_ops"] + gates_per_clifford = RBUtils.gates_per_clifford(count_ops) + + num_qubits = len(self._physical_qubits) + gate_error_ratio = self._get_option("gate_error_ratio") + if gate_error_ratio is None: + # we attempt to get the ratio from the backend properties + gate_error_ratio = self._get_option("error_dict") + if num_qubits in [1, 2]: + if num_qubits == 1: + epg = RBUtils.calculate_1q_epg( + analysis_result["EPC"], + self._physical_qubits, + gate_error_ratio, + gates_per_clifford, + ) + elif self._num_qubits == 2: + epg_1_qubit = self._get_option("epg_1_qubit") + epg = RBUtils.calculate_2q_epg( + analysis_result["EPC"], + self._physical_qubits, + gate_error_ratio, + gates_per_clifford, + epg_1_qubit=epg_1_qubit, + ) + analysis_result["EPG"] = epg return analysis_result diff --git a/qiskit_experiments/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/randomized_benchmarking/rb_experiment.py index b253705103..6398bacfab 100644 --- a/qiskit_experiments/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/randomized_benchmarking/rb_experiment.py @@ -27,6 +27,7 @@ from qiskit_experiments.analysis.data_processing import probability from .rb_analysis import RBAnalysis from .clifford_utils import CliffordUtils +from .rb_utils import RBUtils class RBExperiment(BaseExperiment): @@ -46,7 +47,7 @@ def __init__( lengths: Iterable[int], num_samples: int = 1, seed: Optional[Union[int, Generator]] = None, - full_sampling: bool = False, + full_sampling: Optional[bool] = False, ): """Standard randomized benchmarking experiment. @@ -162,3 +163,13 @@ def _generate_circuit( rb_circ.measure_all() circuits.append(rb_circ) return circuits + + def _postprocess_transpiled_circuits(self, circuits, backend, **run_options): + """Additional post-processing of transpiled circuits before running on backend""" + for c in circuits: + c_count_ops = RBUtils.count_ops(c, self.physical_qubits) + circuit_length = c.metadata["xval"] + average_count_ops = [ + (key, value / circuit_length) for key, value in c_count_ops.items() + ] + c.metadata.update({"count_ops": average_count_ops}) diff --git a/qiskit_experiments/randomized_benchmarking/rb_utils.py b/qiskit_experiments/randomized_benchmarking/rb_utils.py new file mode 100644 index 0000000000..d4248376f3 --- /dev/null +++ b/qiskit_experiments/randomized_benchmarking/rb_utils.py @@ -0,0 +1,264 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019-2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +RB Helper functions +""" + +from typing import Tuple, Dict, Optional, Iterable, List +import numpy as np +from qiskit import QiskitError, QuantumCircuit +from qiskit.providers.backend import Backend + + +class RBUtils: + """A collection of utility functions for computing additional data + from randomized benchmarking experiments""" + + @staticmethod + def get_error_dict_from_backend( + backend: Backend, qubits: Iterable[int] + ) -> Dict[Tuple[Iterable[int], str], float]: + """Attempts to extract error estimates for gates from the backend + properties. + Those estimates are used to assign weights for different gate types + when computing error per gate. + + Args: + backend: The backend from which the properties are taken + qubits: The qubits participating in the experiment, used + to filter irrelevant gates from the result. + + Returns: + A dictionary of the form (qubits, gate) -> value that for each + gate on the given qubits gives its recorded error estimate. + """ + error_dict = {} + try: + for backend_gate in backend.properties().gates: + backend_gate = backend_gate.to_dict() + gate_qubits = tuple(backend_gate["qubits"]) + if all(gate_qubit in qubits for gate_qubit in gate_qubits): + for p in backend_gate["parameters"]: + if p["name"] == "gate_error": + error_dict[(gate_qubits, backend_gate["gate"])] = p["value"] + except AttributeError: + # might happen if the backend has no properties (e.g. qasm simulator) + return None + return error_dict + + @staticmethod + def count_ops( + circuit: QuantumCircuit, qubits: Optional[Iterable[int]] = None + ) -> Dict[Tuple[Iterable[int], str], int]: + """Counts occurances of each gate in the given circuit + + Args: + circuit: The quantum circuit whose gates are counted + qubits: A list of qubits to filter irrelevant gates + + Returns: + A dictionary of the form (qubits, gate) -> value where value + is the number of occurrences of the gate on the given qubits + """ + if qubits is None: + qubits = range(len(circuit.qubits)) + count_ops_result = {} + for instr, qargs, _ in circuit._data: + instr_qubits = [] + skip_instr = False + for qubit in qargs: + qubit_index = circuit.qubits.index(qubit) + if qubit_index not in qubits: + skip_instr = True + instr_qubits.append(qubit_index) + if not skip_instr: + instr_qubits = tuple(instr_qubits) + count_ops_result[(instr_qubits, instr.name)] = ( + count_ops_result.get((instr_qubits, instr.name), 0) + 1 + ) + return count_ops_result + + @staticmethod + def gates_per_clifford( + ops_count: List, + ) -> Dict[Tuple[Iterable[int], str], float]: + """ + Computes the average number of gates per clifford for each gate type + in the input from raw count data coming from multiple circuits. + Args: + ops_count: A List of [key, value] pairs where + key is [qubits, gate_name] and value is the average + number of gates per clifford of the type for the given key + + Returns: + A dictionary with the mean value of values corresponding + to key for each key. + + """ + result = {} + for ((qubits, gate_name), value) in ops_count: + qubits = tuple(qubits) # so we can hash + if (qubits, gate_name) not in result: + result[(qubits, gate_name)] = [] + result[(qubits, gate_name)].append(value) + return {key: np.mean(value) for (key, value) in result.items()} + + @staticmethod + def coherence_limit(nQ=2, T1_list=None, T2_list=None, gatelen=0.1): + """ + The error per gate (1-average_gate_fidelity) given by the T1,T2 limit. + Args: + nQ (int): number of qubits (1 and 2 supported). + T1_list (list): list of T1's (Q1,...,Qn). + T2_list (list): list of T2's (as measured, not Tphi). + If not given assume T2=2*T1 . + gatelen (float): length of the gate. + Returns: + float: coherence limited error per gate. + Raises: + ValueError: if there are invalid inputs + """ + # pylint: disable = invalid-name + + T1 = np.array(T1_list) + + if T2_list is None: + T2 = 2 * T1 + else: + T2 = np.array(T2_list) + + if len(T1) != nQ or len(T2) != nQ: + raise ValueError("T1 and/or T2 not the right length") + + coherence_limit_err = 0 + + if nQ == 1: + + coherence_limit_err = 0.5 * ( + 1.0 - 2.0 / 3.0 * np.exp(-gatelen / T2[0]) - 1.0 / 3.0 * np.exp(-gatelen / T1[0]) + ) + + elif nQ == 2: + + T1factor = 0 + T2factor = 0 + + for i in range(2): + T1factor += 1.0 / 15.0 * np.exp(-gatelen / T1[i]) + T2factor += ( + 2.0 + / 15.0 + * ( + np.exp(-gatelen / T2[i]) + + np.exp(-gatelen * (1.0 / T2[i] + 1.0 / T1[1 - i])) + ) + ) + + T1factor += 1.0 / 15.0 * np.exp(-gatelen * np.sum(1 / T1)) + T2factor += 4.0 / 15.0 * np.exp(-gatelen * np.sum(1 / T2)) + + coherence_limit_err = 0.75 * (1.0 - T1factor - T2factor) + + else: + raise ValueError("Not a valid number of qubits") + + return coherence_limit_err + + @staticmethod + def calculate_1q_epg( + epc_1_qubit: float, + qubits: Iterable[int], + gate_error_ratio: Dict[str, float], + gates_per_clifford: Dict[Tuple[Iterable[int], str], float], + ) -> Dict[int, Dict[str, float]]: + r""" + Convert error per Clifford (EPC) into error per gates (EPGs) of single qubit basis gates. + Args: + epc_1_qubit: The error per clifford rate obtained via experiment + qubits: The qubits for which to compute epg + gate_error_ratio: Estiamte for the ratios between errors on different gates + gates_per_clifford: The computed gates per clifford data + Returns: + A dictionary of the form (qubits, gate) -> value where value + is the epg for the given gate on the specified qubits + """ + epg = {qubit: {} for qubit in qubits} + for qubit in qubits: + error_sum = 0 + found_gates = [] + for (key, value) in gate_error_ratio.items(): + qubits, gate = key + if len(qubits) == 1 and qubits[0] == qubit and key in gates_per_clifford: + found_gates.append(gate) + error_sum += gates_per_clifford[key] * value + for gate in found_gates: + epg[qubit][gate] = (gate_error_ratio[((qubit,), gate)] * epc_1_qubit) / error_sum + return epg + + @staticmethod + def calculate_2q_epg( + epc_2_qubit: float, + qubits: Iterable[int], + gate_error_ratio: Dict[str, float], + gates_per_clifford: Dict[Tuple[Iterable[int], str], float], + epg_1_qubit: Optional[Dict[int, Dict[str, float]]] = None, + gate_2_qubit_type: Optional[str] = "cx", + ) -> Dict[int, Dict[str, float]]: + r""" + Convert error per Clifford (EPC) into error per gates (EPGs) of two-qubit basis gates. + Assumes a single two-qubit gate type is used in transpilation + Args: + epc_2_qubit: The error per clifford rate obtained via experiment + qubits: The qubits for which to compute epg + gate_error_ratio: Estiamte for the ratios between errors on different gates + gates_per_clifford: The computed gates per clifford data + epg_1_qubit: epg data for the 1-qubits gate involved, assumed to + have been obtained from previous experiments + gate_2_qubit_type: The name of the 2-qubit gate to be analyzed + Returns: + The epg value for the specified gate on the specified qubits + given in a dictionary form as in calculate_1q_epg + Raises: + QiskitError: if a non 2-qubit gate was given + """ + epg_2_qubit = {} + qubit_pairs = [] + for key in gate_error_ratio: + qubits, gate = key + if gate == gate_2_qubit_type and key in gates_per_clifford: + if len(qubits) != 2: + raise QiskitError( + "The gate {} is a {}-qubit gate (should be 2-qubit)".format( + gate, len(qubits) + ) + ) + qubit_pair = tuple(sorted(qubits)) + if qubit_pair not in qubit_pairs: + qubit_pairs.append(qubit_pair) + for qubit_pair in qubit_pairs: + alpha_1q = [1.0, 1.0] + if epg_1_qubit is not None: + list_epgs_1q = [epg_1_qubit[qubit_pair[i]] for i in range(2)] + for ind, (qubit, epg_1q) in enumerate(zip(qubit_pair, list_epgs_1q)): + for gate_name, epg in epg_1q.items(): + n_gate = gates_per_clifford.get(((qubit,), gate_name), 0) + alpha_1q[ind] *= (1 - 2 * epg) ** n_gate + alpha_c_1q = 1 / 5 * (alpha_1q[0] + alpha_1q[1] + 3 * alpha_1q[0] * alpha_1q[1]) + alpha_c_2q = (1 - 4 / 3 * epc_2_qubit) / alpha_c_1q + inverse_qubit_pair = (qubit_pair[1], qubit_pair[0]) + n_gate_2q = gates_per_clifford.get( + (qubit_pair, gate_2_qubit_type), 0 + ) + gates_per_clifford.get((inverse_qubit_pair, gate_2_qubit_type), 0) + epg = 3 / 4 * (1 - alpha_c_2q) / n_gate_2q + epg_2_qubit[qubit_pair] = {gate_2_qubit_type: epg} + return epg_2_qubit diff --git a/test/test_rb.py b/test/test_rb.py index df8cb9764c..d0dbb33cdc 100644 --- a/test/test_rb.py +++ b/test/test_rb.py @@ -22,7 +22,20 @@ from qiskit.quantum_info import Clifford from qiskit.test import QiskitTestCase from qiskit.test.mock import FakeParis -from qiskit.circuit.library import XGate, CXGate +from qiskit import QuantumCircuit +from qiskit.circuit.library import ( + IGate, + XGate, + YGate, + ZGate, + HGate, + SGate, + SdgGate, + CXGate, + CZGate, + SwapGate, +) +from qiskit.providers.aer import AerSimulator import qiskit_experiments as qe @@ -40,7 +53,7 @@ def test_rb_experiment(self, qubits: list): Args: qubits (list): A list containing qubit indices for the experiment """ - backend = FakeParis() + backend = AerSimulator.from_backend(FakeParis()) exp_attributes = { "qubits": qubits, "lengths": [1, 4, 6, 9, 13, 16], @@ -135,7 +148,7 @@ def test_interleaved_rb_experiment(self, interleaved_element: "Gate", qubits: li interleaved_element: The Clifford element to interleave qubits (list): A list containing qubit indices for the experiment """ - backend = FakeParis() + backend = AerSimulator.from_backend(FakeParis()) exp_attributes = { "interleaved_element": interleaved_element, "qubits": qubits, @@ -157,3 +170,69 @@ def test_interleaved_rb_experiment(self, interleaved_element: "Gate", qubits: li self.validate_metadata(exp_circuits, exp_attributes) self.validate_circuit_data(exp_data, exp_attributes) self.is_identity(exp_circuits) + + +@ddt +class TestRBUtilities(QiskitTestCase): + """ + A test class for additional functionality provided by the RBExperiment + class. + """ + + instructions = { + "i": IGate(), + "x": XGate(), + "y": YGate(), + "z": ZGate(), + "h": HGate(), + "s": SGate(), + "sdg": SdgGate(), + "cx": CXGate(), + "cz": CZGate(), + "swap": SwapGate(), + } + seed = 42 + + @data( + [1, {((0,), "x"): 3, ((0,), "y"): 2, ((0,), "h"): 1}], + [5, {((1,), "x"): 3, ((4,), "y"): 2, ((1,), "h"): 1, ((1, 4), "cx"): 7}], + ) + @unpack + def test_count_ops(self, num_qubits, expected_counts): + """Testing the count_ops utility function + this function receives a circuit and counts the number of gates + in it, counting gates for different qubits separately""" + circuit = QuantumCircuit(num_qubits) + gates_to_add = [] + for gate, count in expected_counts.items(): + gates_to_add += [gate for _ in range(count)] + rng = np.random.default_rng(self.seed) + rng.shuffle(gates_to_add) + for qubits, gate in gates_to_add: + circuit.append(self.instructions[gate], qubits) + counts = qe.randomized_benchmarking.RBUtils.count_ops(circuit) + self.assertDictEqual(expected_counts, counts) + + def test_calculate_1q_epg(self): + """Testing the calculation of 1 qubit error per gate + The EPG is computed based on the error per clifford determined + in the RB experiment, the gate counts, and an estimate about the + relations between the errors of different gate types + """ + epc_1_qubit = 0.0037 + qubits = [0] + gate_error_ratio = {((0,), "id"): 1, ((0,), "rz"): 0, ((0,), "sx"): 1, ((0,), "x"): 1} + gates_per_clifford = {((0,), "rz"): 10.5, ((0,), "sx"): 8.15, ((0,), "x"): 0.25} + epg = qe.randomized_benchmarking.RBUtils.calculate_1q_epg( + epc_1_qubit, qubits, gate_error_ratio, gates_per_clifford + ) + error_dict = { + ((0,), "rz"): 0, + ((0,), "sx"): 0.0004432101747785104, + ((0,), "x"): 0.0004432101747785104, + } + + for gate in ["x", "sx", "rz"]: + expected_epg = error_dict[((0,), gate)] + actual_epg = epg[0][gate] + self.assertTrue(np.allclose(expected_epg, actual_epg, rtol=1.0e-2))