Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0b14f7f
Added the rb_utils file from Ignis as a class
gadial Jun 1, 2021
ce68e02
Adding transpiled circuits postprocessing
gadial Jun 2, 2021
bac0cff
Computing gate count by qubit
gadial Jun 2, 2021
7f85816
gates_per_clifford was simplified and removed from rb_utils
gadial Jun 2, 2021
e3fd85e
Getting the error dict from the backend
gadial Jun 8, 2021
517aeb9
Computing gates per clifford
gadial Jun 8, 2021
f84c740
EPG for 1 qubit is now working
gadial Jun 9, 2021
79311a4
Refactoring for EPG on 2 qubits
gadial Jun 9, 2021
845a41b
1 qubit epg is working after refactoring
gadial Jun 9, 2021
272159e
2 qubit epg is done but might have bugs
gadial Jun 9, 2021
1102f82
Finalizing epg analysis and removing debug prints
gadial Jun 10, 2021
82bccfb
Added documentation
gadial Jun 10, 2021
ecfb295
Linting
gadial Jun 10, 2021
687ea1b
Small fix
gadial Jun 10, 2021
822b613
Merge branch 'main' into rb_utils_class
gadial Jun 10, 2021
15602b2
Integrating the curve analysis class with the epg computation in RB
gadial Jun 10, 2021
dc8fcf4
Added epg computation test
gadial Jun 10, 2021
37d71c3
Merge branch 'main' into rb_utils_class
gadial Jun 14, 2021
7a91529
Merge branch 'main' into rb_utils_class
gadial Jun 16, 2021
8181b17
Bugfix in n_gate_2q computation
gadial Jun 16, 2021
a0caff8
Passing RB analysis data via options
gadial Jun 16, 2021
cd97183
Linting
gadial Jun 16, 2021
ed0e09e
Linting
gadial Jun 16, 2021
ef0504b
Linting
gadial Jun 16, 2021
8a0a274
Lower test sensitivity
gadial Jun 16, 2021
c2a99cc
Adding more lengths to the rb test
gadial Jun 16, 2021
4f99de1
Linting
gadial Jun 16, 2021
69b8bc5
Merge branch 'main' into rb_utils_class
gadial Jun 17, 2021
7d7a728
Temporarily disabling test due to nondeterministic behavior on testin…
gadial Jun 17, 2021
f92d02f
Temporarily disabling test due to nondeterministic behavior on testin…
gadial Jun 17, 2021
b4382e8
Merge branch 'main' into rb_utils_class
gadial Jun 21, 2021
d963839
Gates per clifford is now computed in the analysis section
gadial Jun 21, 2021
080a5bc
gate_error_ratio is now an analysis option and instead of passing bac…
gadial Jun 21, 2021
95c05a6
Linting
gadial Jun 21, 2021
08b616d
Small fix
gadial Jun 20, 2021
1dc0a21
Small fixes
gadial Jun 23, 2021
6dd2e4c
Added count_ops test
gadial Jun 23, 2021
61187b7
Added calculate_1q_epg test
gadial Jun 23, 2021
5f2d8ef
Merge branch 'main' into rb_utils_class
gadial Jun 23, 2021
27b3a5e
Linting
gadial Jun 23, 2021
8d7e0ad
Linting
gadial Jun 23, 2021
a2ff6d0
Moving error_dict computation from experiment to analysis
gadial Jun 23, 2021
ec4bd25
Merge branch 'main' into rb_utils_class
gadial Jun 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions qiskit_experiments/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions qiskit_experiments/randomized_benchmarking/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 44 additions & 0 deletions qiskit_experiments/randomized_benchmarking/rb_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand All @@ -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
13 changes: 12 additions & 1 deletion qiskit_experiments/randomized_benchmarking/rb_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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.

Expand Down Expand Up @@ -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})
264 changes: 264 additions & 0 deletions qiskit_experiments/randomized_benchmarking/rb_utils.py
Original file line number Diff line number Diff line change
@@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this need to be a class? Looks like this is collection of functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might change it since it may be better to put those functions inside of the experiment/analysis classes. So far I'm trying to get the Ignis format to work before committing to more structural changes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean adding them to RBAnalysis as methods? I think this means all RB subclasses inherit those functionality, even though they don't use, i.e. IRB doesn't need to calculate EPG because we can directly estimate it.

"""A collection of utility functions for computing additional data
from randomized benchmarking experiments"""

@staticmethod
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps you want to decorate with @classmethod? Same for others. (i.e. there is no code creating an instance of RBUtil)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what @staticmethod does. In @classmethod the class itself is a parameter (for setting class variables etc.) and I don't need it here.

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you write physics behind this? This is quite convenient but I felt some difficulty of using this in journal parpers, because we don't provide any technical reference for this calculation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this code was copied from the ignis code written by Dave, so you should refer this question to him:
https://github.com/Qiskit/qiskit-ignis/blob/9201ed8f9970c50308601f4dfd895a1b8255e469/qiskit/ignis/verification/randomized_benchmarking/rb_utils.py#L169-#L224

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not originally my code, but I'll try.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I don't know the original paper of this calculation. Seems like some conventional code used by IBM experimentalists. One thing really close to this form is shown in Appendix G of this paper.

It would be great if you can derive the coherence limit, but this is not mandatory for this PR. Can you write an issue for future extension?

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
Loading