From 2fc4c8ac3744c52fae83e944b75602135722b696 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 17 Nov 2023 23:46:39 +0900 Subject: [PATCH 01/38] WIP: Add layer fidelity experiment --- .../randomized_benchmarking/clifford_utils.py | 11 + .../randomized_benchmarking/layer_fidelity.py | 347 ++++++++++++++++++ .../layer_fidelity_analysis.py | 202 ++++++++++ 3 files changed, 560 insertions(+) create mode 100644 qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py create mode 100644 qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py index a081db7006..5a19fceb97 100644 --- a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py +++ b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py @@ -730,3 +730,14 @@ def _layer_indices_from_num(num: Integral) -> Tuple[Integral, Integral, Integral idx1 = num % _NUM_LAYER_1 idx0 = num // _NUM_LAYER_1 return idx0, idx1, idx2 + + +@lru_cache(maxsize=24 * 24) +def _product_1q_nums(first: Integral, second: Integral) -> Integral: + """Return the 2-qubit Clifford integer that represents the product of 1-qubit Cliffords.""" + qc0 = CliffordUtils.clifford_1_qubit_circuit(first) + qc1 = CliffordUtils.clifford_1_qubit_circuit(second) + qc = QuantumCircuit(2) + qc.compose(qc0, qubits=(0,), inplace=True) + qc.compose(qc1, qubits=(1,), inplace=True) + return num_from_2q_circuit(qc) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py new file mode 100644 index 0000000000..f1ad7813a5 --- /dev/null +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -0,0 +1,347 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. +""" +Layer Fidelity RB Experiment class. +""" +import logging +from collections import defaultdict +from typing import Union, Iterable, Optional, List, Sequence, Tuple + +import numpy as np +from numpy.random import Generator, default_rng +from numpy.random.bit_generator import BitGenerator, SeedSequence + +from qiskit.circuit import QuantumCircuit, CircuitInstruction, Barrier +from qiskit.circuit.library import get_standard_gate_name_mapping +from qiskit.exceptions import QiskitError +from qiskit.providers import BackendV2Converter +from qiskit.providers.backend import Backend, BackendV1, BackendV2 +from qiskit.pulse.instruction_schedule_map import CalibrationPublisher + +from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework.restless_mixin import RestlessMixin + +from .clifford_utils import ( + CliffordUtils, + compose_1q, + compose_2q, + inverse_1q, + inverse_2q, + _product_1q_nums, + _num_from_2q_gate, + _clifford_1q_int_to_instruction, + _clifford_2q_int_to_instruction, + _decompose_clifford_ops, +) +from .layer_fidelity_analysis import LayerFidelityAnalysis + +LOG = logging.getLogger(__name__) + + +GATE_NAME_MAP = get_standard_gate_name_mapping() +NUM_1Q_CLIFFORD = CliffordUtils.NUM_CLIFFORD_1_QUBIT + + +class LayerFidelity(BaseExperiment, RestlessMixin): + """TODO + + # section: overview + + TODO + + # section: analysis_ref + :class:`LayerFidelityAnalysis` + + # section: reference + .. ref_arxiv:: 1 2311.05933 + """ + + def __init__( + self, + physical_qubits: Sequence[int], + two_qubit_layers: Sequence[Sequence[Tuple[int, int]]], + lengths: Iterable[int], + backend: Optional[Backend] = None, + num_samples: int = 3, + seed: Optional[Union[int, SeedSequence, BitGenerator, Generator]] = None, + # full_sampling: Optional[bool] = False, TODO: can we always do full_sampling and remove the option? + two_qubit_gate: Optional[str] = None, + one_qubit_basis_gates: Optional[Sequence[str]] = None, + ): + """Initialize a standard randomized benchmarking experiment. + + Args: + physical_qubits: List of physical qubits for the experiment. + two_qubit_layers: List of pairs of qubits to run on, will use the direction given here. + lengths: A list of RB sequences lengths. + backend: The backend to run the experiment on. + num_samples: Number of samples to generate for each sequence length. + seed: Optional, seed used to initialize ``numpy.random.default_rng``. + when generating circuits. The ``default_rng`` will be initialized + with this seed value every time :meth:`circuits` is called. + two_qubit_gate: Two-qubit gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. + one_qubit_basis_gates: One-qubit gates to use for implementing 1q Clifford operations. + + Raises: + QiskitError: If any invalid argument is supplied. + """ + # Compute full layers + full_layers = [] + for two_q_layer in two_qubit_layers: + qubits_in_layer = {q for qpair in two_q_layer for q in qpair} + layer = two_q_layer + [q for q in physical_qubits if q not in qubits_in_layer] + full_layers.append(layer) + + # Initialize base experiment + super().__init__(physical_qubits, analysis=LayerFidelityAnalysis(full_layers), backend=backend) + + # Verify parameters + # TODO more checks + if len(set(lengths)) != len(lengths): + raise QiskitError( + f"The lengths list {lengths} should not contain " "duplicate elements." + ) + if num_samples <= 0: + raise QiskitError(f"The number of samples {num_samples} should " "be positive.") + if two_qubit_gate not in GATE_NAME_MAP: + pass # TODO: too restrictive to forbidden custom two qubit gate name? + + # Get parameters from backend + if two_qubit_gate is None: + # TODO: implement and raise an error if backend is None + raise NotImplemented() + if one_qubit_basis_gates is None: + # TODO: implement and raise an error if backend is None + raise NotImplemented() + + # Set configurable options + self.set_experiment_options( + lengths=sorted(lengths), + num_samples=num_samples, + seed=seed, + two_qubit_layers=two_qubit_layers, + two_qubit_gate=two_qubit_gate, + one_qubit_basis_gates=tuple(one_qubit_basis_gates), + ) + # self.analysis.set_options(outcome="0" * self.num_qubits) + + @classmethod + def _default_experiment_options(cls) -> Options: + """Default experiment options. + + Experiment Options: + two_qubit_layers (List[List[Tuple[int, int]]]): List of pairs of qubits to run on. + lengths (List[int]): A list of RB sequences lengths. + num_samples (int): Number of samples to generate for each sequence length. + seed (None or int or SeedSequence or BitGenerator or Generator): A seed + used to initialize ``numpy.random.default_rng`` when generating circuits. + The ``default_rng`` will be initialized with this seed value every time + :meth:`circuits` is called. + two_qubit_gate (str): Two-qubit gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. + one_qubit_basis_gates (Tuple[str]): One-qubit gates to use for implementing 1q Clifford operations. + """ + options = super()._default_experiment_options() + options.update_options( + lengths=None, + num_samples=None, + seed=None, + two_qubit_layers=None, + two_qubit_gate=None, + one_qubit_basis_gates=tuple(), + ) + return options + + def set_experiment_options(self, **fields): + """Set the experiment options. + + Args: + fields: The fields to update the options + + Raises: + AttributeError: If the field passed in is not a supported options + """ + for field in {"two_qubit_layers"}: + if hasattr(self._experiment_options, field) and self._experiment_options[field] is not None: + raise AttributeError( + f"Options field {field} is not allowed to update." + ) + super().set_experiment_options(**fields) + + @classmethod + def _default_transpile_options(cls) -> Options: + """Default transpiler options for transpiling RB circuits.""" + return Options(optimization_level=1) + + def set_transpile_options(self, **fields): + """Transpile options is not supported for LayerFidelity experiments. + + Raises: + QiskitError: If `set_transpile_options` is called. + """ + raise QiskitError( + "Custom transpile options is not supported for LayerFidelity experiments." + ) + + def _set_backend(self, backend: Backend): + """Set the backend V2 for RB experiments since RB experiments only support BackendV2 + except for simulators. If BackendV1 is provided, it is converted to V2 and stored. + """ + if isinstance(backend, BackendV1) and "simulator" not in backend.name(): + super()._set_backend(BackendV2Converter(backend, add_delay=True)) + else: + super()._set_backend(backend) + + def __residual_qubits(self, two_qubit_layer): + qubits_in_layer = {q for qpair in two_qubit_layer for q in qpair} + return [q for q in self.physical_qubits if q not in qubits_in_layer] + + def circuits(self) -> List[QuantumCircuit]: + """Return a list of physical circuits to measure layer fidelity. + + Returns: + A list of :class:`QuantumCircuit`. + """ + opts = self.experiment_options + rng = default_rng(seed=opts.seed) + basis_gates = (opts.two_qubit_gate,) + opts.one_qubit_basis_gates + GATE2Q = GATE_NAME_MAP[opts.two_qubit_gate] + GATE2Q_CLIFF = _num_from_2q_gate(GATE2Q) + residal_qubits_by_layer = [self.__residual_qubits(layer) for layer in opts.two_qubit_layers] + # Circuit generation + circuits = [] + num_qubits = max(self.physical_qubits) + 1 + for i_sample in range(opts.num_samples): + for i_set, (two_qubit_layer, one_qubits) in enumerate(zip(opts.two_qubit_layers, residal_qubits_by_layer)): + num_2q_gates = len(two_qubit_layer) + num_1q_gates = len(one_qubits) + composite_qubits = two_qubit_layer + [(q,) for q in one_qubits] + composite_clbits = [(2*c, 2*c+1) for c in range(num_2q_gates)] + composite_clbits.extend([(c,) for c in range(2*num_2q_gates, 2*num_2q_gates+num_1q_gates)]) + for length in opts.lengths: + # initialize cliffords and a ciruit (0: identity clifford) + cliffs_2q = [0] * num_2q_gates + cliffs_1q = [0] * num_1q_gates + circ = QuantumCircuit(num_qubits) + for _ in range(length): + # sample random 1q-Clifford layer + for j, qpair in enumerate(two_qubit_layer): + # sample product of two 1q-Cliffords as 2q interger Clifford + samples = rng.integers(NUM_1Q_CLIFFORD, size=2) + cliffs_2q[j] = compose_2q(cliffs_2q[j], _product_1q_nums(*samples)) + for sample, q in zip(samples, qpair): + circ._append( + _clifford_1q_int_to_instruction( + sample, opts.one_qubit_basis_gates + ), + (circ.qubits[q],), + tuple(), + ) + for k, q in enumerate(one_qubits): + sample = rng.integers(NUM_1Q_CLIFFORD) + cliffs_1q[k] = compose_1q(cliffs_1q[k], sample) + circ._append( + _clifford_1q_int_to_instruction(sample, opts.one_qubit_basis_gates), + (circ.qubits[q],), + tuple(), + ) + circ.barrier(self.physical_qubits) + # add two qubit gates + for j, qpair in enumerate(two_qubit_layer): + circ._append(GATE2Q, tuple(circ.qubits[q] for q in qpair), tuple()) + cliffs_2q[j] = compose_2q(cliffs_2q[j], GATE2Q_CLIFF) + # TODO: add dd if necessary + for k, q in enumerate(one_qubits): + # TODO: add dd if necessary + pass + circ.barrier(self.physical_qubits) + # add the last inverse + for j, qpair in enumerate(two_qubit_layer): + inv = inverse_2q(cliffs_2q[j]) + circ._append( + _clifford_2q_int_to_instruction(inv, basis_gates), + tuple(circ.qubits[q] for q in qpair), + tuple(), + ) + for k, q in enumerate(one_qubits): + inv = inverse_1q(cliffs_1q[k]) + circ._append( + _clifford_1q_int_to_instruction(inv, opts.one_qubit_basis_gates), + (circ.qubits[q],), + tuple(), + ) + + circ.measure_active() # includes insertion of the barrier before measurement + # store composite structure in metadata + circ.metadata = { + 'experiment_type': 'BatchExperiment', 'composite_metadata': [ + { + 'experiment_type': 'ParallelExperiment', + 'composite_index': list(range(num_2q_gates + num_1q_gates)), + 'composite_metadata': [ + {'experiment_type': 'SubLayerFidelity', 'physical_qubits': qpair, 'sample': i_sample, 'xval': length} + for qpair in two_qubit_layer + ] + [ + {'experiment_type': 'SubLayerFidelity', 'physical_qubits': (q,), 'sample': i_sample, 'xval': length} + for q in one_qubits + ], + 'composite_qubits': composite_qubits, + 'composite_clbits': composite_clbits + } + ], + 'composite_index': [i_set] + } + circuits.append(circ) + + return circuits + + def _transpiled_circuits(self) -> List[QuantumCircuit]: + """Return a list of experiment circuits, transpiled.""" + transpiled = [_decompose_clifford_ops(circ) for circ in self.circuits()] + # Set custom calibrations provided in backend + if isinstance(self.backend, BackendV2): + instructions = [] # (op_name, qargs) for each element where qargs means qubit tuple + for two_qubit_layer in self.experiment_options.two_qubit_layers: + for qpair in two_qubit_layer: + instructions.append((self.experiment_options.two_qubit_gate, tuple(qpair))) + for q in self.__residual_qubits(two_qubit_layer): + for gate_1q in self.experiment_options.one_qubit_basis_gates: + instructions.append((gate_1q, (q,))) + + common_calibrations = defaultdict(dict) + for op_name, qargs in instructions: + inst_prop = self.backend.target[op_name].get(qargs, None) + if inst_prop is None: + continue + schedule = inst_prop.calibration + if schedule is None: + continue + publisher = schedule.metadata.get("publisher", CalibrationPublisher.QISKIT) + if publisher != CalibrationPublisher.BACKEND_PROVIDER: + common_calibrations[op_name][(qargs, tuple())] = schedule + + for circ in transpiled: + # This logic is inefficient in terms of payload size and backend compilation + # because this binds every custom pulse to a circuit regardless of + # its existence. It works but redundant calibration must be removed -- NK. + circ.calibrations = common_calibrations + + return transpiled + + def _metadata(self): + metadata = super()._metadata() + # Store measurement level and meas return if they have been + # set for the experiment + for run_opt in ["meas_level", "meas_return"]: + if hasattr(self.run_options, run_opt): + metadata[run_opt] = getattr(self.run_options, run_opt) + + return metadata diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py new file mode 100644 index 0000000000..fa2010582d --- /dev/null +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -0,0 +1,202 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. +""" +Layer Fidelity RB analysis class. +""" +from typing import List, Tuple, Union + +import lmfit + +import qiskit_experiments.curve_analysis as curve +from qiskit_experiments.exceptions import AnalysisError +from qiskit_experiments.framework import CompositeAnalysis, AnalysisResultData, ExperimentData + + +class _SubLayerFidelityAnalysis(curve.CurveAnalysis): + r"""A class to analyze a sub-experiment for estimating layer fidelity, + i.e. one of 1Q/2Q simultaneous direct RBs. + + # section: overview + This analysis takes only single series. + This series is fit by the exponential decay function. + From the fit :math:`\alpha` value this analysis estimates the error per gate (EPG). + + # section: fit_model + .. math:: + + F(x) = a \alpha^x + b + + # section: fit_parameters + defpar a: + desc: Height of decay curve. + init_guess: Determined by :math:`1 - b`. + bounds: [0, 1] + defpar b: + desc: Base line. + init_guess: Determined by :math:`(1/2)^n` where :math:`n` is number of qubit. + bounds: [0, 1] + defpar \alpha: + desc: Depolarizing parameter. + init_guess: Determined by :func:`~.guess.rb_decay`. + bounds: [0, 1] + + # section: reference + .. ref_arxiv:: 1 2311.05933 + """ + + def __init__(self, physical_qubits): + super().__init__( + models=[ + lmfit.models.ExpressionModel( + expr="a * alpha ** x + b", + name="rb_decay", + ) + ] + ) + self._physical_qubits = physical_qubits + + @classmethod + def _default_options(cls): + """Default analysis options. + """ + default_options = super()._default_options() + default_options.plotter.set_figure_options( + xlabel="Layer Length", + ylabel="P(0)", + ) + default_options.plot_raw_data = True + default_options.result_parameters = ["alpha"] + default_options.average_method = "sample" + + return default_options + + def _generate_fit_guesses( + self, + user_opt: curve.FitOptions, + curve_data: curve.ScatterTable, + ) -> Union[curve.FitOptions, List[curve.FitOptions]]: + """Create algorithmic initial fit guess from analysis options and curve data. + + Args: + user_opt: Fit options filled with user provided guess and bounds. + curve_data: Formatted data collection to fit. + + Returns: + List of fit options that are passed to the fitter function. + """ + user_opt.bounds.set_if_empty( + a=(0, 1), + alpha=(0, 1), + b=(0, 1), + ) + + b_guess = 1 / 2 ** len(self._physical_qubits) + alpha_guess = curve.guess.rb_decay(curve_data.x, curve_data.y, b=b_guess) + a_guess = (curve_data.y[0] - b_guess) / (alpha_guess ** curve_data.x[0]) + + user_opt.p0.set_if_empty( + b=b_guess, + a=a_guess, + alpha=alpha_guess, + ) + + return user_opt + + def _create_analysis_results( + self, + fit_data: curve.CurveFitResult, + quality: str, + **metadata, + ) -> List[AnalysisResultData]: + """Create analysis results for important fit parameters. + + Args: + fit_data: Fit outcome. + quality: Quality of fit outcome. + + Returns: + List of analysis result data. + """ + outcomes = super()._create_analysis_results(fit_data, quality, **metadata) + num_qubits = len(self._physical_qubits) + + # Calculate EPC + alpha = fit_data.ufloat_params["alpha"] + scale = (2**num_qubits - 1) / (2**num_qubits) + epg = scale * (1 - alpha) + + outcomes.append( + AnalysisResultData( + name="EPG", + value=epg, + chisq=fit_data.reduced_chisq, + quality=quality, + extra=metadata, + ) + ) + return outcomes + + + +class LayerFidelityAnalysis(CompositeAnalysis): + r"""A class to analyze layer fidelity experiments. + + # section: see_also + * :py:class:`qiskit_experiments.library.characterization.analysis.SubLayerFidelityAnalysis` + + # section: reference + .. ref_arxiv:: 1 2311.05933 + """ + + def __init__(self, layers, analyses=None): + if analyses: + # TODO: Validation + pass + else: + analyses = [] + for a_layer in layers: + a_layer_analyses = [_SubLayerFidelityAnalysis(qubits) for qubits in a_layer] + analyses.append(CompositeAnalysis(a_layer_analyses, flatten_results=True)) + + super().__init__(analyses, flatten_results=True) + + def _run_analysis( + self, experiment_data: ExperimentData + ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: + r"""Run analysis for Layer Fidelity experiment. + It invokes CompositeAnalysis._run_analysis that will invoke + _run_analysis for the sub-experiments (1Q/2Q simultaneous direct RBs). + Based on the results, it computes the result for Layer Fidelity. + """ + + # Run composite analysis and extract sub-experiments results + analysis_results, figures = super()._run_analysis(experiment_data) + + # Calculate Layer Fidelity from EPGs + lf = None # TODO + quality_lf = ( + "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" + ) + lf_result = AnalysisResultData( + name="LF", + value=lf, + chisq=None, + quality=quality_lf, + extra={}, + ) + + # TODO: Plot LF by chain length for a full 2q-gate chain + + # Return combined results + analysis_results = [lf_result] + analysis_results + # figures = [lf_plot] + figures + return analysis_results, figures From 7897561ac5450423a3720191b54c1313249dfeee Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 22 Nov 2023 13:12:04 +0900 Subject: [PATCH 02/38] fix critical bugs and add layer fidelity computation --- .../randomized_benchmarking/layer_fidelity.py | 2 +- .../layer_fidelity_analysis.py | 82 ++++++++++++++----- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index f1ad7813a5..b939be91eb 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -98,7 +98,7 @@ def __init__( full_layers = [] for two_q_layer in two_qubit_layers: qubits_in_layer = {q for qpair in two_q_layer for q in qpair} - layer = two_q_layer + [q for q in physical_qubits if q not in qubits_in_layer] + layer = two_q_layer + [(q, ) for q in physical_qubits if q not in qubits_in_layer] full_layers.append(layer) # Initialize base experiment diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index fa2010582d..73fc5541fd 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -15,15 +15,16 @@ from typing import List, Tuple, Union import lmfit +import numpy as np import qiskit_experiments.curve_analysis as curve +import qiskit_experiments.database_service.device_component as device from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.framework import CompositeAnalysis, AnalysisResultData, ExperimentData -class _SubLayerFidelityAnalysis(curve.CurveAnalysis): - r"""A class to analyze a sub-experiment for estimating layer fidelity, - i.e. one of 1Q/2Q simultaneous direct RBs. +class _ProcessFidelityAnalysis(curve.CurveAnalysis): + r"""A class to estimate process fidelity from one of 1Q/2Q simultaneous direct RB experiments # section: overview This analysis takes only single series. @@ -63,6 +64,7 @@ def __init__(self, physical_qubits): ] ) self._physical_qubits = physical_qubits + self.set_options(outcome="0" * len(physical_qubits)) @classmethod def _default_options(cls): @@ -129,15 +131,14 @@ def _create_analysis_results( outcomes = super()._create_analysis_results(fit_data, quality, **metadata) num_qubits = len(self._physical_qubits) - # Calculate EPC + # Calculate process fidelity alpha = fit_data.ufloat_params["alpha"] - scale = (2**num_qubits - 1) / (2**num_qubits) - epg = scale * (1 - alpha) + pf = (1 + (2**num_qubits - 1) * alpha) / (2**num_qubits) outcomes.append( AnalysisResultData( - name="EPG", - value=epg, + name="ProcessFidelity", + value=pf, chisq=fit_data.reduced_chisq, quality=quality, extra=metadata, @@ -145,14 +146,59 @@ def _create_analysis_results( ) return outcomes + def _get_experiment_components(self, experiment_data: ExperimentData): + """Set physical qubits to the experiment components.""" + return [device.Qubit(qubit) for qubit in self._physical_qubits] + + +class _SingleLayerFidelityAnalysis(CompositeAnalysis): + r"""A class to estimate a process fidelity per disjoint layer. + + # section: reference + .. ref_arxiv:: 1 2311.05933 + """ + + def __init__(self, layer, analyses=None): + if analyses: + # TODO: Validation + pass + else: + analyses = [_ProcessFidelityAnalysis(qubits) for qubits in layer] + + super().__init__(analyses, flatten_results=True) + + def _run_analysis( + self, experiment_data: ExperimentData + ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: + r"""TODO""" + + # Run composite analysis and extract sub-experiments results + analysis_results, figures = super()._run_analysis(experiment_data) + + # Calculate single layer fidelity from process fidelities of subsystems + pfs = [res.value for res in analysis_results if res.name == "ProcessFidelity"] + slf = np.prod(pfs) + quality_slf = ( + "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" + ) + slf_result = AnalysisResultData( + name="SingleLF", + value=slf, + chisq=None, + quality=quality_slf, + extra={}, + ) + + # TODO: Plot LF by chain length for a full 2q-gate chain + + # Return combined results + analysis_results = [slf_result] + analysis_results + return analysis_results, figures class LayerFidelityAnalysis(CompositeAnalysis): r"""A class to analyze layer fidelity experiments. - # section: see_also - * :py:class:`qiskit_experiments.library.characterization.analysis.SubLayerFidelityAnalysis` - # section: reference .. ref_arxiv:: 1 2311.05933 """ @@ -162,12 +208,10 @@ def __init__(self, layers, analyses=None): # TODO: Validation pass else: - analyses = [] - for a_layer in layers: - a_layer_analyses = [_SubLayerFidelityAnalysis(qubits) for qubits in a_layer] - analyses.append(CompositeAnalysis(a_layer_analyses, flatten_results=True)) + analyses = [_SingleLayerFidelityAnalysis(a_layer) for a_layer in layers] super().__init__(analyses, flatten_results=True) + self.num_layers = len(layers) def _run_analysis( self, experiment_data: ExperimentData @@ -181,8 +225,9 @@ def _run_analysis( # Run composite analysis and extract sub-experiments results analysis_results, figures = super()._run_analysis(experiment_data) - # Calculate Layer Fidelity from EPGs - lf = None # TODO + # Calculate full layer fidelity from single layer fidelities + slfs = [res.value for res in analysis_results if res.name == "SingleLF"] + lf = np.prod(slfs) quality_lf = ( "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" ) @@ -194,9 +239,6 @@ def _run_analysis( extra={}, ) - # TODO: Plot LF by chain length for a full 2q-gate chain - # Return combined results analysis_results = [lf_result] + analysis_results - # figures = [lf_plot] + figures return analysis_results, figures From 2029739db7d18929ec95f680a2375076448e427c Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 22 Nov 2023 13:21:12 +0900 Subject: [PATCH 03/38] fix style --- .../randomized_benchmarking/layer_fidelity.py | 59 ++++++++++++------- .../layer_fidelity_analysis.py | 15 ++--- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index b939be91eb..cb099a2d1e 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -98,11 +98,13 @@ def __init__( full_layers = [] for two_q_layer in two_qubit_layers: qubits_in_layer = {q for qpair in two_q_layer for q in qpair} - layer = two_q_layer + [(q, ) for q in physical_qubits if q not in qubits_in_layer] + layer = two_q_layer + [(q,) for q in physical_qubits if q not in qubits_in_layer] full_layers.append(layer) # Initialize base experiment - super().__init__(physical_qubits, analysis=LayerFidelityAnalysis(full_layers), backend=backend) + super().__init__( + physical_qubits, analysis=LayerFidelityAnalysis(full_layers), backend=backend + ) # Verify parameters # TODO more checks @@ -170,10 +172,11 @@ def set_experiment_options(self, **fields): AttributeError: If the field passed in is not a supported options """ for field in {"two_qubit_layers"}: - if hasattr(self._experiment_options, field) and self._experiment_options[field] is not None: - raise AttributeError( - f"Options field {field} is not allowed to update." - ) + if ( + hasattr(self._experiment_options, field) + and self._experiment_options[field] is not None + ): + raise AttributeError(f"Options field {field} is not allowed to update.") super().set_experiment_options(**fields) @classmethod @@ -220,12 +223,16 @@ def circuits(self) -> List[QuantumCircuit]: circuits = [] num_qubits = max(self.physical_qubits) + 1 for i_sample in range(opts.num_samples): - for i_set, (two_qubit_layer, one_qubits) in enumerate(zip(opts.two_qubit_layers, residal_qubits_by_layer)): + for i_set, (two_qubit_layer, one_qubits) in enumerate( + zip(opts.two_qubit_layers, residal_qubits_by_layer) + ): num_2q_gates = len(two_qubit_layer) num_1q_gates = len(one_qubits) composite_qubits = two_qubit_layer + [(q,) for q in one_qubits] - composite_clbits = [(2*c, 2*c+1) for c in range(num_2q_gates)] - composite_clbits.extend([(c,) for c in range(2*num_2q_gates, 2*num_2q_gates+num_1q_gates)]) + composite_clbits = [(2 * c, 2 * c + 1) for c in range(num_2q_gates)] + composite_clbits.extend( + [(c,) for c in range(2 * num_2q_gates, 2 * num_2q_gates + num_1q_gates)] + ) for length in opts.lengths: # initialize cliffords and a ciruit (0: identity clifford) cliffs_2q = [0] * num_2q_gates @@ -282,22 +289,34 @@ def circuits(self) -> List[QuantumCircuit]: circ.measure_active() # includes insertion of the barrier before measurement # store composite structure in metadata circ.metadata = { - 'experiment_type': 'BatchExperiment', 'composite_metadata': [ + "experiment_type": "BatchExperiment", + "composite_metadata": [ { - 'experiment_type': 'ParallelExperiment', - 'composite_index': list(range(num_2q_gates + num_1q_gates)), - 'composite_metadata': [ - {'experiment_type': 'SubLayerFidelity', 'physical_qubits': qpair, 'sample': i_sample, 'xval': length} + "experiment_type": "ParallelExperiment", + "composite_index": list(range(num_2q_gates + num_1q_gates)), + "composite_metadata": [ + { + "experiment_type": "SubLayerFidelity", + "physical_qubits": qpair, + "sample": i_sample, + "xval": length, + } for qpair in two_qubit_layer - ] + [ - {'experiment_type': 'SubLayerFidelity', 'physical_qubits': (q,), 'sample': i_sample, 'xval': length} + ] + + [ + { + "experiment_type": "SubLayerFidelity", + "physical_qubits": (q,), + "sample": i_sample, + "xval": length, + } for q in one_qubits - ], - 'composite_qubits': composite_qubits, - 'composite_clbits': composite_clbits + ], + "composite_qubits": composite_qubits, + "composite_clbits": composite_clbits, } ], - 'composite_index': [i_set] + "composite_index": [i_set], } circuits.append(circ) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 73fc5541fd..48432702e8 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """ -Layer Fidelity RB analysis class. +Analysis classes for Layer Fidelity RB. """ from typing import List, Tuple, Union @@ -29,7 +29,7 @@ class _ProcessFidelityAnalysis(curve.CurveAnalysis): # section: overview This analysis takes only single series. This series is fit by the exponential decay function. - From the fit :math:`\alpha` value this analysis estimates the error per gate (EPG). + From the fit :math:`\alpha` value this analysis estimates the process fidelity. # section: fit_model .. math:: @@ -68,8 +68,7 @@ def __init__(self, physical_qubits): @classmethod def _default_options(cls): - """Default analysis options. - """ + """Default analysis options.""" default_options = super()._default_options() default_options.plotter.set_figure_options( xlabel="Layer Length", @@ -178,9 +177,7 @@ def _run_analysis( # Calculate single layer fidelity from process fidelities of subsystems pfs = [res.value for res in analysis_results if res.name == "ProcessFidelity"] slf = np.prod(pfs) - quality_slf = ( - "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" - ) + quality_slf = "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" slf_result = AnalysisResultData( name="SingleLF", value=slf, @@ -228,9 +225,7 @@ def _run_analysis( # Calculate full layer fidelity from single layer fidelities slfs = [res.value for res in analysis_results if res.name == "SingleLF"] lf = np.prod(slfs) - quality_lf = ( - "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" - ) + quality_lf = "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" lf_result = AnalysisResultData( name="LF", value=lf, From 781b8ebdc6804d6aec1de8a805d628948376f712 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 22 Nov 2023 14:26:43 +0900 Subject: [PATCH 04/38] fix critical bug in measurements --- .../library/randomized_benchmarking/layer_fidelity.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index cb099a2d1e..1804d052d5 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -237,7 +237,7 @@ def circuits(self) -> List[QuantumCircuit]: # initialize cliffords and a ciruit (0: identity clifford) cliffs_2q = [0] * num_2q_gates cliffs_1q = [0] * num_1q_gates - circ = QuantumCircuit(num_qubits) + circ = QuantumCircuit(num_qubits, num_qubits) for _ in range(length): # sample random 1q-Clifford layer for j, qpair in enumerate(two_qubit_layer): @@ -285,8 +285,10 @@ def circuits(self) -> List[QuantumCircuit]: (circ.qubits[q],), tuple(), ) - - circ.measure_active() # includes insertion of the barrier before measurement + # add the measurements + circ.barrier(self.physical_qubits) + for qubits, clbits in zip(composite_qubits, composite_clbits): + circ.measure(qubits, clbits) # store composite structure in metadata circ.metadata = { "experiment_type": "BatchExperiment", From 1b46de52a279578c6474f6aee10126f595f3f7eb Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 22 Nov 2023 17:15:31 +0900 Subject: [PATCH 05/38] add test --- .../randomized_benchmarking/__init__.py | 2 + .../test_layer_fidelity.py | 221 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 test/library/randomized_benchmarking/test_layer_fidelity.py diff --git a/qiskit_experiments/library/randomized_benchmarking/__init__.py b/qiskit_experiments/library/randomized_benchmarking/__init__.py index 371f0a3679..ddf1a8289f 100644 --- a/qiskit_experiments/library/randomized_benchmarking/__init__.py +++ b/qiskit_experiments/library/randomized_benchmarking/__init__.py @@ -61,3 +61,5 @@ from .clifford_utils import CliffordUtils from .rb_utils import RBUtils from .clifford_synthesis import RBDefaultCliffordSynthesis +from .layer_fidelity import LayerFidelity +from .layer_fidelity_analysis import LayerFidelityAnalysis diff --git a/test/library/randomized_benchmarking/test_layer_fidelity.py b/test/library/randomized_benchmarking/test_layer_fidelity.py new file mode 100644 index 0000000000..669b4713f9 --- /dev/null +++ b/test/library/randomized_benchmarking/test_layer_fidelity.py @@ -0,0 +1,221 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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. + +"""Test for layer fidelity experiments.""" +import copy +import numpy as np + +from test.base import QiskitExperimentsTestCase +from test.library.randomized_benchmarking.mixin import RBTestMixin +from ddt import ddt, data, unpack + +from qiskit.circuit.library import SXGate +from qiskit.exceptions import QiskitError +from qiskit.providers.fake_provider import FakeManilaV2 +from qiskit.pulse import Schedule +from qiskit_experiments.library.randomized_benchmarking import LayerFidelity, LayerFidelityAnalysis + + +@ddt +class TestLayerFidelity(QiskitExperimentsTestCase, RBTestMixin): + """Test for LayerFidelity without running the experiments.""" + + # ### Tests for configuration ### + def test_experiment_config(self): + """Test converting to and from config works""" + exp = LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], + lengths=[10, 20, 30], + seed=42, + two_qubit_gate="cx", + one_qubit_basis_gates=["rz", "sx", "x"], + ) + loaded_exp = LayerFidelity.from_config(exp.config()) + self.assertNotEqual(exp, loaded_exp) + self.assertEqualExtended(exp, loaded_exp) + + # def test_invalid_two_qubit_layers(self): + # """Test raise error when creating experiment with invalid configs.""" + # valid_kwargs = { + # "lengths": [10, 20, 30], + # "two_qubit_gate": "cx", + # "one_qubit_basis_gates": ["rz", "sx", "x"], + # } + # # not disjoit + # with self.assertRaises(QiskitError): + # LayerFidelity( + # physical_qubits=(0, 1, 2, 3), + # two_qubit_layers=[[(0, 1), (1, 2)]], + # **valid_kwargs + # ) + # # no 2q-gate on the qubits (FakeManilaV2 has no cx gate on (0, 3)) + # with self.assertRaises(QiskitError): + # LayerFidelity( + # physical_qubits=(0, 1, 2, 3), + # two_qubit_layers=[[(0, 3)]], + # backend=FakeManilaV2(), + # **valid_kwargs + # ) + + def test_roundtrip_serializable(self): + """Test round trip JSON serialization""" + exp = LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], + lengths=[10, 20, 30], + seed=42, + two_qubit_gate="cx", + one_qubit_basis_gates=["rz", "sx", "x"], + ) + self.assertRoundTripSerializable(exp, strict_type=False) + + def test_circuit_roundtrip_serializable(self): + """Test circuits round trip JSON serialization""" + exp = LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], + lengths=[10, 20, 30], + seed=42, + two_qubit_gate="cx", + one_qubit_basis_gates=["rz", "sx", "x"], + ) + self.assertRoundTripSerializable(exp._transpiled_circuits()) + + def test_analysis_config(self): + """ "Test converting analysis to and from config works""" + analysis = LayerFidelityAnalysis(layers=[[(1, 0), (2, 3)], [(1, 2), (0,), (3,)]]) + loaded = LayerFidelityAnalysis.from_config(analysis.config()) + self.assertNotEqual(analysis, loaded) + self.assertEqual(analysis.config(), loaded.config()) + + # ### Tests for circuit generation ### + @data( + [(1, 2), [[(1, 2)]]], + [(1, 3, 4), [[(3, 4)]]], + [(4, 3, 2, 1, 0), [[(0, 1), (3, 2)], [(1, 2), (3, 4)]]], + ) + @unpack + def test_generate_circuits(self, qubits, two_qubit_layers): + """Test RB circuit generation""" + exp = LayerFidelity( + physical_qubits=qubits, + two_qubit_layers=two_qubit_layers, + lengths=[1, 2, 3], + seed=42, + two_qubit_gate="cx", + one_qubit_basis_gates=["rz", "sx", "x"], + ) + circuits = exp.circuits() + self.assertAllIdentity(circuits) + + def test_return_same_circuit_for_same_config(self): + """Test if setting the same seed returns the same circuits.""" + exp1 = LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], + lengths=[10, 20, 30], + seed=42, + two_qubit_gate="cx", + one_qubit_basis_gates=["rz", "sx", "x"], + ) + + exp2 = LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], + lengths=[10, 20, 30], + seed=42, + two_qubit_gate="cx", + one_qubit_basis_gates=["rz", "sx", "x"], + ) + + 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()) + + # ### Tests for transpiled circuit generation ### + def test_calibrations_via_custom_backend(self): + """Test if calibrations given as custom backend show up in transpiled circuits.""" + qubits = (2,) + my_sched = Schedule(name="custom_sx_gate") + my_backend = copy.deepcopy(FakeManilaV2()) + my_backend.target["sx"][qubits].calibration = my_sched + + exp = LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], + lengths=[10, 20, 30], + seed=42, + backend=my_backend, + ) + transpiled = exp._transpiled_circuits() + for qc in transpiled: + self.assertTrue(qc.calibrations) + self.assertTrue(qc.has_calibration_for((SXGate(), [qc.qubits[q] for q in qubits], []))) + self.assertEqual(qc.calibrations["sx"][(qubits, tuple())], my_sched) + + def test_backend_with_directed_basis_gates(self): + """Test if correct circuits are generated from backend with directed basis gates.""" + my_backend = copy.deepcopy(FakeManilaV2()) + del my_backend.target["cx"][(1, 2)] # make cx on {1, 2} one-sided + + exp = LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], + lengths=[10, 20, 30], + seed=42, + backend=my_backend, + ) + transpiled = exp._transpiled_circuits() + for qc in transpiled: + self.assertTrue(qc.count_ops().get("cx", 0) > 0) + expected_qubits = (qc.qubits[2], qc.qubits[1]) + for inst in qc: + if inst.operation.name == "cx": + self.assertEqual(inst.qubits, expected_qubits) + + +class TestRunLayerFidelity(QiskitExperimentsTestCase, RBTestMixin): + """Test for running LayerFidelity on noisy simulator.""" + + def test_three_qubit(self): + """Test two qubit RB. Use default basis gates.""" + exp = LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], + lengths=[1, 4, 16, 64, 256], + seed=42, + backend=FakeManilaV2(), + ) + expdata = exp.run() + self.assertExperimentDone(expdata) + + lf = expdata.analysis_results("LF").value.n + slfs = [res.value.n for res in expdata.analysis_results("SingleLF")] + self.assertAlmostEqual(lf, np.prod(slfs)) + + def test_expdata_serialization(self): + """Test serializing experiment data works.""" + exp = LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], + lengths=[1, 4, 16, 64, 256], + seed=42, + backend=FakeManilaV2(), + ) + expdata = exp.run() + self.assertExperimentDone(expdata) + self.assertRoundTripSerializable(expdata) + self.assertRoundTripPickle(expdata) From 5c6f927e37b9d37ab29b6cdfe36d2f56b677c4ab Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Tue, 28 Nov 2023 21:15:03 +0900 Subject: [PATCH 06/38] compute EPLG --- .../layer_fidelity_analysis.py | 15 ++++-- .../test_layer_fidelity.py | 48 +++++++++---------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 48432702e8..9e9bab5104 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -186,8 +186,6 @@ def _run_analysis( extra={}, ) - # TODO: Plot LF by chain length for a full 2q-gate chain - # Return combined results analysis_results = [slf_result] + analysis_results return analysis_results, figures @@ -209,6 +207,7 @@ def __init__(self, layers, analyses=None): super().__init__(analyses, flatten_results=True) self.num_layers = len(layers) + self.num_2q_gates = sum(1 if len(qs) == 2 else 0 for lay in layers for qs in lay) def _run_analysis( self, experiment_data: ExperimentData @@ -233,7 +232,15 @@ def _run_analysis( quality=quality_lf, extra={}, ) - + eplg = 1 - (lf ** (1/self.num_2q_gates)) + eplg_result = AnalysisResultData( + name="EPLG", + value=eplg, + chisq=None, + quality=quality_lf, + extra={}, + ) + # Return combined results - analysis_results = [lf_result] + analysis_results + analysis_results = [lf_result, eplg_result] + analysis_results return analysis_results, figures diff --git a/test/library/randomized_benchmarking/test_layer_fidelity.py b/test/library/randomized_benchmarking/test_layer_fidelity.py index 669b4713f9..a461318f3d 100644 --- a/test/library/randomized_benchmarking/test_layer_fidelity.py +++ b/test/library/randomized_benchmarking/test_layer_fidelity.py @@ -44,28 +44,28 @@ def test_experiment_config(self): self.assertNotEqual(exp, loaded_exp) self.assertEqualExtended(exp, loaded_exp) - # def test_invalid_two_qubit_layers(self): - # """Test raise error when creating experiment with invalid configs.""" - # valid_kwargs = { - # "lengths": [10, 20, 30], - # "two_qubit_gate": "cx", - # "one_qubit_basis_gates": ["rz", "sx", "x"], - # } - # # not disjoit - # with self.assertRaises(QiskitError): - # LayerFidelity( - # physical_qubits=(0, 1, 2, 3), - # two_qubit_layers=[[(0, 1), (1, 2)]], - # **valid_kwargs - # ) - # # no 2q-gate on the qubits (FakeManilaV2 has no cx gate on (0, 3)) - # with self.assertRaises(QiskitError): - # LayerFidelity( - # physical_qubits=(0, 1, 2, 3), - # two_qubit_layers=[[(0, 3)]], - # backend=FakeManilaV2(), - # **valid_kwargs - # ) + def test_invalid_two_qubit_layers(self): + """Test raise error when creating experiment with invalid configs.""" + valid_kwargs = { + "lengths": [10, 20, 30], + "two_qubit_gate": "cx", + "one_qubit_basis_gates": ["rz", "sx", "x"], + } + # not disjoit + with self.assertRaises(QiskitError): + LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(0, 1), (1, 2)]], + **valid_kwargs + ) + # no 2q-gate on the qubits (FakeManilaV2 has no cx gate on (0, 3)) + with self.assertRaises(QiskitError): + LayerFidelity( + physical_qubits=(0, 1, 2, 3), + two_qubit_layers=[[(0, 3)]], + backend=FakeManilaV2(), + **valid_kwargs + ) def test_roundtrip_serializable(self): """Test round trip JSON serialization""" @@ -190,8 +190,8 @@ def test_backend_with_directed_basis_gates(self): class TestRunLayerFidelity(QiskitExperimentsTestCase, RBTestMixin): """Test for running LayerFidelity on noisy simulator.""" - def test_three_qubit(self): - """Test two qubit RB. Use default basis gates.""" + def test_run_layer_fidelity(self): + """Test layer fidelity RB. Use default basis gates.""" exp = LayerFidelity( physical_qubits=(0, 1, 2, 3), two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], From a6c03264a477442aaa9e97689117ba7294896ea2 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 29 Nov 2023 23:54:39 +0900 Subject: [PATCH 07/38] Fixes for backends with ECRGate employing #1288 --- .../randomized_benchmarking/clifford_utils.py | 16 ++++-- .../randomized_benchmarking/layer_fidelity.py | 52 +++++++++---------- .../layer_fidelity_analysis.py | 4 +- .../test_layer_fidelity.py | 6 +-- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py index 5a19fceb97..8dae210f42 100644 --- a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py +++ b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py @@ -486,7 +486,11 @@ def inverse_1q(num: Integral) -> Integral: def num_from_1q_circuit(qc: QuantumCircuit) -> Integral: - """Convert a given 1-qubit Clifford circuit to the corresponding integer.""" + """Convert a given 1-qubit Clifford circuit to the corresponding integer. + + Note: The circuit must consist of gates in :const:`_CLIFF_SINGLE_GATE_MAP_1Q`, + RZGate, Delay and Barrier. + """ num = 0 for inst in qc: rhs = _num_from_1q_gate(op=inst.operation) @@ -497,7 +501,7 @@ def num_from_1q_circuit(qc: QuantumCircuit) -> Integral: def _num_from_1q_gate(op: Instruction) -> int: """ Convert a given 1-qubit clifford operation to the corresponding integer. - Note that supported operations are limited to ones in :const:`CLIFF_SINGLE_GATE_MAP_1Q` or Rz gate. + Note that supported operations are limited to ones in :const:`_CLIFF_SINGLE_GATE_MAP_1Q` or Rz gate. Args: op: operation to be converted. @@ -556,7 +560,11 @@ def inverse_2q(num: Integral) -> Integral: def num_from_2q_circuit(qc: QuantumCircuit) -> Integral: - """Convert a given 2-qubit Clifford circuit to the corresponding integer.""" + """Convert a given 2-qubit Clifford circuit to the corresponding integer. + + Note: The circuit must consist of gates in :const:`_CLIFF_SINGLE_GATE_MAP_2Q`, + RZGate, Delay and Barrier. + """ lhs = 0 for rhs in _clifford_2q_nums_from_2q_circuit(qc): lhs = _CLIFFORD_COMPOSE_2Q_DENSE[lhs, _clifford_num_to_dense_index[rhs]] @@ -568,7 +576,7 @@ def _num_from_2q_gate( ) -> int: """ Convert a given 1-qubit clifford operation to the corresponding integer. - Note that supported operations are limited to ones in `CLIFF_SINGLE_GATE_MAP_2Q` or Rz gate. + Note that supported operations are limited to ones in `_CLIFF_SINGLE_GATE_MAP_2Q` or Rz gate. Args: op: operation of instruction to be converted. diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 1804d052d5..9e44eb3164 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -12,6 +12,7 @@ """ Layer Fidelity RB Experiment class. """ +import functools import logging from collections import defaultdict from typing import Union, Iterable, Optional, List, Sequence, Tuple @@ -25,6 +26,7 @@ from qiskit.exceptions import QiskitError from qiskit.providers import BackendV2Converter from qiskit.providers.backend import Backend, BackendV1, BackendV2 +from qiskit.quantum_info import Clifford from qiskit.pulse.instruction_schedule_map import CalibrationPublisher from qiskit_experiments.framework import BaseExperiment, Options @@ -32,12 +34,13 @@ from .clifford_utils import ( CliffordUtils, + DEFAULT_SYNTHESIS_METHOD, compose_1q, compose_2q, inverse_1q, inverse_2q, + num_from_2q_circuit, _product_1q_nums, - _num_from_2q_gate, _clifford_1q_int_to_instruction, _clifford_2q_int_to_instruction, _decompose_clifford_ops, @@ -150,6 +153,8 @@ def _default_experiment_options(cls) -> Options: :meth:`circuits` is called. two_qubit_gate (str): Two-qubit gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. one_qubit_basis_gates (Tuple[str]): One-qubit gates to use for implementing 1q Clifford operations. + clifford_synthesis_method (str): The name of the Clifford synthesis plugin to use + for building circuits of RB sequences. """ options = super()._default_experiment_options() options.update_options( @@ -158,7 +163,8 @@ def _default_experiment_options(cls) -> Options: seed=None, two_qubit_layers=None, two_qubit_gate=None, - one_qubit_basis_gates=tuple(), + one_qubit_basis_gates=(), + clifford_synthesis_method=DEFAULT_SYNTHESIS_METHOD, ) return options @@ -215,10 +221,20 @@ def circuits(self) -> List[QuantumCircuit]: """ opts = self.experiment_options rng = default_rng(seed=opts.seed) - basis_gates = (opts.two_qubit_gate,) + opts.one_qubit_basis_gates GATE2Q = GATE_NAME_MAP[opts.two_qubit_gate] - GATE2Q_CLIFF = _num_from_2q_gate(GATE2Q) + GATE2Q_CLIFF = num_from_2q_circuit(Clifford(GATE2Q).to_circuit()) residal_qubits_by_layer = [self.__residual_qubits(layer) for layer in opts.two_qubit_layers] + _to_gate_1q = functools.partial( + _clifford_1q_int_to_instruction, + basis_gates=opts.one_qubit_basis_gates, + synthesis_method=opts.clifford_synthesis_method, + ) + _to_gate_2q = functools.partial( + _clifford_2q_int_to_instruction, + basis_gates=(opts.two_qubit_gate,) + opts.one_qubit_basis_gates, + coupling_tuple=((0, 1),), + synthesis_method=opts.clifford_synthesis_method, + ) # Circuit generation circuits = [] num_qubits = max(self.physical_qubits) + 1 @@ -245,25 +261,15 @@ def circuits(self) -> List[QuantumCircuit]: samples = rng.integers(NUM_1Q_CLIFFORD, size=2) cliffs_2q[j] = compose_2q(cliffs_2q[j], _product_1q_nums(*samples)) for sample, q in zip(samples, qpair): - circ._append( - _clifford_1q_int_to_instruction( - sample, opts.one_qubit_basis_gates - ), - (circ.qubits[q],), - tuple(), - ) + circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) for k, q in enumerate(one_qubits): sample = rng.integers(NUM_1Q_CLIFFORD) cliffs_1q[k] = compose_1q(cliffs_1q[k], sample) - circ._append( - _clifford_1q_int_to_instruction(sample, opts.one_qubit_basis_gates), - (circ.qubits[q],), - tuple(), - ) + circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) circ.barrier(self.physical_qubits) # add two qubit gates for j, qpair in enumerate(two_qubit_layer): - circ._append(GATE2Q, tuple(circ.qubits[q] for q in qpair), tuple()) + circ._append(GATE2Q, tuple(circ.qubits[q] for q in qpair), ()) cliffs_2q[j] = compose_2q(cliffs_2q[j], GATE2Q_CLIFF) # TODO: add dd if necessary for k, q in enumerate(one_qubits): @@ -273,18 +279,10 @@ def circuits(self) -> List[QuantumCircuit]: # add the last inverse for j, qpair in enumerate(two_qubit_layer): inv = inverse_2q(cliffs_2q[j]) - circ._append( - _clifford_2q_int_to_instruction(inv, basis_gates), - tuple(circ.qubits[q] for q in qpair), - tuple(), - ) + circ._append(_to_gate_2q(inv), tuple(circ.qubits[q] for q in qpair), ()) for k, q in enumerate(one_qubits): inv = inverse_1q(cliffs_1q[k]) - circ._append( - _clifford_1q_int_to_instruction(inv, opts.one_qubit_basis_gates), - (circ.qubits[q],), - tuple(), - ) + circ._append(_to_gate_1q(inv), (circ.qubits[q],), ()) # add the measurements circ.barrier(self.physical_qubits) for qubits, clbits in zip(composite_qubits, composite_clbits): diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 9e9bab5104..577f951c11 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -232,7 +232,7 @@ def _run_analysis( quality=quality_lf, extra={}, ) - eplg = 1 - (lf ** (1/self.num_2q_gates)) + eplg = 1 - (lf ** (1 / self.num_2q_gates)) eplg_result = AnalysisResultData( name="EPLG", value=eplg, @@ -240,7 +240,7 @@ def _run_analysis( quality=quality_lf, extra={}, ) - + # Return combined results analysis_results = [lf_result, eplg_result] + analysis_results return analysis_results, figures diff --git a/test/library/randomized_benchmarking/test_layer_fidelity.py b/test/library/randomized_benchmarking/test_layer_fidelity.py index a461318f3d..2449c49842 100644 --- a/test/library/randomized_benchmarking/test_layer_fidelity.py +++ b/test/library/randomized_benchmarking/test_layer_fidelity.py @@ -54,9 +54,7 @@ def test_invalid_two_qubit_layers(self): # not disjoit with self.assertRaises(QiskitError): LayerFidelity( - physical_qubits=(0, 1, 2, 3), - two_qubit_layers=[[(0, 1), (1, 2)]], - **valid_kwargs + physical_qubits=(0, 1, 2, 3), two_qubit_layers=[[(0, 1), (1, 2)]], **valid_kwargs ) # no 2q-gate on the qubits (FakeManilaV2 has no cx gate on (0, 3)) with self.assertRaises(QiskitError): @@ -64,7 +62,7 @@ def test_invalid_two_qubit_layers(self): physical_qubits=(0, 1, 2, 3), two_qubit_layers=[[(0, 3)]], backend=FakeManilaV2(), - **valid_kwargs + **valid_kwargs, ) def test_roundtrip_serializable(self): From 53dc8ca50fd9f1879653186420eb19b24390a75e Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Thu, 30 Nov 2023 23:48:52 +0900 Subject: [PATCH 08/38] Add figure title --- .../randomized_benchmarking/layer_fidelity_analysis.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 577f951c11..a7306cbde1 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -65,14 +65,17 @@ def __init__(self, physical_qubits): ) self._physical_qubits = physical_qubits self.set_options(outcome="0" * len(physical_qubits)) + self.plotter.set_figure_options( + figure_title=f"Simultaneous Direct RB on Qubit{physical_qubits}", + ) @classmethod def _default_options(cls): """Default analysis options.""" default_options = super()._default_options() default_options.plotter.set_figure_options( - xlabel="Layer Length", - ylabel="P(0)", + xlabel="Layers", + ylabel="Ground State Population", ) default_options.plot_raw_data = True default_options.result_parameters = ["alpha"] From eebed5b8e38399ddb219c5f6b87f78164bc569ec Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 1 Dec 2023 00:24:10 +0900 Subject: [PATCH 09/38] Add qubits column to analysis results table --- .../randomized_benchmarking/layer_fidelity_analysis.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index a7306cbde1..0d0c8b4b7c 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -137,6 +137,7 @@ def _create_analysis_results( alpha = fit_data.ufloat_params["alpha"] pf = (1 + (2**num_qubits - 1) * alpha) / (2**num_qubits) + metadata["qubits"] = self._physical_qubits outcomes.append( AnalysisResultData( name="ProcessFidelity", @@ -168,6 +169,7 @@ def __init__(self, layer, analyses=None): analyses = [_ProcessFidelityAnalysis(qubits) for qubits in layer] super().__init__(analyses, flatten_results=True) + self._layer = layer def _run_analysis( self, experiment_data: ExperimentData @@ -193,6 +195,10 @@ def _run_analysis( analysis_results = [slf_result] + analysis_results return analysis_results, figures + def _get_experiment_components(self, experiment_data: ExperimentData): + """Set physical qubits to the experiment components.""" + return [device.Qubit(q) for qubits in self._layer for q in qubits] + class LayerFidelityAnalysis(CompositeAnalysis): r"""A class to analyze layer fidelity experiments. From 706f17ed707a4788e3ee81eed28820ec8b48b279 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 1 Dec 2023 18:45:37 +0900 Subject: [PATCH 10/38] Change to return results with quality=='failed' so that the lack of sub-results does not cause incorrect parent analysis --- .../randomized_benchmarking/layer_fidelity.py | 1 + .../layer_fidelity_analysis.py | 125 +++++++++++------- 2 files changed, 80 insertions(+), 46 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 9e44eb3164..f8ab601e69 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -363,4 +363,5 @@ def _metadata(self): if hasattr(self.run_options, run_opt): metadata[run_opt] = getattr(self.run_options, run_opt) + metadata["two_qubit_layers"] = self.experiment_options["two_qubit_layers"] return metadata diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 0d0c8b4b7c..42faca439b 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -149,6 +149,24 @@ def _create_analysis_results( ) return outcomes + def _run_analysis( + self, experiment_data: ExperimentData + ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: + r"""TODO + + Note: Empty analysis results will be returned when failing analysis. + """ + try: + return super()._run_analysis(experiment_data) + except Exception: + failed_result = AnalysisResultData( + name="ProcessFidelity", + value=None, + quality="failed", + extra={"qubits": self._physical_qubits}, + ) + return [failed_result], [] + def _get_experiment_components(self, experiment_data: ExperimentData): """Set physical qubits to the experiment components.""" return [device.Qubit(qubit) for qubit in self._physical_qubits] @@ -157,6 +175,8 @@ def _get_experiment_components(self, experiment_data: ExperimentData): class _SingleLayerFidelityAnalysis(CompositeAnalysis): r"""A class to estimate a process fidelity per disjoint layer. + Note: Empty analysis results will be returned when failing analysis. + # section: reference .. ref_arxiv:: 1 2311.05933 """ @@ -175,25 +195,30 @@ def _run_analysis( self, experiment_data: ExperimentData ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: r"""TODO""" - - # Run composite analysis and extract sub-experiments results - analysis_results, figures = super()._run_analysis(experiment_data) - - # Calculate single layer fidelity from process fidelities of subsystems - pfs = [res.value for res in analysis_results if res.name == "ProcessFidelity"] - slf = np.prod(pfs) - quality_slf = "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" - slf_result = AnalysisResultData( - name="SingleLF", - value=slf, - chisq=None, - quality=quality_slf, - extra={}, - ) - - # Return combined results - analysis_results = [slf_result] + analysis_results - return analysis_results, figures + try: + # Run composite analysis and extract sub-experiments results + analysis_results, figures = super()._run_analysis(experiment_data) + # Calculate single layer fidelity from process fidelities of subsystems + pfs = [res.value for res in analysis_results if res.name == "ProcessFidelity"] + slf = np.prod(pfs) + quality_slf = "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" + slf_result = AnalysisResultData( + name="SingleLF", + value=slf, + quality=quality_slf, + extra={"qubits": [q for qubits in self._layer for q in qubits]}, + ) + # Return combined results + analysis_results = [slf_result] + analysis_results + return analysis_results, figures + except Exception: + failed_result = AnalysisResultData( + name="SingleLF", + value=None, + quality="failed", + extra={"qubits": [q for qubits in self._layer for q in qubits]}, + ) + return [failed_result] + analysis_results, figures def _get_experiment_components(self, experiment_data: ExperimentData): """Set physical qubits to the experiment components.""" @@ -226,30 +251,38 @@ def _run_analysis( _run_analysis for the sub-experiments (1Q/2Q simultaneous direct RBs). Based on the results, it computes the result for Layer Fidelity. """ - - # Run composite analysis and extract sub-experiments results - analysis_results, figures = super()._run_analysis(experiment_data) - - # Calculate full layer fidelity from single layer fidelities - slfs = [res.value for res in analysis_results if res.name == "SingleLF"] - lf = np.prod(slfs) - quality_lf = "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" - lf_result = AnalysisResultData( - name="LF", - value=lf, - chisq=None, - quality=quality_lf, - extra={}, - ) - eplg = 1 - (lf ** (1 / self.num_2q_gates)) - eplg_result = AnalysisResultData( - name="EPLG", - value=eplg, - chisq=None, - quality=quality_lf, - extra={}, - ) - - # Return combined results - analysis_results = [lf_result, eplg_result] + analysis_results - return analysis_results, figures + try: + # Run composite analysis and extract sub-experiments results + analysis_results, figures = super()._run_analysis(experiment_data) + # Calculate full layer fidelity from single layer fidelities + slfs = [res.value for res in analysis_results if res.name == "SingleLF"] + lf = np.prod(slfs) + quality_lf = "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" + lf_result = AnalysisResultData( + name="LF", + value=lf, + quality=quality_lf, + ) + eplg = 1 - (lf ** (1 / self.num_2q_gates)) + eplg_result = AnalysisResultData( + name="EPLG", + value=eplg, + quality=quality_lf, + ) + # Return combined results + analysis_results = [lf_result, eplg_result] + analysis_results + return analysis_results, figures + except Exception: + failed_results = [ + AnalysisResultData( + name="LF", + value=None, + quality="failed", + ), + AnalysisResultData( + name="EPLG", + value=None, + quality="failed", + ) + ] + return failed_results + analysis_results, figures From f538f155d98aab9df2344bf04d6764eb86e9e5e1 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Sat, 2 Dec 2023 00:13:19 +0900 Subject: [PATCH 11/38] add logging --- .../library/randomized_benchmarking/layer_fidelity.py | 8 +------- .../randomized_benchmarking/layer_fidelity_analysis.py | 7 +++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index f8ab601e69..7cd88c04b8 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -357,11 +357,5 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]: def _metadata(self): metadata = super()._metadata() - # Store measurement level and meas return if they have been - # set for the experiment - for run_opt in ["meas_level", "meas_return"]: - if hasattr(self.run_options, run_opt): - metadata[run_opt] = getattr(self.run_options, run_opt) - - metadata["two_qubit_layers"] = self.experiment_options["two_qubit_layers"] + metadata["two_qubit_layers"] = self.experiment_options.two_qubit_layers return metadata diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 42faca439b..ed19b5685f 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -15,6 +15,8 @@ from typing import List, Tuple, Union import lmfit +import logging +import traceback import numpy as np import qiskit_experiments.curve_analysis as curve @@ -22,6 +24,8 @@ from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.framework import CompositeAnalysis, AnalysisResultData, ExperimentData +LOG = logging.getLogger(__name__) + class _ProcessFidelityAnalysis(curve.CurveAnalysis): r"""A class to estimate process fidelity from one of 1Q/2Q simultaneous direct RB experiments @@ -159,6 +163,7 @@ def _run_analysis( try: return super()._run_analysis(experiment_data) except Exception: + LOG.error(f"{self.__class__.__name__}({self._physical_qubits}) failed: {traceback.format_exc()}") failed_result = AnalysisResultData( name="ProcessFidelity", value=None, @@ -212,6 +217,7 @@ def _run_analysis( analysis_results = [slf_result] + analysis_results return analysis_results, figures except Exception: + LOG.error(f"{self.__class__.__name__} failed: {traceback.format_exc()}") failed_result = AnalysisResultData( name="SingleLF", value=None, @@ -273,6 +279,7 @@ def _run_analysis( analysis_results = [lf_result, eplg_result] + analysis_results return analysis_results, figures except Exception: + LOG.error(f"{self.__class__.__name__} failed: {traceback.format_exc()}") failed_results = [ AnalysisResultData( name="LF", From 9d760444ce4d5100b6680c3c42f929b87ea00983 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Thu, 7 Dec 2023 11:47:54 +0900 Subject: [PATCH 12/38] Add replicate_in_parallel (a.k.a. seq_rep) option --- .../randomized_benchmarking/layer_fidelity.py | 170 ++++++++++++++---- .../layer_fidelity_analysis.py | 10 +- 2 files changed, 140 insertions(+), 40 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 7cd88c04b8..92058861ee 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -79,6 +79,7 @@ def __init__( # full_sampling: Optional[bool] = False, TODO: can we always do full_sampling and remove the option? two_qubit_gate: Optional[str] = None, one_qubit_basis_gates: Optional[Sequence[str]] = None, + replicate_in_parallel: bool = True, ): """Initialize a standard randomized benchmarking experiment. @@ -93,6 +94,7 @@ def __init__( with this seed value every time :meth:`circuits` is called. two_qubit_gate: Two-qubit gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. one_qubit_basis_gates: One-qubit gates to use for implementing 1q Clifford operations. + replicate_in_parallel: Use a common 1Q/2Q direct RB sequence for all quibit pairs in a layer or not. Raises: QiskitError: If any invalid argument is supplied. @@ -136,6 +138,7 @@ def __init__( two_qubit_layers=two_qubit_layers, two_qubit_gate=two_qubit_gate, one_qubit_basis_gates=tuple(one_qubit_basis_gates), + replicate_in_parallel=replicate_in_parallel, ) # self.analysis.set_options(outcome="0" * self.num_qubits) @@ -153,6 +156,7 @@ def _default_experiment_options(cls) -> Options: :meth:`circuits` is called. two_qubit_gate (str): Two-qubit gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. one_qubit_basis_gates (Tuple[str]): One-qubit gates to use for implementing 1q Clifford operations. + replicate_in_parallel (bool): Use a common 1Q/2Q direct RB sequence for all quibit pairs in a layer or not. clifford_synthesis_method (str): The name of the Clifford synthesis plugin to use for building circuits of RB sequences. """ @@ -165,6 +169,7 @@ def _default_experiment_options(cls) -> Options: two_qubit_gate=None, one_qubit_basis_gates=(), clifford_synthesis_method=DEFAULT_SYNTHESIS_METHOD, + replicate_in_parallel=True, ) return options @@ -220,10 +225,9 @@ def circuits(self) -> List[QuantumCircuit]: A list of :class:`QuantumCircuit`. """ opts = self.experiment_options - rng = default_rng(seed=opts.seed) - GATE2Q = GATE_NAME_MAP[opts.two_qubit_gate] - GATE2Q_CLIFF = num_from_2q_circuit(Clifford(GATE2Q).to_circuit()) residal_qubits_by_layer = [self.__residual_qubits(layer) for layer in opts.two_qubit_layers] + rng = default_rng(seed=opts.seed) + # define functions and variables for speed _to_gate_1q = functools.partial( _clifford_1q_int_to_instruction, basis_gates=opts.one_qubit_basis_gates, @@ -235,6 +239,8 @@ def circuits(self) -> List[QuantumCircuit]: coupling_tuple=((0, 1),), synthesis_method=opts.clifford_synthesis_method, ) + GATE2Q = GATE_NAME_MAP[opts.two_qubit_gate] + GATE2Q_CLIFF = num_from_2q_circuit(Clifford(GATE2Q).to_circuit()) # Circuit generation circuits = [] num_qubits = max(self.physical_qubits) + 1 @@ -250,41 +256,37 @@ def circuits(self) -> List[QuantumCircuit]: [(c,) for c in range(2 * num_2q_gates, 2 * num_2q_gates + num_1q_gates)] ) for length in opts.lengths: - # initialize cliffords and a ciruit (0: identity clifford) - cliffs_2q = [0] * num_2q_gates - cliffs_1q = [0] * num_1q_gates circ = QuantumCircuit(num_qubits, num_qubits) - for _ in range(length): - # sample random 1q-Clifford layer - for j, qpair in enumerate(two_qubit_layer): - # sample product of two 1q-Cliffords as 2q interger Clifford - samples = rng.integers(NUM_1Q_CLIFFORD, size=2) - cliffs_2q[j] = compose_2q(cliffs_2q[j], _product_1q_nums(*samples)) - for sample, q in zip(samples, qpair): - circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) - for k, q in enumerate(one_qubits): - sample = rng.integers(NUM_1Q_CLIFFORD) - cliffs_1q[k] = compose_1q(cliffs_1q[k], sample) - circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) - circ.barrier(self.physical_qubits) - # add two qubit gates - for j, qpair in enumerate(two_qubit_layer): - circ._append(GATE2Q, tuple(circ.qubits[q] for q in qpair), ()) - cliffs_2q[j] = compose_2q(cliffs_2q[j], GATE2Q_CLIFF) - # TODO: add dd if necessary - for k, q in enumerate(one_qubits): - # TODO: add dd if necessary - pass - circ.barrier(self.physical_qubits) - # add the last inverse - for j, qpair in enumerate(two_qubit_layer): - inv = inverse_2q(cliffs_2q[j]) - circ._append(_to_gate_2q(inv), tuple(circ.qubits[q] for q in qpair), ()) - for k, q in enumerate(one_qubits): - inv = inverse_1q(cliffs_1q[k]) - circ._append(_to_gate_1q(inv), (circ.qubits[q],), ()) + BARRIER_INST = CircuitInstruction(Barrier(num_qubits), circ.qubits) + # add the main body of the circuit switching implementation for speed + if opts.replicate_in_parallel: + self.__circuit_body_with_replication( + circ, + length, + two_qubit_layer, + one_qubits, + rng, + _to_gate_1q, + _to_gate_2q, + GATE2Q, + GATE2Q_CLIFF, + BARRIER_INST, + ) + else: + self.__circuit_body( + circ, + length, + two_qubit_layer, + one_qubits, + rng, + _to_gate_1q, + _to_gate_2q, + GATE2Q, + GATE2Q_CLIFF, + BARRIER_INST, + ) # add the measurements - circ.barrier(self.physical_qubits) + circ._append(BARRIER_INST) for qubits, clbits in zip(composite_qubits, composite_clbits): circ.measure(qubits, clbits) # store composite structure in metadata @@ -293,7 +295,7 @@ def circuits(self) -> List[QuantumCircuit]: "composite_metadata": [ { "experiment_type": "ParallelExperiment", - "composite_index": list(range(num_2q_gates + num_1q_gates)), + "composite_index": list(range(len(composite_qubits))), "composite_metadata": [ { "experiment_type": "SubLayerFidelity", @@ -322,6 +324,100 @@ def circuits(self) -> List[QuantumCircuit]: return circuits + @staticmethod + def __circuit_body( + circ, + length, + two_qubit_layer, + one_qubits, + rng, + _to_gate_1q, + _to_gate_2q, + GATE2Q, + GATE2Q_CLIFF, + BARRIER_INST, + ): + # initialize cliffords and a ciruit (0: identity clifford) + cliffs_2q = [0] * len(two_qubit_layer) + cliffs_1q = [0] * len(one_qubits) + for _ in range(length): + # sample random 1q-Clifford layer + for j, qpair in enumerate(two_qubit_layer): + # sample product of two 1q-Cliffords as 2q interger Clifford + samples = rng.integers(NUM_1Q_CLIFFORD, size=2) + cliffs_2q[j] = compose_2q(cliffs_2q[j], _product_1q_nums(*samples)) + for sample, q in zip(samples, qpair): + circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) + for k, q in enumerate(one_qubits): + sample = rng.integers(NUM_1Q_CLIFFORD) + cliffs_1q[k] = compose_1q(cliffs_1q[k], sample) + circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) + circ._append(BARRIER_INST) + # add two qubit gates + for j, qpair in enumerate(two_qubit_layer): + circ._append(GATE2Q, tuple(circ.qubits[q] for q in qpair), ()) + cliffs_2q[j] = compose_2q(cliffs_2q[j], GATE2Q_CLIFF) + # TODO: add dd if necessary + for k, q in enumerate(one_qubits): + # TODO: add dd if necessary + pass + circ._append(BARRIER_INST) + # add the last inverse + for j, qpair in enumerate(two_qubit_layer): + inv = inverse_2q(cliffs_2q[j]) + circ._append(_to_gate_2q(inv), tuple(circ.qubits[q] for q in qpair), ()) + for k, q in enumerate(one_qubits): + inv = inverse_1q(cliffs_1q[k]) + circ._append(_to_gate_1q(inv), (circ.qubits[q],), ()) + return circ + + @staticmethod + def __circuit_body_with_replication( + circ, + length, + two_qubit_layer, + one_qubits, + rng, + _to_gate_1q, + _to_gate_2q, + GATE2Q, + GATE2Q_CLIFF, + BARRIER_INST, + ): + # initialize cliffords and a ciruit (0: identity clifford) + cliff_2q = 0 + cliff_1q = 0 + for _ in range(length): + # sample random 1q-Clifford layer + samples = rng.integers(NUM_1Q_CLIFFORD, size=2) + cliff_2q = compose_2q(cliff_2q, _product_1q_nums(*samples)) + for qpair in two_qubit_layer: + for sample, q in zip(samples, qpair): + circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) + if one_qubits: + sample = rng.integers(NUM_1Q_CLIFFORD) + cliff_1q = compose_1q(cliff_1q, sample) + for q in one_qubits: + circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) + circ._append(BARRIER_INST) + # add two qubit gates + cliff_2q = compose_2q(cliff_2q, GATE2Q_CLIFF) + for qpair in two_qubit_layer: + circ._append(GATE2Q, tuple(circ.qubits[q] for q in qpair), ()) + # TODO: add dd if necessary + for q in one_qubits: + # TODO: add dd if necessary + pass + circ._append(BARRIER_INST) + # add the last inverse + inv = inverse_2q(cliff_2q) + for qpair in two_qubit_layer: + circ._append(_to_gate_2q(inv), tuple(circ.qubits[q] for q in qpair), ()) + inv = inverse_1q(cliff_1q) + for q in one_qubits: + circ._append(_to_gate_1q(inv), (circ.qubits[q],), ()) + return circ + def _transpiled_circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits, transpiled.""" transpiled = [_decompose_clifford_ops(circ) for circ in self.circuits()] diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index ed19b5685f..811dff1fb1 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -163,7 +163,9 @@ def _run_analysis( try: return super()._run_analysis(experiment_data) except Exception: - LOG.error(f"{self.__class__.__name__}({self._physical_qubits}) failed: {traceback.format_exc()}") + LOG.error( + f"{self.__class__.__name__}({self._physical_qubits}) failed: {traceback.format_exc()}" + ) failed_result = AnalysisResultData( name="ProcessFidelity", value=None, @@ -206,7 +208,9 @@ def _run_analysis( # Calculate single layer fidelity from process fidelities of subsystems pfs = [res.value for res in analysis_results if res.name == "ProcessFidelity"] slf = np.prod(pfs) - quality_slf = "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" + quality_slf = ( + "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" + ) slf_result = AnalysisResultData( name="SingleLF", value=slf, @@ -290,6 +294,6 @@ def _run_analysis( name="EPLG", value=None, quality="failed", - ) + ), ] return failed_results + analysis_results, figures From 489538da406c461a2b2c866296cc664a1f482769 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Tue, 12 Dec 2023 22:45:53 +0900 Subject: [PATCH 13/38] fix bugs --- .../randomized_benchmarking/layer_fidelity.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 92058861ee..5c11fbe5f8 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -182,12 +182,13 @@ def set_experiment_options(self, **fields): Raises: AttributeError: If the field passed in is not a supported options """ - for field in {"two_qubit_layers"}: - if ( - hasattr(self._experiment_options, field) - and self._experiment_options[field] is not None - ): - raise AttributeError(f"Options field {field} is not allowed to update.") + for field in fields: + if field in {"two_qubit_layers"}: + if ( + hasattr(self._experiment_options, field) + and self._experiment_options[field] is not None + ): + raise AttributeError(f"Options field {field} is not allowed to update.") super().set_experiment_options(**fields) @classmethod @@ -436,7 +437,10 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]: inst_prop = self.backend.target[op_name].get(qargs, None) if inst_prop is None: continue - schedule = inst_prop.calibration + try: + schedule = inst_prop.calibration + except Exception: # for backends that provide an invalid schedule + continue if schedule is None: continue publisher = schedule.metadata.get("publisher", CalibrationPublisher.QISKIT) From 27eec4f84fb4afd9f123e3885f0c444395c7ce54 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Tue, 12 Dec 2023 23:08:56 +0900 Subject: [PATCH 14/38] fix some lint errors --- .../randomized_benchmarking/layer_fidelity.py | 18 +++++++++++------- .../layer_fidelity_analysis.py | 15 +++++++++------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 5c11fbe5f8..09abd35814 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -76,7 +76,7 @@ def __init__( backend: Optional[Backend] = None, num_samples: int = 3, seed: Optional[Union[int, SeedSequence, BitGenerator, Generator]] = None, - # full_sampling: Optional[bool] = False, TODO: can we always do full_sampling and remove the option? + # full_sampling: Optional[bool] = True, TODO: can we remove this option? two_qubit_gate: Optional[str] = None, one_qubit_basis_gates: Optional[Sequence[str]] = None, replicate_in_parallel: bool = True, @@ -92,9 +92,11 @@ def __init__( seed: Optional, seed used to initialize ``numpy.random.default_rng``. when generating circuits. The ``default_rng`` will be initialized with this seed value every time :meth:`circuits` is called. - two_qubit_gate: Two-qubit gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. + two_qubit_gate: Two-qubit gate name (e.g. "cx", "cz", "ecr") + of which the two qubit layers consist. one_qubit_basis_gates: One-qubit gates to use for implementing 1q Clifford operations. - replicate_in_parallel: Use a common 1Q/2Q direct RB sequence for all quibit pairs in a layer or not. + replicate_in_parallel: Use a common 1Q/2Q direct RB sequence + for all quibit pairs in a layer or not. Raises: QiskitError: If any invalid argument is supplied. @@ -154,9 +156,11 @@ def _default_experiment_options(cls) -> Options: used to initialize ``numpy.random.default_rng`` when generating circuits. The ``default_rng`` will be initialized with this seed value every time :meth:`circuits` is called. - two_qubit_gate (str): Two-qubit gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. - one_qubit_basis_gates (Tuple[str]): One-qubit gates to use for implementing 1q Clifford operations. - replicate_in_parallel (bool): Use a common 1Q/2Q direct RB sequence for all quibit pairs in a layer or not. + two_qubit_gate (str): Two-qubit gate name (e.g. "cx", "cz", "ecr") + of which the two qubit layers consist. + one_qubit_basis_gates (Tuple[str]): One-qubit gates to use for implementing 1q Cliffords. + replicate_in_parallel (bool): Use a common 1Q/2Q direct RB sequence + for all quibit pairs in a layer or not. clifford_synthesis_method (str): The name of the Clifford synthesis plugin to use for building circuits of RB sequences. """ @@ -439,7 +443,7 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]: continue try: schedule = inst_prop.calibration - except Exception: # for backends that provide an invalid schedule + except: # TODO remove after qiskit #11397 continue if schedule is None: continue diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 811dff1fb1..11813e04f3 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -162,9 +162,12 @@ def _run_analysis( """ try: return super()._run_analysis(experiment_data) - except Exception: + except: # pylint: disable=broad-except LOG.error( - f"{self.__class__.__name__}({self._physical_qubits}) failed: {traceback.format_exc()}" + "%s(%s) failed: %s", + self.__class__.__name__, + str(self._physical_qubits), + traceback.format_exc(), ) failed_result = AnalysisResultData( name="ProcessFidelity", @@ -220,8 +223,8 @@ def _run_analysis( # Return combined results analysis_results = [slf_result] + analysis_results return analysis_results, figures - except Exception: - LOG.error(f"{self.__class__.__name__} failed: {traceback.format_exc()}") + except: # pylint: disable=broad-except + LOG.error("%s failed: %s", self.__class__.__name__, traceback.format_exc()) failed_result = AnalysisResultData( name="SingleLF", value=None, @@ -282,8 +285,8 @@ def _run_analysis( # Return combined results analysis_results = [lf_result, eplg_result] + analysis_results return analysis_results, figures - except Exception: - LOG.error(f"{self.__class__.__name__} failed: {traceback.format_exc()}") + except: # pylint: disable=broad-except + LOG.error("%s failed: %s", self.__class__.__name__, traceback.format_exc()) failed_results = [ AnalysisResultData( name="LF", From 3515ad84840763dfb2d22cea555cbe52509716f5 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Thu, 21 Dec 2023 17:25:06 +0900 Subject: [PATCH 15/38] improve fitting quality check --- .../layer_fidelity_analysis.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 11813e04f3..27209babd2 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -181,6 +181,32 @@ def _get_experiment_components(self, experiment_data: ExperimentData): """Set physical qubits to the experiment components.""" return [device.Qubit(qubit) for qubit in self._physical_qubits] + def _evaluate_quality( + self, + fit_data: curve.CurveFitResult, + ) -> Union[str, None]: + """Evaluate quality of the fit result. + + Args: + fit_data: Fit outcome. + + Returns: + String that represents fit result quality: "good" or "bad". + """ + quality = super()._evaluate_quality(fit_data) + y_intercept = fit_data.params["a"] + fit_data.params["b"] + ideal_limit = 1 / (len(self._physical_qubits)**2) + # Too large SPAM + if y_intercept < 0.7: + quality = "bad" + # Convergence to a bad value (probably due to bad readout) + if fit_data.params["b"] <= 0 or abs(fit_data.params["b"] - ideal_limit) > 0.3: + quality = "bad" + # Too good fidelity (negative decay) + if fit_data.params["alpha"] < 0: + quality = "bad" + return quality + class _SingleLayerFidelityAnalysis(CompositeAnalysis): r"""A class to estimate a process fidelity per disjoint layer. From 5e4b3be3c04f2f29607871b981cab6450aa097e4 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Thu, 21 Dec 2023 23:20:04 +0900 Subject: [PATCH 16/38] add bad-quality reason and parameters as extra fields --- .../layer_fidelity_analysis.py | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 27209babd2..68d2b5c035 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -141,7 +141,11 @@ def _create_analysis_results( alpha = fit_data.ufloat_params["alpha"] pf = (1 + (2**num_qubits - 1) * alpha) / (2**num_qubits) + quality, reason = self.__evaluate_quality(fit_data) + metadata["qubits"] = self._physical_qubits + metadata["reason"] = reason + metadata.update(fit_data.params) outcomes.append( AnalysisResultData( name="ProcessFidelity", @@ -172,8 +176,8 @@ def _run_analysis( failed_result = AnalysisResultData( name="ProcessFidelity", value=None, - quality="failed", - extra={"qubits": self._physical_qubits}, + quality="bad", + extra={"qubits": self._physical_qubits, "reason": "analysis_failure"}, ) return [failed_result], [] @@ -181,31 +185,36 @@ def _get_experiment_components(self, experiment_data: ExperimentData): """Set physical qubits to the experiment components.""" return [device.Qubit(qubit) for qubit in self._physical_qubits] - def _evaluate_quality( + def __evaluate_quality( self, fit_data: curve.CurveFitResult, - ) -> Union[str, None]: - """Evaluate quality of the fit result. + ) -> Tuple[str, Union[str, None]]: + """Evaluate quality of the fit result and the reason if it is no good. Args: fit_data: Fit outcome. Returns: - String that represents fit result quality: "good" or "bad". + Pair of strings that represent quality ("good" or "bad") and its reason if "bad". """ - quality = super()._evaluate_quality(fit_data) - y_intercept = fit_data.params["a"] + fit_data.params["b"] - ideal_limit = 1 / (len(self._physical_qubits)**2) # Too large SPAM + y_intercept = fit_data.params["a"] + fit_data.params["b"] if y_intercept < 0.7: - quality = "bad" + return "bad", "large_spam" # Convergence to a bad value (probably due to bad readout) + ideal_limit = 1 / (2 ** len(self._physical_qubits)) if fit_data.params["b"] <= 0 or abs(fit_data.params["b"] - ideal_limit) > 0.3: - quality = "bad" + return "bad", "biased_tail" # Too good fidelity (negative decay) if fit_data.params["alpha"] < 0: - quality = "bad" - return quality + return "bad", "negative_decay" + # Large residual errors in terms of reduced Chi-square + if fit_data.reduced_chisq > 3.0: + return "bad", "large_chisq" + # Too good Chi-square + if fit_data.reduced_chisq == 0: + return "bad", "zero_chisq" + return "good", None class _SingleLayerFidelityAnalysis(CompositeAnalysis): @@ -254,8 +263,11 @@ def _run_analysis( failed_result = AnalysisResultData( name="SingleLF", value=None, - quality="failed", - extra={"qubits": [q for qubits in self._layer for q in qubits]}, + quality="bad", + extra={ + "qubits": [q for qubits in self._layer for q in qubits], + "reason": "analysis_failure", + }, ) return [failed_result] + analysis_results, figures @@ -317,12 +329,14 @@ def _run_analysis( AnalysisResultData( name="LF", value=None, - quality="failed", + quality="bad", + extra={"reason": "analysis_failure"}, ), AnalysisResultData( name="EPLG", value=None, - quality="failed", + quality="bad", + extra={"reason": "analysis_failure"}, ), ] return failed_results + analysis_results, figures From 94cf76fd45dc097d0d5559164b2d6a8f11542ab1 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 22 Dec 2023 10:27:13 +0900 Subject: [PATCH 17/38] add generator of circuits --- .../randomized_benchmarking/layer_fidelity.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 09abd35814..d3d78bd159 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -229,6 +229,14 @@ def circuits(self) -> List[QuantumCircuit]: Returns: A list of :class:`QuantumCircuit`. """ + return list(self.circuits_generator()) + + def circuits_generator(self) -> Iterable[QuantumCircuit]: + """Generate physical circuits to measure layer fidelity. + + Returns: + A generator of :class:`QuantumCircuit`s. + """ opts = self.experiment_options residal_qubits_by_layer = [self.__residual_qubits(layer) for layer in opts.two_qubit_layers] rng = default_rng(seed=opts.seed) @@ -325,9 +333,7 @@ def circuits(self) -> List[QuantumCircuit]: ], "composite_index": [i_set], } - circuits.append(circ) - - return circuits + yield circ @staticmethod def __circuit_body( From ee4357743bd7f761c22edb26f1d349e0065b9b4c Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 22 Dec 2023 23:46:39 +0900 Subject: [PATCH 18/38] fix critical bug in the computation of process fidelity --- .../library/randomized_benchmarking/layer_fidelity_analysis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 68d2b5c035..8da06a0ccb 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -136,10 +136,11 @@ def _create_analysis_results( """ outcomes = super()._create_analysis_results(fit_data, quality, **metadata) num_qubits = len(self._physical_qubits) + d = 2**num_qubits # Calculate process fidelity alpha = fit_data.ufloat_params["alpha"] - pf = (1 + (2**num_qubits - 1) * alpha) / (2**num_qubits) + pf = (1 + (d**2 - 1) * alpha) / (d**2) quality, reason = self.__evaluate_quality(fit_data) From 7411a1ad1e0a4a1c90fa8b8544148901c3760777 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 14 Feb 2024 16:05:53 +0900 Subject: [PATCH 19/38] Add more validations --- .../randomized_benchmarking/layer_fidelity.py | 81 ++++++++++++------- .../layer_fidelity_analysis.py | 35 ++++---- 2 files changed, 74 insertions(+), 42 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index d3d78bd159..5c694fbd6e 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -21,7 +21,7 @@ from numpy.random import Generator, default_rng from numpy.random.bit_generator import BitGenerator, SeedSequence -from qiskit.circuit import QuantumCircuit, CircuitInstruction, Barrier +from qiskit.circuit import QuantumCircuit, CircuitInstruction, Barrier, Gate from qiskit.circuit.library import get_standard_gate_name_mapping from qiskit.exceptions import QiskitError from qiskit.providers import BackendV2Converter @@ -50,7 +50,6 @@ LOG = logging.getLogger(__name__) -GATE_NAME_MAP = get_standard_gate_name_mapping() NUM_1Q_CLIFFORD = CliffordUtils.NUM_CLIFFORD_1_QUBIT @@ -74,12 +73,11 @@ def __init__( two_qubit_layers: Sequence[Sequence[Tuple[int, int]]], lengths: Iterable[int], backend: Optional[Backend] = None, - num_samples: int = 3, + num_samples: int = 6, seed: Optional[Union[int, SeedSequence, BitGenerator, Generator]] = None, - # full_sampling: Optional[bool] = True, TODO: can we remove this option? two_qubit_gate: Optional[str] = None, one_qubit_basis_gates: Optional[Sequence[str]] = None, - replicate_in_parallel: bool = True, + replicate_in_parallel: bool = False, # TODO: remove this option ): """Initialize a standard randomized benchmarking experiment. @@ -105,6 +103,9 @@ def __init__( full_layers = [] for two_q_layer in two_qubit_layers: qubits_in_layer = {q for qpair in two_q_layer for q in qpair} + for q in qubits_in_layer: + if q not in physical_qubits: + raise QiskitError(f"Qubit {q} in two_qubit_layers is not in physical_qubits") layer = two_q_layer + [(q,) for q in physical_qubits if q not in qubits_in_layer] full_layers.append(layer) @@ -112,25 +113,53 @@ def __init__( super().__init__( physical_qubits, analysis=LayerFidelityAnalysis(full_layers), backend=backend ) + # assert isinstance(backend, BackendV2) # Verify parameters - # TODO more checks if len(set(lengths)) != len(lengths): - raise QiskitError( - f"The lengths list {lengths} should not contain " "duplicate elements." - ) + raise QiskitError(f"The lengths list {lengths} should not contain duplicate elements.") if num_samples <= 0: - raise QiskitError(f"The number of samples {num_samples} should " "be positive.") - if two_qubit_gate not in GATE_NAME_MAP: - pass # TODO: too restrictive to forbidden custom two qubit gate name? - - # Get parameters from backend - if two_qubit_gate is None: - # TODO: implement and raise an error if backend is None - raise NotImplemented() - if one_qubit_basis_gates is None: - # TODO: implement and raise an error if backend is None - raise NotImplemented() + raise QiskitError(f"The number of samples {num_samples} should be positive.") + + if two_qubit_gate: + if self.backend: + if two_qubit_gate not in self.backend.target.operation_names: + raise QiskitError(f"two_qubit_gate {two_qubit_gate} is not in backend.target") + for two_q_layer in two_qubit_layers: + for qpair in two_q_layer: + if not self.backend.target.instruction_supported(two_qubit_gate, qpair): + raise QiskitError(f"{two_qubit_gate}{qpair} is not in backend.target") + else: + if self.backend: + # Try to set default two_qubit_gate from backend + for op in self.backend.target.operations: + if isinstance(op, Gate) and op.num_qubits == 2: + two_qubit_gate = op.name + LOG.info("%s is set for two_qubit_gate", op.name) + break + if not two_qubit_gate: + raise QiskitError(f"two_qubit_gate is not provided and failed to set from backend.") + + if one_qubit_basis_gates: + for gate in one_qubit_basis_gates: + if gate not in self.backend.target.operation_names: + raise QiskitError(f"{gate} in one_qubit_basis_gates is not in backend.target") + for gate in one_qubit_basis_gates: + for q in self.physical_qubits: + if not self.backend.target.instruction_supported(gate, (q,)): + raise QiskitError(f"{gate}({q}) is not in backend.target") + else: + if self.backend: + # Try to set default one_qubit_basis_gates from backend + one_qubit_basis_gates = [] + for op in self.backend.target.operations: + if isinstance(op, Gate) and op.num_qubits == 1: + one_qubit_basis_gates.append(op.name) + LOG.info("%s is set for one_qubit_basis_gates", str(one_qubit_basis_gates)) + if not one_qubit_basis_gates: + raise QiskitError( + f"one_qubit_basis_gates is not provided and failed to set from backend." + ) # Set configurable options self.set_experiment_options( @@ -142,7 +171,6 @@ def __init__( one_qubit_basis_gates=tuple(one_qubit_basis_gates), replicate_in_parallel=replicate_in_parallel, ) - # self.analysis.set_options(outcome="0" * self.num_qubits) @classmethod def _default_experiment_options(cls) -> Options: @@ -211,10 +239,10 @@ def set_transpile_options(self, **fields): ) def _set_backend(self, backend: Backend): - """Set the backend V2 for RB experiments since RB experiments only support BackendV2 - except for simulators. If BackendV1 is provided, it is converted to V2 and stored. + """Set the backend V2 for RB experiments since RB experiments only support BackendV2. + If BackendV1 is provided, it is converted to V2 and stored. """ - if isinstance(backend, BackendV1) and "simulator" not in backend.name(): + if isinstance(backend, BackendV1): super()._set_backend(BackendV2Converter(backend, add_delay=True)) else: super()._set_backend(backend) @@ -252,7 +280,7 @@ def circuits_generator(self) -> Iterable[QuantumCircuit]: coupling_tuple=((0, 1),), synthesis_method=opts.clifford_synthesis_method, ) - GATE2Q = GATE_NAME_MAP[opts.two_qubit_gate] + GATE2Q = self.backend.target.operation_from_name(opts.two_qubit_gate) GATE2Q_CLIFF = num_from_2q_circuit(Clifford(GATE2Q).to_circuit()) # Circuit generation circuits = [] @@ -458,9 +486,6 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]: common_calibrations[op_name][(qargs, tuple())] = schedule for circ in transpiled: - # This logic is inefficient in terms of payload size and backend compilation - # because this binds every custom pulse to a circuit regardless of - # its existence. It works but redundant calibration must be removed -- NK. circ.calibrations = common_calibrations return transpiled diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 8da06a0ccb..4190147a6f 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -140,7 +140,7 @@ def _create_analysis_results( # Calculate process fidelity alpha = fit_data.ufloat_params["alpha"] - pf = (1 + (d**2 - 1) * alpha) / (d**2) + pf = (1 + (d * d - 1) * alpha) / (d * d) quality, reason = self.__evaluate_quality(fit_data) @@ -161,10 +161,6 @@ def _create_analysis_results( def _run_analysis( self, experiment_data: ExperimentData ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: - r"""TODO - - Note: Empty analysis results will be returned when failing analysis. - """ try: return super()._run_analysis(experiment_data) except: # pylint: disable=broad-except @@ -221,7 +217,7 @@ def __evaluate_quality( class _SingleLayerFidelityAnalysis(CompositeAnalysis): r"""A class to estimate a process fidelity per disjoint layer. - Note: Empty analysis results will be returned when failing analysis. + TODO: Add math. # section: reference .. ref_arxiv:: 1 2311.05933 @@ -229,8 +225,8 @@ class _SingleLayerFidelityAnalysis(CompositeAnalysis): def __init__(self, layer, analyses=None): if analyses: - # TODO: Validation - pass + if len(layer) != len(analyses): + raise AnalysisError(f"'analyses' must have the same length with 'layer'") else: analyses = [_ProcessFidelityAnalysis(qubits) for qubits in layer] @@ -240,7 +236,6 @@ def __init__(self, layer, analyses=None): def _run_analysis( self, experiment_data: ExperimentData ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: - r"""TODO""" try: # Run composite analysis and extract sub-experiments results analysis_results, figures = super()._run_analysis(experiment_data) @@ -286,8 +281,8 @@ class LayerFidelityAnalysis(CompositeAnalysis): def __init__(self, layers, analyses=None): if analyses: - # TODO: Validation - pass + if len(layers) != len(analyses): + raise AnalysisError(f"'analyses' must have the same length with 'layers'") else: analyses = [_SingleLayerFidelityAnalysis(a_layer) for a_layer in layers] @@ -299,9 +294,21 @@ def _run_analysis( self, experiment_data: ExperimentData ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: r"""Run analysis for Layer Fidelity experiment. - It invokes CompositeAnalysis._run_analysis that will invoke - _run_analysis for the sub-experiments (1Q/2Q simultaneous direct RBs). - Based on the results, it computes the result for Layer Fidelity. + + It invokes :meth:`CompositeAnalysis._run_analysis` that will invoke + ``_run_analysis`` for the sub-experiments (1Q/2Q simultaneous direct RBs for each layer). + Based on the results, it computes Layer Fidelity and EPLG (error per layered gate). + + TODO: Add math. + + Args: + experiment_data: the experiment data to analyze. + + Returns: + A pair ``(analysis_results, figures)`` where ``analysis_results`` + is a list of :class:`AnalysisResultData` objects, and ``figures`` + is a list of any figures for the experiment. + If an analysis fails, an analysis result with ``None`` value will be returned. """ try: # Run composite analysis and extract sub-experiments results From 3fc44fd1d4075fcee7fea99e24f6e1bf6c992bec Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 14 Feb 2024 18:58:08 +0900 Subject: [PATCH 20/38] Fixes to pass all the tests --- .../randomized_benchmarking/layer_fidelity.py | 112 ++++++++++++------ .../test_layer_fidelity.py | 5 +- 2 files changed, 78 insertions(+), 39 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 5c694fbd6e..c3f0a0de64 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -15,7 +15,7 @@ import functools import logging from collections import defaultdict -from typing import Union, Iterable, Optional, List, Sequence, Tuple +from typing import Union, Iterable, Optional, List, Sequence, Tuple, Dict import numpy as np from numpy.random import Generator, default_rng @@ -30,6 +30,7 @@ from qiskit.pulse.instruction_schedule_map import CalibrationPublisher from qiskit_experiments.framework import BaseExperiment, Options +from qiskit_experiments.framework.configs import ExperimentConfig from qiskit_experiments.framework.restless_mixin import RestlessMixin from .clifford_utils import ( @@ -50,6 +51,7 @@ LOG = logging.getLogger(__name__) +GATE_NAME_MAP = get_standard_gate_name_mapping() NUM_1Q_CLIFFORD = CliffordUtils.NUM_CLIFFORD_1_QUBIT @@ -103,6 +105,10 @@ def __init__( full_layers = [] for two_q_layer in two_qubit_layers: qubits_in_layer = {q for qpair in two_q_layer for q in qpair} + if len(qubits_in_layer) != 2 * len(two_q_layer): + raise QiskitError( + f"two_qubit_layers have a layer with gates on non-disjoint qubits" + ) for q in qubits_in_layer: if q not in physical_qubits: raise QiskitError(f"Qubit {q} in two_qubit_layers is not in physical_qubits") @@ -121,45 +127,39 @@ def __init__( if num_samples <= 0: raise QiskitError(f"The number of samples {num_samples} should be positive.") - if two_qubit_gate: - if self.backend: - if two_qubit_gate not in self.backend.target.operation_names: - raise QiskitError(f"two_qubit_gate {two_qubit_gate} is not in backend.target") - for two_q_layer in two_qubit_layers: - for qpair in two_q_layer: - if not self.backend.target.instruction_supported(two_qubit_gate, qpair): - raise QiskitError(f"{two_qubit_gate}{qpair} is not in backend.target") - else: - if self.backend: - # Try to set default two_qubit_gate from backend - for op in self.backend.target.operations: - if isinstance(op, Gate) and op.num_qubits == 2: - two_qubit_gate = op.name - LOG.info("%s is set for two_qubit_gate", op.name) - break + if two_qubit_gate is None: + if self.backend is None: + raise QiskitError(f"two_qubit_gate or backend must be supplied.") + # Try to set default two_qubit_gate from backend + for op in self.backend.target.operations: + if isinstance(op, Gate) and op.num_qubits == 2: + two_qubit_gate = op.name + LOG.info("%s is set for two_qubit_gate", op.name) + break if not two_qubit_gate: raise QiskitError(f"two_qubit_gate is not provided and failed to set from backend.") - - if one_qubit_basis_gates: - for gate in one_qubit_basis_gates: - if gate not in self.backend.target.operation_names: - raise QiskitError(f"{gate} in one_qubit_basis_gates is not in backend.target") - for gate in one_qubit_basis_gates: - for q in self.physical_qubits: - if not self.backend.target.instruction_supported(gate, (q,)): - raise QiskitError(f"{gate}({q}) is not in backend.target") else: - if self.backend: - # Try to set default one_qubit_basis_gates from backend - one_qubit_basis_gates = [] - for op in self.backend.target.operations: - if isinstance(op, Gate) and op.num_qubits == 1: - one_qubit_basis_gates.append(op.name) - LOG.info("%s is set for one_qubit_basis_gates", str(one_qubit_basis_gates)) + if self.backend is None and two_qubit_gate not in GATE_NAME_MAP: + raise QiskitError(f"Unknown two_qubit_gate: {two_qubit_gate}.") + + if one_qubit_basis_gates is None: + if self.backend is None: + raise QiskitError(f"one_qubit_basis_gates or backend must be supplied.") + # Try to set default one_qubit_basis_gates from backend + one_qubit_basis_gates = [] + for op in self.backend.target.operations: + if isinstance(op, Gate) and op.num_qubits == 1: + one_qubit_basis_gates.append(op.name) + LOG.info("%s is set for one_qubit_basis_gates", str(one_qubit_basis_gates)) if not one_qubit_basis_gates: raise QiskitError( f"one_qubit_basis_gates is not provided and failed to set from backend." ) + else: + if self.backend is None: + for gate in one_qubit_basis_gates: + if gate not in GATE_NAME_MAP: + raise QiskitError(f"Unknown gate in one_qubit_basis_gates: {gate}.") # Set configurable options self.set_experiment_options( @@ -172,6 +172,9 @@ def __init__( replicate_in_parallel=replicate_in_parallel, ) + # Verify two_qubit_gate and one_qubit_basis_gates + self.__validate_basis_gates() + @classmethod def _default_experiment_options(cls) -> Options: """Default experiment options. @@ -199,7 +202,7 @@ def _default_experiment_options(cls) -> Options: seed=None, two_qubit_layers=None, two_qubit_gate=None, - one_qubit_basis_gates=(), + one_qubit_basis_gates=None, clifford_synthesis_method=DEFAULT_SYNTHESIS_METHOD, replicate_in_parallel=True, ) @@ -246,6 +249,28 @@ def _set_backend(self, backend: Backend): super()._set_backend(BackendV2Converter(backend, add_delay=True)) else: super()._set_backend(backend) + self.__validate_basis_gates() + + def __validate_basis_gates(self) -> None: + if not self.backend: + return + opts = self.experiment_options + # validate two_qubit_gate if it is set + if opts.two_qubit_gate: + if opts.two_qubit_gate not in self.backend.target.operation_names: + raise QiskitError(f"two_qubit_gate {opts.two_qubit_gate} is not in backend.target") + for two_q_layer in opts.two_qubit_layers: + for qpair in two_q_layer: + if not self.backend.target.instruction_supported(opts.two_qubit_gate, qpair): + raise QiskitError(f"{opts.two_qubit_gate}{qpair} is not in backend.target") + # validate one_qubit_basis_gates if it is set + for gate in opts.one_qubit_basis_gates or []: + if gate not in self.backend.target.operation_names: + raise QiskitError(f"{gate} in one_qubit_basis_gates is not in backend.target") + for gate in opts.one_qubit_basis_gates or []: + for q in self.physical_qubits: + if not self.backend.target.instruction_supported(gate, (q,)): + raise QiskitError(f"{gate}({q}) is not in backend.target") def __residual_qubits(self, two_qubit_layer): qubits_in_layer = {q for qpair in two_qubit_layer for q in qpair} @@ -280,7 +305,10 @@ def circuits_generator(self) -> Iterable[QuantumCircuit]: coupling_tuple=((0, 1),), synthesis_method=opts.clifford_synthesis_method, ) - GATE2Q = self.backend.target.operation_from_name(opts.two_qubit_gate) + if self.backend: + GATE2Q = self.backend.target.operation_from_name(opts.two_qubit_gate) + else: + GATE2Q = GATE_NAME_MAP[opts.two_qubit_gate] GATE2Q_CLIFF = num_from_2q_circuit(Clifford(GATE2Q).to_circuit()) # Circuit generation circuits = [] @@ -462,11 +490,11 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]: transpiled = [_decompose_clifford_ops(circ) for circ in self.circuits()] # Set custom calibrations provided in backend if isinstance(self.backend, BackendV2): - instructions = [] # (op_name, qargs) for each element where qargs means qubit tuple + instructions = [] # (op_name, qargs) for each element where qargs mean qubit tuple for two_qubit_layer in self.experiment_options.two_qubit_layers: for qpair in two_qubit_layer: instructions.append((self.experiment_options.two_qubit_gate, tuple(qpair))) - for q in self.__residual_qubits(two_qubit_layer): + for q in self.physical_qubits: for gate_1q in self.experiment_options.one_qubit_basis_gates: instructions.append((gate_1q, (q,))) @@ -494,3 +522,13 @@ def _metadata(self): metadata = super()._metadata() metadata["two_qubit_layers"] = self.experiment_options.two_qubit_layers return metadata + + @classmethod + def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "LayerFidelity": + """Initialize an experiment from experiment config""" + if isinstance(config, dict): + config = ExperimentConfig(**dict) + ret = cls(*config.args, **config.kwargs) + if config.run_options: + ret.set_run_options(**config.run_options) + return ret diff --git a/test/library/randomized_benchmarking/test_layer_fidelity.py b/test/library/randomized_benchmarking/test_layer_fidelity.py index 2449c49842..e81152db91 100644 --- a/test/library/randomized_benchmarking/test_layer_fidelity.py +++ b/test/library/randomized_benchmarking/test_layer_fidelity.py @@ -171,13 +171,14 @@ def test_backend_with_directed_basis_gates(self): exp = LayerFidelity( physical_qubits=(0, 1, 2, 3), - two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], + two_qubit_layers=[[(1, 0), (2, 3)], [(2, 1)]], lengths=[10, 20, 30], seed=42, + num_samples=1, backend=my_backend, ) transpiled = exp._transpiled_circuits() - for qc in transpiled: + for qc in transpiled[3:]: # check only the second layer self.assertTrue(qc.count_ops().get("cx", 0) > 0) expected_qubits = (qc.qubits[2], qc.qubits[1]) for inst in qc: From d84ebced0deb69cc0ed8ff60d46d766a050f0afb Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 14 Feb 2024 19:42:42 +0900 Subject: [PATCH 21/38] Fix lint --- .../randomized_benchmarking/layer_fidelity.py | 50 +++++++++---------- .../layer_fidelity_analysis.py | 12 ++--- .../test_layer_fidelity.py | 5 +- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index c3f0a0de64..2965e103c0 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -17,7 +17,6 @@ from collections import defaultdict from typing import Union, Iterable, Optional, List, Sequence, Tuple, Dict -import numpy as np from numpy.random import Generator, default_rng from numpy.random.bit_generator import BitGenerator, SeedSequence @@ -106,9 +105,7 @@ def __init__( for two_q_layer in two_qubit_layers: qubits_in_layer = {q for qpair in two_q_layer for q in qpair} if len(qubits_in_layer) != 2 * len(two_q_layer): - raise QiskitError( - f"two_qubit_layers have a layer with gates on non-disjoint qubits" - ) + raise QiskitError("two_qubit_layers have a layer with gates on non-disjoint qubits") for q in qubits_in_layer: if q not in physical_qubits: raise QiskitError(f"Qubit {q} in two_qubit_layers is not in physical_qubits") @@ -129,7 +126,7 @@ def __init__( if two_qubit_gate is None: if self.backend is None: - raise QiskitError(f"two_qubit_gate or backend must be supplied.") + raise QiskitError("two_qubit_gate or backend must be supplied.") # Try to set default two_qubit_gate from backend for op in self.backend.target.operations: if isinstance(op, Gate) and op.num_qubits == 2: @@ -137,14 +134,14 @@ def __init__( LOG.info("%s is set for two_qubit_gate", op.name) break if not two_qubit_gate: - raise QiskitError(f"two_qubit_gate is not provided and failed to set from backend.") + raise QiskitError("two_qubit_gate is not provided and failed to set from backend.") else: if self.backend is None and two_qubit_gate not in GATE_NAME_MAP: raise QiskitError(f"Unknown two_qubit_gate: {two_qubit_gate}.") if one_qubit_basis_gates is None: if self.backend is None: - raise QiskitError(f"one_qubit_basis_gates or backend must be supplied.") + raise QiskitError("one_qubit_basis_gates or backend must be supplied.") # Try to set default one_qubit_basis_gates from backend one_qubit_basis_gates = [] for op in self.backend.target.operations: @@ -153,7 +150,7 @@ def __init__( LOG.info("%s is set for one_qubit_basis_gates", str(one_qubit_basis_gates)) if not one_qubit_basis_gates: raise QiskitError( - f"one_qubit_basis_gates is not provided and failed to set from backend." + "one_qubit_basis_gates is not provided and failed to set from backend." ) else: if self.backend is None: @@ -306,12 +303,11 @@ def circuits_generator(self) -> Iterable[QuantumCircuit]: synthesis_method=opts.clifford_synthesis_method, ) if self.backend: - GATE2Q = self.backend.target.operation_from_name(opts.two_qubit_gate) + gate2q = self.backend.target.operation_from_name(opts.two_qubit_gate) else: - GATE2Q = GATE_NAME_MAP[opts.two_qubit_gate] - GATE2Q_CLIFF = num_from_2q_circuit(Clifford(GATE2Q).to_circuit()) + gate2q = GATE_NAME_MAP[opts.two_qubit_gate] + gate2q_cliff = num_from_2q_circuit(Clifford(gate2q).to_circuit()) # Circuit generation - circuits = [] num_qubits = max(self.physical_qubits) + 1 for i_sample in range(opts.num_samples): for i_set, (two_qubit_layer, one_qubits) in enumerate( @@ -326,7 +322,7 @@ def circuits_generator(self) -> Iterable[QuantumCircuit]: ) for length in opts.lengths: circ = QuantumCircuit(num_qubits, num_qubits) - BARRIER_INST = CircuitInstruction(Barrier(num_qubits), circ.qubits) + barrier_inst = CircuitInstruction(Barrier(num_qubits), circ.qubits) # add the main body of the circuit switching implementation for speed if opts.replicate_in_parallel: self.__circuit_body_with_replication( @@ -337,9 +333,9 @@ def circuits_generator(self) -> Iterable[QuantumCircuit]: rng, _to_gate_1q, _to_gate_2q, - GATE2Q, - GATE2Q_CLIFF, - BARRIER_INST, + gate2q, + gate2q_cliff, + barrier_inst, ) else: self.__circuit_body( @@ -350,12 +346,12 @@ def circuits_generator(self) -> Iterable[QuantumCircuit]: rng, _to_gate_1q, _to_gate_2q, - GATE2Q, - GATE2Q_CLIFF, - BARRIER_INST, + gate2q, + gate2q_cliff, + barrier_inst, ) # add the measurements - circ._append(BARRIER_INST) + circ._append(barrier_inst) for qubits, clbits in zip(composite_qubits, composite_clbits): circ.measure(qubits, clbits) # store composite structure in metadata @@ -400,9 +396,9 @@ def __circuit_body( rng, _to_gate_1q, _to_gate_2q, - GATE2Q, - GATE2Q_CLIFF, - BARRIER_INST, + gate2q, + gate2q_cliff, + barrier_inst, ): # initialize cliffords and a ciruit (0: identity clifford) cliffs_2q = [0] * len(two_qubit_layer) @@ -419,16 +415,16 @@ def __circuit_body( sample = rng.integers(NUM_1Q_CLIFFORD) cliffs_1q[k] = compose_1q(cliffs_1q[k], sample) circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) - circ._append(BARRIER_INST) + circ._append(barrier_inst) # add two qubit gates for j, qpair in enumerate(two_qubit_layer): - circ._append(GATE2Q, tuple(circ.qubits[q] for q in qpair), ()) - cliffs_2q[j] = compose_2q(cliffs_2q[j], GATE2Q_CLIFF) + circ._append(gate2q, tuple(circ.qubits[q] for q in qpair), ()) + cliffs_2q[j] = compose_2q(cliffs_2q[j], gate2q_cliff) # TODO: add dd if necessary for k, q in enumerate(one_qubits): # TODO: add dd if necessary pass - circ._append(BARRIER_INST) + circ._append(barrier_inst) # add the last inverse for j, qpair in enumerate(two_qubit_layer): inv = inverse_2q(cliffs_2q[j]) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 4190147a6f..70808faba5 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -14,9 +14,9 @@ """ from typing import List, Tuple, Union -import lmfit import logging import traceback +import lmfit import numpy as np import qiskit_experiments.curve_analysis as curve @@ -163,7 +163,7 @@ def _run_analysis( ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: try: return super()._run_analysis(experiment_data) - except: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except LOG.error( "%s(%s) failed: %s", self.__class__.__name__, @@ -226,7 +226,7 @@ class _SingleLayerFidelityAnalysis(CompositeAnalysis): def __init__(self, layer, analyses=None): if analyses: if len(layer) != len(analyses): - raise AnalysisError(f"'analyses' must have the same length with 'layer'") + raise AnalysisError("'analyses' must have the same length with 'layer'") else: analyses = [_ProcessFidelityAnalysis(qubits) for qubits in layer] @@ -254,7 +254,7 @@ def _run_analysis( # Return combined results analysis_results = [slf_result] + analysis_results return analysis_results, figures - except: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except LOG.error("%s failed: %s", self.__class__.__name__, traceback.format_exc()) failed_result = AnalysisResultData( name="SingleLF", @@ -282,7 +282,7 @@ class LayerFidelityAnalysis(CompositeAnalysis): def __init__(self, layers, analyses=None): if analyses: if len(layers) != len(analyses): - raise AnalysisError(f"'analyses' must have the same length with 'layers'") + raise AnalysisError("'analyses' must have the same length with 'layers'") else: analyses = [_SingleLayerFidelityAnalysis(a_layer) for a_layer in layers] @@ -331,7 +331,7 @@ def _run_analysis( # Return combined results analysis_results = [lf_result, eplg_result] + analysis_results return analysis_results, figures - except: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except LOG.error("%s failed: %s", self.__class__.__name__, traceback.format_exc()) failed_results = [ AnalysisResultData( diff --git a/test/library/randomized_benchmarking/test_layer_fidelity.py b/test/library/randomized_benchmarking/test_layer_fidelity.py index e81152db91..7cb0ae4f47 100644 --- a/test/library/randomized_benchmarking/test_layer_fidelity.py +++ b/test/library/randomized_benchmarking/test_layer_fidelity.py @@ -11,11 +11,10 @@ # that they have been altered from the originals. """Test for layer fidelity experiments.""" -import copy -import numpy as np - from test.base import QiskitExperimentsTestCase from test.library.randomized_benchmarking.mixin import RBTestMixin +import copy +import numpy as np from ddt import ddt, data, unpack from qiskit.circuit.library import SXGate From 494462c1db7aff0e9a929a7fbf75c1aec0a4cea5 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 14 Feb 2024 19:44:34 +0900 Subject: [PATCH 22/38] Require qiskit#11397 --- .../library/randomized_benchmarking/layer_fidelity.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 2965e103c0..7c500b5e8f 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -499,10 +499,7 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]: inst_prop = self.backend.target[op_name].get(qargs, None) if inst_prop is None: continue - try: - schedule = inst_prop.calibration - except: # TODO remove after qiskit #11397 - continue + schedule = inst_prop.calibration if schedule is None: continue publisher = schedule.metadata.get("publisher", CalibrationPublisher.QISKIT) From e2ef0e2a5da6c572a2968eee7c3574681b11705f Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 14 Feb 2024 19:49:35 +0900 Subject: [PATCH 23/38] Drop replicate_in_parallel option --- .../randomized_benchmarking/layer_fidelity.py | 93 +++---------------- 1 file changed, 12 insertions(+), 81 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 7c500b5e8f..220cb73f4b 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -78,7 +78,6 @@ def __init__( seed: Optional[Union[int, SeedSequence, BitGenerator, Generator]] = None, two_qubit_gate: Optional[str] = None, one_qubit_basis_gates: Optional[Sequence[str]] = None, - replicate_in_parallel: bool = False, # TODO: remove this option ): """Initialize a standard randomized benchmarking experiment. @@ -94,8 +93,6 @@ def __init__( two_qubit_gate: Two-qubit gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. one_qubit_basis_gates: One-qubit gates to use for implementing 1q Clifford operations. - replicate_in_parallel: Use a common 1Q/2Q direct RB sequence - for all quibit pairs in a layer or not. Raises: QiskitError: If any invalid argument is supplied. @@ -166,7 +163,6 @@ def __init__( two_qubit_layers=two_qubit_layers, two_qubit_gate=two_qubit_gate, one_qubit_basis_gates=tuple(one_qubit_basis_gates), - replicate_in_parallel=replicate_in_parallel, ) # Verify two_qubit_gate and one_qubit_basis_gates @@ -187,8 +183,6 @@ def _default_experiment_options(cls) -> Options: two_qubit_gate (str): Two-qubit gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. one_qubit_basis_gates (Tuple[str]): One-qubit gates to use for implementing 1q Cliffords. - replicate_in_parallel (bool): Use a common 1Q/2Q direct RB sequence - for all quibit pairs in a layer or not. clifford_synthesis_method (str): The name of the Clifford synthesis plugin to use for building circuits of RB sequences. """ @@ -201,7 +195,6 @@ def _default_experiment_options(cls) -> Options: two_qubit_gate=None, one_qubit_basis_gates=None, clifford_synthesis_method=DEFAULT_SYNTHESIS_METHOD, - replicate_in_parallel=True, ) return options @@ -323,33 +316,18 @@ def circuits_generator(self) -> Iterable[QuantumCircuit]: for length in opts.lengths: circ = QuantumCircuit(num_qubits, num_qubits) barrier_inst = CircuitInstruction(Barrier(num_qubits), circ.qubits) - # add the main body of the circuit switching implementation for speed - if opts.replicate_in_parallel: - self.__circuit_body_with_replication( - circ, - length, - two_qubit_layer, - one_qubits, - rng, - _to_gate_1q, - _to_gate_2q, - gate2q, - gate2q_cliff, - barrier_inst, - ) - else: - self.__circuit_body( - circ, - length, - two_qubit_layer, - one_qubits, - rng, - _to_gate_1q, - _to_gate_2q, - gate2q, - gate2q_cliff, - barrier_inst, - ) + self.__circuit_body( + circ, + length, + two_qubit_layer, + one_qubits, + rng, + _to_gate_1q, + _to_gate_2q, + gate2q, + gate2q_cliff, + barrier_inst, + ) # add the measurements circ._append(barrier_inst) for qubits, clbits in zip(composite_qubits, composite_clbits): @@ -434,53 +412,6 @@ def __circuit_body( circ._append(_to_gate_1q(inv), (circ.qubits[q],), ()) return circ - @staticmethod - def __circuit_body_with_replication( - circ, - length, - two_qubit_layer, - one_qubits, - rng, - _to_gate_1q, - _to_gate_2q, - GATE2Q, - GATE2Q_CLIFF, - BARRIER_INST, - ): - # initialize cliffords and a ciruit (0: identity clifford) - cliff_2q = 0 - cliff_1q = 0 - for _ in range(length): - # sample random 1q-Clifford layer - samples = rng.integers(NUM_1Q_CLIFFORD, size=2) - cliff_2q = compose_2q(cliff_2q, _product_1q_nums(*samples)) - for qpair in two_qubit_layer: - for sample, q in zip(samples, qpair): - circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) - if one_qubits: - sample = rng.integers(NUM_1Q_CLIFFORD) - cliff_1q = compose_1q(cliff_1q, sample) - for q in one_qubits: - circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) - circ._append(BARRIER_INST) - # add two qubit gates - cliff_2q = compose_2q(cliff_2q, GATE2Q_CLIFF) - for qpair in two_qubit_layer: - circ._append(GATE2Q, tuple(circ.qubits[q] for q in qpair), ()) - # TODO: add dd if necessary - for q in one_qubits: - # TODO: add dd if necessary - pass - circ._append(BARRIER_INST) - # add the last inverse - inv = inverse_2q(cliff_2q) - for qpair in two_qubit_layer: - circ._append(_to_gate_2q(inv), tuple(circ.qubits[q] for q in qpair), ()) - inv = inverse_1q(cliff_1q) - for q in one_qubits: - circ._append(_to_gate_1q(inv), (circ.qubits[q],), ()) - return circ - def _transpiled_circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits, transpiled.""" transpiled = [_decompose_clifford_ops(circ) for circ in self.circuits()] From 78eefaeb5f544c50462763da8e9fca671816ad94 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 14 Feb 2024 20:01:13 +0900 Subject: [PATCH 24/38] Change to use fake provider in qiskit-ibm-runtime --- test/library/randomized_benchmarking/test_layer_fidelity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/library/randomized_benchmarking/test_layer_fidelity.py b/test/library/randomized_benchmarking/test_layer_fidelity.py index 7cb0ae4f47..00f7ea28fb 100644 --- a/test/library/randomized_benchmarking/test_layer_fidelity.py +++ b/test/library/randomized_benchmarking/test_layer_fidelity.py @@ -19,8 +19,8 @@ from qiskit.circuit.library import SXGate from qiskit.exceptions import QiskitError -from qiskit.providers.fake_provider import FakeManilaV2 from qiskit.pulse import Schedule +from qiskit_ibm_runtime.fake_provider import FakeManilaV2 from qiskit_experiments.library.randomized_benchmarking import LayerFidelity, LayerFidelityAnalysis From d2ed0f7ad8fa538ccc8278ff1efcd75e15ea2343 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 14 Feb 2024 22:46:52 +0900 Subject: [PATCH 25/38] Fix to follow changes in what CurveAnalysis._run_analysis returns --- .../layer_fidelity_analysis.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 70808faba5..836c829e7d 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -23,6 +23,7 @@ import qiskit_experiments.database_service.device_component as device from qiskit_experiments.exceptions import AnalysisError from qiskit_experiments.framework import CompositeAnalysis, AnalysisResultData, ExperimentData +from qiskit_experiments.framework.containers import FigureType, ArtifactData LOG = logging.getLogger(__name__) @@ -160,7 +161,7 @@ def _create_analysis_results( def _run_analysis( self, experiment_data: ExperimentData - ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: + ) -> Tuple[List[Union[AnalysisResultData, ArtifactData]], List[FigureType]]: try: return super()._run_analysis(experiment_data) except Exception: # pylint: disable=broad-except @@ -235,16 +236,15 @@ def __init__(self, layer, analyses=None): def _run_analysis( self, experiment_data: ExperimentData - ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: + ) -> Tuple[List[Union[AnalysisResultData, ArtifactData]], List[FigureType]]: try: # Run composite analysis and extract sub-experiments results analysis_results, figures = super()._run_analysis(experiment_data) # Calculate single layer fidelity from process fidelities of subsystems - pfs = [res.value for res in analysis_results if res.name == "ProcessFidelity"] + pf_results = [res for res in analysis_results if res.name == "ProcessFidelity"] + pfs = [res.value for res in pf_results] slf = np.prod(pfs) - quality_slf = ( - "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" - ) + quality_slf = "good" if all(sub.quality == "good" for sub in pf_results) else "bad" slf_result = AnalysisResultData( name="SingleLF", value=slf, @@ -292,7 +292,7 @@ def __init__(self, layers, analyses=None): def _run_analysis( self, experiment_data: ExperimentData - ) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]: + ) -> Tuple[List[Union[AnalysisResultData, ArtifactData]], List[FigureType]]: r"""Run analysis for Layer Fidelity experiment. It invokes :meth:`CompositeAnalysis._run_analysis` that will invoke @@ -314,9 +314,10 @@ def _run_analysis( # Run composite analysis and extract sub-experiments results analysis_results, figures = super()._run_analysis(experiment_data) # Calculate full layer fidelity from single layer fidelities - slfs = [res.value for res in analysis_results if res.name == "SingleLF"] + slf_results = [res for res in analysis_results if res.name == "SingleLF"] + slfs = [res.value for res in slf_results] lf = np.prod(slfs) - quality_lf = "good" if all(sub.quality == "good" for sub in analysis_results) else "bad" + quality_lf = "good" if all(sub.quality == "good" for sub in slf_results) else "bad" lf_result = AnalysisResultData( name="LF", value=lf, From 2702fcd384611d4adf2f100f192d36908feb61f3 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Mon, 26 Feb 2024 22:07:24 +0900 Subject: [PATCH 26/38] More informative figure names --- .../library/randomized_benchmarking/layer_fidelity.py | 8 ++++---- .../randomized_benchmarking/layer_fidelity_analysis.py | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 220cb73f4b..e7c7caa087 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -84,9 +84,9 @@ def __init__( Args: physical_qubits: List of physical qubits for the experiment. two_qubit_layers: List of pairs of qubits to run on, will use the direction given here. - lengths: A list of RB sequences lengths. + lengths: A list of layer lengths. backend: The backend to run the experiment on. - num_samples: Number of samples to generate for each sequence length. + num_samples: Number of samples to generate for each layer length. seed: Optional, seed used to initialize ``numpy.random.default_rng``. when generating circuits. The ``default_rng`` will be initialized with this seed value every time :meth:`circuits` is called. @@ -174,8 +174,8 @@ def _default_experiment_options(cls) -> Options: Experiment Options: two_qubit_layers (List[List[Tuple[int, int]]]): List of pairs of qubits to run on. - lengths (List[int]): A list of RB sequences lengths. - num_samples (int): Number of samples to generate for each sequence length. + lengths (List[int]): A list of layer lengths. + num_samples (int): Number of samples to generate for each layer length. seed (None or int or SeedSequence or BitGenerator or Generator): A seed used to initialize ``numpy.random.default_rng`` when generating circuits. The ``default_rng`` will be initialized with this seed value every time diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 836c829e7d..b9fde5874f 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -69,7 +69,10 @@ def __init__(self, physical_qubits): ] ) self._physical_qubits = physical_qubits - self.set_options(outcome="0" * len(physical_qubits)) + self.set_options( + outcome="0" * len(physical_qubits), + figure_names="DirectRB_Q" + "_Q".join(map(str, physical_qubits)) + ".svg", + ) self.plotter.set_figure_options( figure_title=f"Simultaneous Direct RB on Qubit{physical_qubits}", ) From 0393978aaa19b4780710a42e461bf1674591da16 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 20 Mar 2024 01:24:39 +0900 Subject: [PATCH 27/38] Improve docs --- .../randomized_benchmarking/layer_fidelity.py | 35 +++++++++++++------ .../layer_fidelity_analysis.py | 25 +++++++------ 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index e7c7caa087..d1c85b8b78 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -55,11 +55,21 @@ class LayerFidelity(BaseExperiment, RestlessMixin): - """TODO + r"""A holistic benchmarking experiment to characterize the full quality of the devices at scale. # section: overview - - TODO + Layer Fidelity (LF) is a method to estimate the fidelity of + a connecting set of two-qubit gates over :math:`N` qubits by measuring gate errors + using simultaneous direct randomized benchmarking (RB) in disjoint layers. + LF can easily be expressed as a layer size independent quantity, error per layered gate (EPLG): + :math:`EPLG = 1 - LF^{1/N_{2Q}}` where :math:`N_{2Q}` is number of 2-qubit gates in the layers. + + Each of the 2-qubit (or 1-qubit) direct RBs yields the decaying probabilities + to get back to the ground state for an increasing sequence length (i.e. number of layers), + fits the exponential curve to estimate the decay rate, and calculates + the process fidelity of the subsystem from the rate. + LF is calculated as the product of the 2-qubit (or 1-qubit) process fidelities. + See Ref. [1] for details. # section: analysis_ref :class:`LayerFidelityAnalysis` @@ -79,20 +89,25 @@ def __init__( two_qubit_gate: Optional[str] = None, one_qubit_basis_gates: Optional[Sequence[str]] = None, ): - """Initialize a standard randomized benchmarking experiment. + """Initialize a layer fidelity experiment. Args: physical_qubits: List of physical qubits for the experiment. - two_qubit_layers: List of pairs of qubits to run on, will use the direction given here. - lengths: A list of layer lengths. + two_qubit_layers: List of list of qubit pairs where a list of qubit pairs + corresponds with a two-qubit gate layer. Qubit direction matters. + lengths: A list of layer lengths (the number of depth points). backend: The backend to run the experiment on. - num_samples: Number of samples to generate for each layer length. + num_samples: Number of samples (i.e. circuits) to generate for each layer length. seed: Optional, seed used to initialize ``numpy.random.default_rng``. when generating circuits. The ``default_rng`` will be initialized with this seed value every time :meth:`circuits` is called. - two_qubit_gate: Two-qubit gate name (e.g. "cx", "cz", "ecr") + two_qubit_gate: Optional, two-qubit gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. - one_qubit_basis_gates: One-qubit gates to use for implementing 1q Clifford operations. + If not specified (but ``backend is supplied), + one of 2q-gates supported in the backend is automatically set. + one_qubit_basis_gates: Optional, one-qubit gates to use for implementing 1q Clifford operations. + If not specified (but ``backend is supplied), + all 1q-gates supported in the backend are automatically set. Raises: QiskitError: If any invalid argument is supplied. @@ -275,7 +290,7 @@ def circuits(self) -> List[QuantumCircuit]: return list(self.circuits_generator()) def circuits_generator(self) -> Iterable[QuantumCircuit]: - """Generate physical circuits to measure layer fidelity. + """Return a generator of physical circuits to measure layer fidelity. Returns: A generator of :class:`QuantumCircuit`s. diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index b9fde5874f..0ae7da6c6b 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -32,9 +32,10 @@ class _ProcessFidelityAnalysis(curve.CurveAnalysis): r"""A class to estimate process fidelity from one of 1Q/2Q simultaneous direct RB experiments # section: overview - This analysis takes only single series. + This analysis takes only a single series. This series is fit by the exponential decay function. - From the fit :math:`\alpha` value this analysis estimates the process fidelity. + From the fit :math:`\alpha` value this analysis estimates the process fidelity: + .. math:: F = \frac{1+(d^2-1)\alpha}{d^2} # section: fit_model .. math:: @@ -219,13 +220,7 @@ def __evaluate_quality( class _SingleLayerFidelityAnalysis(CompositeAnalysis): - r"""A class to estimate a process fidelity per disjoint layer. - - TODO: Add math. - - # section: reference - .. ref_arxiv:: 1 2311.05933 - """ + """A class to estimate a process fidelity per disjoint layer.""" def __init__(self, layer, analyses=None): if analyses: @@ -278,6 +273,12 @@ def _get_experiment_components(self, experiment_data: ExperimentData): class LayerFidelityAnalysis(CompositeAnalysis): r"""A class to analyze layer fidelity experiments. + # section: overview + It estimates Layer Fidelity and EPLG (error per layered gate) + by fitting the exponential curve to estimate the decay rate, hence the process fidelity, + for each 2-qubit (or 1-qubit) direct randomized benchmarking result. + See Ref. [1] for details. + # section: reference .. ref_arxiv:: 1 2311.05933 """ @@ -298,12 +299,10 @@ def _run_analysis( ) -> Tuple[List[Union[AnalysisResultData, ArtifactData]], List[FigureType]]: r"""Run analysis for Layer Fidelity experiment. - It invokes :meth:`CompositeAnalysis._run_analysis` that will invoke - ``_run_analysis`` for the sub-experiments (1Q/2Q simultaneous direct RBs for each layer). + It invokes :meth:`CompositeAnalysis._run_analysis` that will recursively invoke + ``_run_analysis`` of the sub-experiments (1Q/2Q simultaneous direct RBs for each layer). Based on the results, it computes Layer Fidelity and EPLG (error per layered gate). - TODO: Add math. - Args: experiment_data: the experiment data to analyze. From d208460bf3da99d85e5e2b069f021c6efdec4058 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 20 Mar 2024 02:09:58 +0900 Subject: [PATCH 28/38] fix lint --- .../library/randomized_benchmarking/layer_fidelity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index d1c85b8b78..cf27470261 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -101,11 +101,11 @@ def __init__( seed: Optional, seed used to initialize ``numpy.random.default_rng``. when generating circuits. The ``default_rng`` will be initialized with this seed value every time :meth:`circuits` is called. - two_qubit_gate: Optional, two-qubit gate name (e.g. "cx", "cz", "ecr") + two_qubit_gate: Optional, 2q-gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. If not specified (but ``backend is supplied), one of 2q-gates supported in the backend is automatically set. - one_qubit_basis_gates: Optional, one-qubit gates to use for implementing 1q Clifford operations. + one_qubit_basis_gates: Optional, 1q-gates to use for implementing 1q-Clifford operations. If not specified (but ``backend is supplied), all 1q-gates supported in the backend are automatically set. From bf713dd53bae5bb30c2c3599af8cb4bb0fcdadab Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 22 Mar 2024 12:29:23 +0900 Subject: [PATCH 29/38] Add release note --- .../layer-fidelity-1e09dea9e5b69515.yaml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml diff --git a/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml b/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml new file mode 100644 index 0000000000..c04f6d56d4 --- /dev/null +++ b/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml @@ -0,0 +1,23 @@ +--- +features: + - | + Add a new experiment class :class:`.LayerFidelity` + to measure `layer fidelity `_, + which is a holistic benchmark to characterize the full quality of the devices at scale. + + It has an experimental feature: its :meth:`circuits` + exceptionally returns circuits on physical qubits (not virtual qubits as usual). + Its analysis class :class:`.LayerFidelityAnalysis` returns :class:`.AnalysisResultData` + which contains several ``extra`` entries to help additional analyses: e.g. + ``qubits`` to ease the query of subanalysis results and + ``reason`` to tell users why the ``quality`` of the analysis was ``"bad"``. + + For example, the syntax for pulling out the individual fidelities looks like below. + + .. code-block:: python + + df = exp_data.analysis_results(dataframe=True) + df[(df.name=="ProcessFidelity") & (df.qubits==(59, 60))].value + + See `an example notebook + `_ for more examples. From bacc3da2bbe8e37acafdb95cd48f3369217de74c Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 22 Mar 2024 14:43:20 +0900 Subject: [PATCH 30/38] Add to autosummary --- qiskit_experiments/library/randomized_benchmarking/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiskit_experiments/library/randomized_benchmarking/__init__.py b/qiskit_experiments/library/randomized_benchmarking/__init__.py index ddf1a8289f..6ec3daa157 100644 --- a/qiskit_experiments/library/randomized_benchmarking/__init__.py +++ b/qiskit_experiments/library/randomized_benchmarking/__init__.py @@ -25,6 +25,7 @@ StandardRB InterleavedRB + LayerFidelity Analysis @@ -36,6 +37,7 @@ RBAnalysis InterleavedRBAnalysis + LayerFidelityAnalysis Synthesis ========= From 599afc4c7cb3501ff548f7e9e128666835926cdd Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 22 Mar 2024 17:17:47 +0900 Subject: [PATCH 31/38] Fix bugs in docs --- .../library/randomized_benchmarking/layer_fidelity.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index cf27470261..3b07b6ea85 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -103,10 +103,10 @@ def __init__( with this seed value every time :meth:`circuits` is called. two_qubit_gate: Optional, 2q-gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. - If not specified (but ``backend is supplied), + If not specified (but ``backend`` is supplied), one of 2q-gates supported in the backend is automatically set. one_qubit_basis_gates: Optional, 1q-gates to use for implementing 1q-Clifford operations. - If not specified (but ``backend is supplied), + If not specified (but ``backend`` is supplied), all 1q-gates supported in the backend are automatically set. Raises: @@ -285,7 +285,7 @@ def circuits(self) -> List[QuantumCircuit]: """Return a list of physical circuits to measure layer fidelity. Returns: - A list of :class:`QuantumCircuit`. + A list of :class:`QuantumCircuit`\s. """ return list(self.circuits_generator()) @@ -293,7 +293,7 @@ def circuits_generator(self) -> Iterable[QuantumCircuit]: """Return a generator of physical circuits to measure layer fidelity. Returns: - A generator of :class:`QuantumCircuit`s. + A generator of :class:`QuantumCircuit`\s. """ opts = self.experiment_options residal_qubits_by_layer = [self.__residual_qubits(layer) for layer in opts.two_qubit_layers] From 9e09d8e6a8667a022213c702bc76f928c8ef84c3 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Fri, 22 Mar 2024 17:36:49 +0900 Subject: [PATCH 32/38] Fix lint --- .../library/randomized_benchmarking/layer_fidelity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 3b07b6ea85..ffe9a1f6df 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -282,7 +282,7 @@ def __residual_qubits(self, two_qubit_layer): return [q for q in self.physical_qubits if q not in qubits_in_layer] def circuits(self) -> List[QuantumCircuit]: - """Return a list of physical circuits to measure layer fidelity. + r"""Return a list of physical circuits to measure layer fidelity. Returns: A list of :class:`QuantumCircuit`\s. @@ -290,7 +290,7 @@ def circuits(self) -> List[QuantumCircuit]: return list(self.circuits_generator()) def circuits_generator(self) -> Iterable[QuantumCircuit]: - """Return a generator of physical circuits to measure layer fidelity. + r"""Return a generator of physical circuits to measure layer fidelity. Returns: A generator of :class:`QuantumCircuit`\s. From 9d5ac70120bddbc92ef2cbbca3b36957959d509e Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Mon, 1 Apr 2024 15:54:13 +0900 Subject: [PATCH 33/38] Make the method for evaluating result quality possible to override --- .../randomized_benchmarking/layer_fidelity_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py index 0ae7da6c6b..8508863ad8 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity_analysis.py @@ -147,7 +147,7 @@ def _create_analysis_results( alpha = fit_data.ufloat_params["alpha"] pf = (1 + (d * d - 1) * alpha) / (d * d) - quality, reason = self.__evaluate_quality(fit_data) + quality, reason = self._evaluate_quality_with_reason(fit_data) metadata["qubits"] = self._physical_qubits metadata["reason"] = reason @@ -187,7 +187,7 @@ def _get_experiment_components(self, experiment_data: ExperimentData): """Set physical qubits to the experiment components.""" return [device.Qubit(qubit) for qubit in self._physical_qubits] - def __evaluate_quality( + def _evaluate_quality_with_reason( self, fit_data: curve.CurveFitResult, ) -> Tuple[str, Union[str, None]]: From 38e4636129d6662b74745cc8817617122a04b7d2 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Mon, 8 Apr 2024 17:40:32 +0900 Subject: [PATCH 34/38] Improve docs and reno following reviewer's comments --- .../randomized_benchmarking/layer_fidelity.py | 12 +++++++----- .../notes/layer-fidelity-1e09dea9e5b69515.yaml | 8 +++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index ffe9a1f6df..6dbd80d5c0 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -93,14 +93,15 @@ def __init__( Args: physical_qubits: List of physical qubits for the experiment. - two_qubit_layers: List of list of qubit pairs where a list of qubit pairs - corresponds with a two-qubit gate layer. Qubit direction matters. + two_qubit_layers: List of two-qubit gate layers to run on. Each two-qubit + gate layer must be given as a list of directed qubit pairs. lengths: A list of layer lengths (the number of depth points). - backend: The backend to run the experiment on. + backend: The backend to run the experiment on. Note that either ``backend`` or + ``two_qubit_gate`` and ``one_qubit_basis_gates`` must be set at instantiation. num_samples: Number of samples (i.e. circuits) to generate for each layer length. seed: Optional, seed used to initialize ``numpy.random.default_rng``. when generating circuits. The ``default_rng`` will be initialized - with this seed value every time :meth:`circuits` is called. + with this seed value every time :meth:~.LayerFidelity.circuits` is called. two_qubit_gate: Optional, 2q-gate name (e.g. "cx", "cz", "ecr") of which the two qubit layers consist. If not specified (but ``backend`` is supplied), @@ -188,7 +189,8 @@ def _default_experiment_options(cls) -> Options: """Default experiment options. Experiment Options: - two_qubit_layers (List[List[Tuple[int, int]]]): List of pairs of qubits to run on. + two_qubit_layers (List[List[Tuple[int, int]]]): List of two-qubit gate layers to run on. + Each two-qubit gate layer must be given as a list of directed qubit pairs. lengths (List[int]): A list of layer lengths. num_samples (int): Number of samples to generate for each layer length. seed (None or int or SeedSequence or BitGenerator or Generator): A seed diff --git a/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml b/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml index c04f6d56d4..6ca0fe3183 100644 --- a/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml +++ b/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml @@ -1,8 +1,8 @@ --- features: - | - Add a new experiment class :class:`.LayerFidelity` - to measure `layer fidelity `_, + Add a new experiment class :class:`.LayerFidelity` to measure + `layer fidelity and EPLG (error per layered gate) `_, which is a holistic benchmark to characterize the full quality of the devices at scale. It has an experimental feature: its :meth:`circuits` @@ -20,4 +20,6 @@ features: df[(df.name=="ProcessFidelity") & (df.qubits==(59, 60))].value See `an example notebook - `_ for more examples. + `_ for more examples + such as how to select a best possible qubit chain to measure and + how to plot EPLG as a function of (sub)chain length. \ No newline at end of file From 0c3bd308145771a57b624112497bc20a09ea030e Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Mon, 8 Apr 2024 18:59:33 +0900 Subject: [PATCH 35/38] Cache 2q clifford integer table of the tensor product of 1q clifford integers in a file --- .../randomized_benchmarking/clifford_utils.py | 13 ++++--------- .../data/clifford_tensor_1q.npz | Bin 0 -> 1355 bytes .../data/generate_clifford_data.py | 18 +++++++++++++++--- .../randomized_benchmarking/layer_fidelity.py | 9 ++++++--- 4 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 qiskit_experiments/library/randomized_benchmarking/data/clifford_tensor_1q.npz diff --git a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py index 8dae210f42..666f8b0cc9 100644 --- a/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py +++ b/qiskit_experiments/library/randomized_benchmarking/clifford_utils.py @@ -46,6 +46,7 @@ _valid_sparse_indices = _clifford_compose_2q_data["valid_sparse_indices"] # map a clifford number to the index of _CLIFFORD_COMPOSE_2Q_DENSE _clifford_num_to_dense_index = {idx: ii for ii, idx in enumerate(_valid_sparse_indices)} +_CLIFFORD_TENSOR_1Q = np.load(f"{_DATA_FOLDER}/clifford_tensor_1q.npz")["table"] # Transpilation utilities def _transpile_clifford_circuit( @@ -740,12 +741,6 @@ def _layer_indices_from_num(num: Integral) -> Tuple[Integral, Integral, Integral return idx0, idx1, idx2 -@lru_cache(maxsize=24 * 24) -def _product_1q_nums(first: Integral, second: Integral) -> Integral: - """Return the 2-qubit Clifford integer that represents the product of 1-qubit Cliffords.""" - qc0 = CliffordUtils.clifford_1_qubit_circuit(first) - qc1 = CliffordUtils.clifford_1_qubit_circuit(second) - qc = QuantumCircuit(2) - qc.compose(qc0, qubits=(0,), inplace=True) - qc.compose(qc1, qubits=(1,), inplace=True) - return num_from_2q_circuit(qc) +def _tensor_1q_nums(first: Integral, second: Integral) -> Integral: + """Return the 2-qubit Clifford integer that is the tensor product of 1-qubit Cliffords.""" + return _CLIFFORD_TENSOR_1Q[first, second] diff --git a/qiskit_experiments/library/randomized_benchmarking/data/clifford_tensor_1q.npz b/qiskit_experiments/library/randomized_benchmarking/data/clifford_tensor_1q.npz new file mode 100644 index 0000000000000000000000000000000000000000..7725682f70de413190f39651eba0279b168346d4 GIT binary patch literal 1355 zcmZ{keM}Q)9LL*LoD2y@aQG599hr!zjWWX2(AzXy6LA4!jM>oMkSt`wjS**v?ez}C zH>T1lDk9EvZbp_R>lQPcMX#rz6xU*%Z)Cpo)7 zg@7U|G^!j)E&kL`JxbI{vG+u*i}YBOrAP>o3)K^>AZdxK)ibED({!T}6kri{>mP~4 zc=Cw7fLpoaJp5m6W5S3$10AVVPB4-E_#sLtQU``KIlZ;zQb4p~Ps@9xO9A?DOFkAF z!cX$CM&kNrIS+}Lub||I{bh1C#ns^!K7)3}^Y7WTOQ&4Q4)@`0m^h2@IOI36U=!Yo z#!5_skY*p)svr}I2eeDM$>#(!A*~#6h^yVbkVc2^Ds2r>NJq-wnzW9c9f`Yd+n^8P zP!4?>4&~4XaZR3)wZ+1}4KqN{!q>a6?+5t|SD~~!qcAWpze0xQ;isrrq5GPaGqHx> zMZ0!zRz6lk40g)-2xrDmv$0vkbq|mRf3JCmc8(FA23e1Uc{vk;%AQK0RK4nw zGtkII{2&wEN@Jt;^U}G%Wh?U_(d-cBsqIK~1JPRu4A^iX6pQbxy)Jnr6+URip3y%@ z^>l9BeJ7E}1YgFFF~eo}VKka;x;Z2p*pN{YjoPo80i?6!MQE@?X>rD&*bkPF!D>yMN~@~S kzm#M6zy9}co747Yy|(|?4XY~BrfLs*ILqLvPIkn90J2Q^Qvd(} literal 0 HcmV?d00001 diff --git a/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py b/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py index 7ebd45dcab..5193dc35d8 100644 --- a/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py +++ b/qiskit_experiments/library/randomized_benchmarking/data/generate_clifford_data.py @@ -59,7 +59,7 @@ def _hash_cliff(cliff): def gen_clifford_inverse_1q(): - """Generate table data for integer 1Q Clifford inversion""" + """Generate data for integer 1Q Clifford inversion table""" invs = np.empty(NUM_CLIFFORD_1Q, dtype=int) for i, cliff_i in _CLIFF_1Q.items(): invs[i] = _TO_INT_1Q[_hash_cliff(cliff_i.adjoint())] @@ -68,7 +68,7 @@ def gen_clifford_inverse_1q(): def gen_clifford_compose_1q(): - """Generate table data for integer 1Q Clifford composition.""" + """Generate data for integer 1Q Clifford composition table""" products = np.empty((NUM_CLIFFORD_1Q, NUM_CLIFFORD_1Q), dtype=int) for i, cliff_i in _CLIFF_1Q.items(): for j, cliff_j in _CLIFF_1Q.items(): @@ -83,7 +83,7 @@ def gen_clifford_compose_1q(): def gen_clifford_inverse_2q(): - """Generate table data for integer 2Q Clifford inversion""" + """Generate data for integer 2Q Clifford inversion table""" invs = np.empty(NUM_CLIFFORD_2Q, dtype=int) for i, cliff_i in _CLIFF_2Q.items(): invs[i] = _TO_INT_2Q[_hash_cliff(cliff_i.adjoint())] @@ -191,6 +191,16 @@ def gen_cliff_single_2q_gate_map(): return table +def gen_clifford_tensor_1q(): + """Generate data for 2Q integer Clifford table of the tensor product of 1Q integer Cliffords.""" + products = np.empty((NUM_CLIFFORD_1Q, NUM_CLIFFORD_1Q), dtype=int) + for i, cliff_i in _CLIFF_1Q.items(): + for j, cliff_j in _CLIFF_1Q.items(): + cliff = cliff_i.tensor(cliff_j) + products[i, j] = _TO_INT_2Q[_hash_cliff(cliff)] + return products + + if __name__ == "__main__": if _CLIFF_SINGLE_GATE_MAP_1Q != gen_cliff_single_1q_gate_map(): raise Exception( @@ -212,3 +222,5 @@ def gen_cliff_single_2q_gate_map(): table=_CLIFFORD_COMPOSE_2Q_DENSE, valid_sparse_indices=valid_sparse_indices, ) + + np.savez_compressed("clifford_tensor_1q.npz", table=gen_clifford_tensor_1q()) diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 6dbd80d5c0..40c5247f87 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -40,7 +40,7 @@ inverse_1q, inverse_2q, num_from_2q_circuit, - _product_1q_nums, + _tensor_1q_nums, _clifford_1q_int_to_instruction, _clifford_2q_int_to_instruction, _decompose_clifford_ops, @@ -403,8 +403,11 @@ def __circuit_body( for j, qpair in enumerate(two_qubit_layer): # sample product of two 1q-Cliffords as 2q interger Clifford samples = rng.integers(NUM_1Q_CLIFFORD, size=2) - cliffs_2q[j] = compose_2q(cliffs_2q[j], _product_1q_nums(*samples)) - for sample, q in zip(samples, qpair): + cliffs_2q[j] = compose_2q(cliffs_2q[j], _tensor_1q_nums(*samples)) + # For Clifford 1 (x) Clifford 2, in its circuit representation, + # Clifford 1 acts on the 2nd qubit and Clifford 2 acts on the 1st qubit. + # That's why the qpair is reversed here. + for sample, q in zip(samples, reversed(qpair)): circ._append(_to_gate_1q(sample), (circ.qubits[q],), ()) for k, q in enumerate(one_qubits): sample = rng.integers(NUM_1Q_CLIFFORD) From ec3f838f276d5a3e7aa36c4bb76fac318caf6255 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko <15028342+itoko@users.noreply.github.com> Date: Mon, 22 Apr 2024 23:46:50 +0900 Subject: [PATCH 36/38] Update URL link in release note --- releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml b/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml index 6ca0fe3183..278afcad93 100644 --- a/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml +++ b/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml @@ -20,6 +20,6 @@ features: df[(df.name=="ProcessFidelity") & (df.qubits==(59, 60))].value See `an example notebook - `_ for more examples + `_ for more examples such as how to select a best possible qubit chain to measure and how to plot EPLG as a function of (sub)chain length. \ No newline at end of file From fafc2330d09e4aa6a7e6975f880175101c7aed1d Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 1 May 2024 00:15:25 +0900 Subject: [PATCH 37/38] fix url link in reno --- releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml b/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml index 278afcad93..b72f42a615 100644 --- a/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml +++ b/releasenotes/notes/layer-fidelity-1e09dea9e5b69515.yaml @@ -20,6 +20,6 @@ features: df[(df.name=="ProcessFidelity") & (df.qubits==(59, 60))].value See `an example notebook - `_ for more examples - such as how to select a best possible qubit chain to measure and + `_ + for more examples such as how to select a best possible qubit chain to measure and how to plot EPLG as a function of (sub)chain length. \ No newline at end of file From bc4e3c4c432cc44797da3286547fbd5405a3fcf6 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko Date: Wed, 1 May 2024 09:23:49 +0900 Subject: [PATCH 38/38] try to fix docs CI failure caused by the new Aer release --- docs/manuals/measurement/readout_mitigation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manuals/measurement/readout_mitigation.rst b/docs/manuals/measurement/readout_mitigation.rst index cc145b47f1..1a6b8d54d7 100644 --- a/docs/manuals/measurement/readout_mitigation.rst +++ b/docs/manuals/measurement/readout_mitigation.rst @@ -106,7 +106,7 @@ Mitigation example .. jupyter-execute:: qc = QuantumCircuit(num_qubits) - qc.h(0) + qc.sx(0) for i in range(1, num_qubits): qc.cx(i - 1, i) qc.measure_all()