Source code for qiskit_experiments.library.randomized_benchmarking.purity_rb_analysis

# This code is part of Qiskit.
#
# (C) Copyright IBM 2024.
#
# 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.
"""
Purity RB analysis class.
"""

from typing import List, Dict, Union

from qiskit.result import sampled_expectation_value

from qiskit_experiments.curve_analysis import ScatterTable
import qiskit_experiments.curve_analysis as curve
from qiskit_experiments.framework import AnalysisResultData
from .rb_analysis import RBAnalysis, _calculate_epg, _exclude_1q_error


[docs] class PurityRBAnalysis(RBAnalysis): r"""A class to analyze purity randomized benchmarking experiments. # section: overview This analysis extends :class:`RBAnalysis` to handle purity-based randomized benchmarking experiments. This analysis computes the purity :math:`\text{Tr}(\rho^2)` from multiple measurement bases (3 for 1Q, 9 for 2Q). The purity values are then fit to the same exponential decay model as standard RB. The Error Per Clifford (EPC) is extracted from the fit parameter :math:`\alpha` using :math:`\text{EPC} = \frac{2^n - 1}{2^n}(1 - \sqrt{\alpha})`. See :class:`~qiskit_experiments.library.randomized_benchmarking.rb_analysis.RBAnalysis` for the standard RB analysis. # 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: Squared depolarizing parameter (compared to standard RB). init_guess: Determined by :func:`~.guess.rb_decay` and squared. bounds: [0, 1] """ # Removed __init__ as it only calls super().__init__() with no additional logic def _run_data_processing( self, raw_data: List[Dict], category: str = "raw", ) -> ScatterTable: """Perform data processing from the experiment result payload. For purity this converts the counts into Trace(rho^2) and then runs the rest of the standard RB fitters For now this does it by spoofing a new counts dictionary and then calling the super _run_data_processing Args: raw_data: Payload in the experiment data. category: Category string of the output dataset. Returns: Processed data that will be sent to the formatter method. Raises: DataProcessorError: When key for x values is not found in the metadata. ValueError: When data processor is not provided. """ # figure out the number of qubits... has to be 1 or 2 for now if self.options.outcome == "0": nq = 1 elif self.options.outcome == "00": nq = 2 else: raise ValueError("Only supporting 1 or 2Q purity") ntrials = int(len(raw_data) / 3**nq) raw_data2 = [] nshots = int(sum(raw_data[0]["counts"].values())) for i in range(ntrials): trial_raw = [d for d in raw_data if d["metadata"]["trial"] == i] raw_data2.append(trial_raw[0]) purity = 1 / 2**nq if nq == 1: for ii in range(3): purity += sampled_expectation_value(trial_raw[ii]["counts"], "Z") ** 2 / 2**nq else: for ii in range(9): purity += sampled_expectation_value(trial_raw[ii]["counts"], "ZZ") ** 2 / 2**nq purity += ( sampled_expectation_value(trial_raw[ii]["counts"], "IZ") ** 2 / 2**nq / 3 ** (nq - 1) ) purity += ( sampled_expectation_value(trial_raw[ii]["counts"], "ZI") ** 2 / 2**nq / 3 ** (nq - 1) ) raw_data2[-1]["counts"] = { "0" * nq: int(purity * nshots * 10), "1" * nq: int((1 - purity) * nshots * 10), } return super()._run_data_processing(raw_data2, category) 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 = curve.CurveAnalysis._create_analysis_results(self, fit_data, quality, **metadata) num_qubits = len(self._physical_qubits) # Calculate EPC # For purity we need to correct by alpha = fit_data.ufloat_params["alpha"] ** 0.5 scale = (2**num_qubits - 1) / (2**num_qubits) epc = scale * (1 - alpha) outcomes.append( AnalysisResultData( name="EPC_pur", value=epc, chisq=fit_data.reduced_chisq, quality=quality, extra=metadata, ) ) # Correction for 1Q depolarizing channel if EPGs are provided if self.options.epg_1_qubit and num_qubits == 2: epc = _exclude_1q_error( epc=epc, qubits=self._physical_qubits, gate_counts_per_clifford=self._gate_counts_per_clifford, extra_analyses=self.options.epg_1_qubit, ) outcomes.append( AnalysisResultData( name="EPC_pur_corrected", value=epc, chisq=fit_data.reduced_chisq, quality=quality, extra=metadata, ) ) # Calculate EPG if self._gate_counts_per_clifford is not None and self.options.gate_error_ratio: epg_dict = _calculate_epg( epc=epc, qubits=self._physical_qubits, gate_error_ratio=self.options.gate_error_ratio, gate_counts_per_clifford=self._gate_counts_per_clifford, ) if epg_dict: for gate, epg_val in epg_dict.items(): outcomes.append( AnalysisResultData( name=f"EPG_pur_{gate}", value=epg_val, chisq=fit_data.reduced_chisq, quality=quality, extra=metadata, ) ) return outcomes 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) if len(curve_data.x) > 3: alpha_guess = curve.guess.rb_decay(curve_data.x[0:3], curve_data.y[0:3], b=b_guess) else: alpha_guess = curve.guess.rb_decay(curve_data.x, curve_data.y, b=b_guess) alpha_guess = alpha_guess**2 if alpha_guess < 0.6: a_guess = curve_data.y[0] - b_guess else: 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