class FineAmplitudeAnalysis(ErrorAmplificationAnalysis): r"""An analysis class for fine amplitude calibrations to define the fixed parameters. # section: note The following parameters are fixed. * :math:`{\rm apg}` The angle per gate is set by the user, for example pi for a pi-pulse. * :math:`{\rm phase\_offset}` The phase offset in the cosine oscillation, for example, :math:`\pi/2` if a square-root of X gate is added before the repeated gates. """ # The intended angle per gat of the gate being calibrated, e.g. pi for a pi-pulse. __series__ = [ curve.SeriesDef( # pylint: disable=line-too-long fit_func=lambda x, amp, d_theta, phase_offset, base, angle_per_gate: base + 0.5 * amp * (2 * x - 1), plot_color="green", model_description=r"{\rm base} + \frac{{\rm amp}}{2} * (2 * x - 1)", name="spam cal.", filter_kwargs={"series": "spam-cal"}, ), curve.SeriesDef( # pylint: disable=line-too-long fit_func=lambda x, amp, d_theta, phase_offset, base, angle_per_gate: curve.fit_function.cos( x, amp=0.5 * amp, freq=(d_theta + angle_per_gate) / (2 * np.pi), phase=-phase_offset, baseline=base, ), plot_color="blue", model_description=r"\frac{{\rm amp}}{2}\cos\left(x[{\rm d}\theta + {\rm apg} ] " r"+ {\rm phase\_offset}\right)+{\rm base}", name="fine amp.", filter_kwargs={"series": 1}, ), ]
class ResonanceAnalysis(curve.CurveAnalysis): r"""A class to analyze a resonance, typically seen as a peak. Overview This analysis takes only single series. This series is fit by the Gaussian function. Fit Model The fit is based on the following Gaussian function. .. math:: F(x) = a \exp(-(x-f)^2/(2\sigma^2)) + b Fit Parameters - :math:`a`: Peak height. - :math:`b`: Base line. - :math:`f`: Center frequency. This is the fit parameter of main interest. - :math:`\sigma`: Standard deviation of Gaussian function. Initial Guesses - :math:`a`: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.max_height`. - :math:`b`: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.\ constant_spectral_offset`. - :math:`f`: Frequency at max height position calculated by :func:`~qiskit_experiments.curve_analysis.guess.max_height`. - :math:`\sigma`: Calculated from FWHM of peak :math:`w` such that :math:`w / \sqrt{8} \ln{2}`, where FWHM is calculated by :func:`~qiskit_experiments.curve_analysis.guess.full_width_half_max`. Bounds - :math:`a`: [-2, 2] scaled with maximum signal value. - :math:`b`: [-1, 1] scaled with maximum signal value. - :math:`f`: [min(x), max(x)] of frequency scan range. - :math:`\sigma`: [0, :math:`\Delta x`] where :math:`\Delta x` represents frequency scan range. """ __series__ = [ curve.SeriesDef( fit_func=lambda x, a, sigma, freq, b: curve.fit_function.gaussian( x, amp=a, sigma=sigma, x0=freq, baseline=b), plot_color="blue", model_description=r"a \exp(-(x-f)^2/(2\sigma^2)) + b", ) ] @classmethod def _default_options(cls) -> Options: options = super()._default_options() options.result_parameters = [curve.ParameterRepr("freq", "f01", "Hz")] options.normalization = True options.xlabel = "Frequency" options.ylabel = "Signal (arb. units)" options.xval_unit = "Hz" return options def _generate_fit_guesses( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Compute the initial guesses. Args: user_opt: Fit options filled with user provided guess and bounds. Returns: List of fit options that are passed to the fitter function. """ curve_data = self._data() max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True) user_opt.bounds.set_if_empty( a=(-2 * max_abs_y, 2 * max_abs_y), sigma=(0, np.ptp(curve_data.x)), freq=(min(curve_data.x), max(curve_data.x)), b=(-max_abs_y, max_abs_y), ) user_opt.p0.set_if_empty( b=curve.guess.constant_spectral_offset(curve_data.y)) y_ = curve_data.y - user_opt.p0["b"] _, peak_idx = curve.guess.max_height(y_, absolute=True) fwhm = curve.guess.full_width_half_max(curve_data.x, y_, peak_idx) user_opt.p0.set_if_empty( a=curve_data.y[peak_idx] - user_opt.p0["b"], freq=curve_data.x[peak_idx], sigma=fwhm / np.sqrt(8 * np.log(2)), ) return user_opt def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - a reduced chi-squared less than 3, - a peak within the scanned frequency range, - a standard deviation that is not larger than the scanned frequency range, - a standard deviation that is wider than the smallest frequency increment, - a signal-to-noise ratio, defined as the amplitude of the peak divided by the square root of the median y-value less the fit offset, greater than a threshold of two, and - a standard error on the sigma of the Gaussian that is smaller than the sigma. """ curve_data = self._data() max_freq = np.max(curve_data.x) min_freq = np.min(curve_data.x) freq_increment = np.mean(np.diff(curve_data.x)) fit_a = fit_data.fitval("a").value fit_b = fit_data.fitval("b").value fit_freq = fit_data.fitval("freq").value fit_sigma = fit_data.fitval("sigma").value fit_sigma_err = fit_data.fitval("sigma").stderr snr = abs(fit_a) / np.sqrt(abs(np.median(curve_data.y) - fit_b)) fit_width_ratio = fit_sigma / (max_freq - min_freq) criteria = [ min_freq <= fit_freq <= max_freq, 1.5 * freq_increment < fit_sigma, fit_width_ratio < 0.25, fit_data.reduced_chisq < 3, (fit_sigma_err is None or fit_sigma_err < fit_sigma), snr > 2, ] if all(criteria): return "good" return "bad"
class InterleavedRBAnalysis(RBAnalysis): r"""A class to analyze interleaved randomized benchmarking experiment. # section: overview This analysis takes only two series for standard and interleaved RB curve fitting. From the fit :math:`\alpha` and :math:`\alpha_c` value this analysis estimates the error per Clifford (EPC) of the interleaved gate. The EPC estimate is obtained using the equation .. math:: r_{\mathcal{C}}^{\text{est}} = \frac{\left(d-1\right)\left(1-\alpha_{\overline{\mathcal{C}}}/\alpha\right)}{d} The systematic error bounds are given by .. math:: E = \min\left\{ \begin{array}{c} \frac{\left(d-1\right)\left[\left|\alpha-\alpha_{\overline{\mathcal{C}}}\right| +\left(1-\alpha\right)\right]}{d} \\ \frac{2\left(d^{2}-1\right)\left(1-\alpha\right)} {\alpha d^{2}}+\frac{4\sqrt{1-\alpha}\sqrt{d^{2}-1}}{\alpha} \end{array} \right. See Ref. [1] for more details. # section: fit_model The fit is based on the following decay functions: Fit model for standard RB .. math:: F(x) = a \alpha^{x} + b Fit model for interleaved RB .. math:: F(x) = a (\alpha_c \alpha)^{x_2} + b # section: fit_parameters defpar a: desc: Height of decay curve. init_guess: Determined by the average :math:`a` of the standard and interleaved RB. bounds: [0, 1] defpar b: desc: Base line. init_guess: Determined by the average :math:`b` of the standard and interleaved RB. Usually equivalent to :math:`(1/2)^n` where :math:`n` is number of qubit. bounds: [0, 1] defpar \alpha: desc: Depolarizing parameter. init_guess: Determined by the slope of :math:`(y - b)^{-x}` of the first and the second data point of the standard RB. bounds: [0, 1] defpar \alpha_c: desc: Ratio of the depolarizing parameter of interleaved RB to standard RB curve. init_guess: Estimate :math:`\alpha' = \alpha_c \alpha` from the interleaved RB curve, then divide this by the initial guess of :math:`\alpha`. bounds: [0, 1] # section: reference .. ref_arxiv:: 1 1203.4550 """ __series__ = [ curve.SeriesDef( name="Standard", fit_func=lambda x, a, alpha, alpha_c, b: curve.fit_function.exponential_decay( x, amp=a, lamb=-1.0, base=alpha, baseline=b ), filter_kwargs={"interleaved": False}, plot_color="red", plot_symbol=".", model_description=r"a \alpha^{x} + b", ), curve.SeriesDef( name="Interleaved", fit_func=lambda x, a, alpha, alpha_c, b: curve.fit_function.exponential_decay( x, amp=a, lamb=-1.0, base=alpha * alpha_c, baseline=b ), filter_kwargs={"interleaved": True}, plot_color="orange", plot_symbol="^", model_description=r"a (\alpha_c\alpha)^{x} + b", ), ] @classmethod def _default_options(cls): """Default analysis options.""" default_options = super()._default_options() default_options.result_parameters = ["alpha", "alpha_c"] return default_options def _generate_fit_guesses( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Compute the initial guesses. Args: user_opt: Fit options filled with user provided guess and bounds. Returns: List of fit options that are passed to the fitter function. """ user_opt.bounds.set_if_empty( a=(0, 1), alpha=(0, 1), alpha_c=(0, 1), b=(0, 1), ) # for standard RB curve std_curve = self._data(series_name="Standard") opt_std = user_opt.copy() opt_std = self._initial_guess(opt_std, std_curve.x, std_curve.y, self._num_qubits) # for interleaved RB curve int_curve = self._data(series_name="Interleaved") opt_int = user_opt.copy() if opt_int.p0["alpha_c"] is not None: opt_int.p0["alpha"] = opt_std.p0["alpha"] * opt_int.p0["alpha_c"] opt_int = self._initial_guess(opt_int, int_curve.x, int_curve.y, self._num_qubits) user_opt.p0.set_if_empty( a=np.mean([opt_std.p0["a"], opt_int.p0["a"]]), alpha=opt_std.p0["alpha"], alpha_c=min(opt_int.p0["alpha"] / opt_std.p0["alpha"], 1), b=np.mean([opt_std.p0["b"], opt_int.p0["b"]]), ) return user_opt def _extra_database_entry(self, fit_data: curve.FitData) -> List[AnalysisResultData]: """Calculate EPC.""" nrb = 2**self._num_qubits scale = (nrb - 1) / nrb alpha = fit_data.fitval("alpha") alpha_c = fit_data.fitval("alpha_c") # Calculate epc_est (=r_c^est) - Eq. (4): epc = scale * (1 - alpha_c) # Calculate the systematic error bounds - Eq. (5): systematic_err_1 = scale * (abs(alpha.n - alpha_c.n) + (1 - alpha.n)) systematic_err_2 = ( 2 * (nrb * nrb - 1) * (1 - alpha.n) / (alpha.n * nrb * nrb) + 4 * (np.sqrt(1 - alpha.n)) * (np.sqrt(nrb * nrb - 1)) / alpha.n ) systematic_err = min(systematic_err_1, systematic_err_2) systematic_err_l = epc.n - systematic_err systematic_err_r = epc.n + systematic_err extra_data = AnalysisResultData( name="EPC", value=epc, chisq=fit_data.reduced_chisq, quality=self._evaluate_quality(fit_data), extra={ "EPC_systematic_err": systematic_err, "EPC_systematic_bounds": [max(systematic_err_l, 0), systematic_err_r], }, ) return [extra_data]
class ResonanceAnalysis(curve.CurveAnalysis): r"""A class to analyze a resonance peak with a square rooted Lorentzian function. Overview This analysis takes only single series. This series is fit to the square root of a Lorentzian function. Fit Model The fit is based on the following Lorentzian function. .. math:: F(x) = a{\rm abs}\left(\frac{1}{1 + 2i(x - x0)/\kappa}\right) + b Fit Parameters - :math:`a`: Peak height. - :math:`b`: Base line. - :math:`x0`: Center value. This is typically the fit parameter of interest. - :math:`\kappa`: Linewidth. Initial Guesses - :math:`a`: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.max_height`. - :math:`b`: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.\ constant_spectral_offset`. - :math:`x0`: The max height position is calculated by the function :func:`~qiskit_experiments.curve_analysis.guess.max_height`. - :math:`\kappa`: Calculated from FWHM of the peak using :func:`~qiskit_experiments.curve_analysis.guess.full_width_half_max`. Bounds - :math:`a`: [-2, 2] scaled with maximum signal value. - :math:`b`: [-1, 1] scaled with maximum signal value. - :math:`f`: [min(x), max(x)] of x-value scan range. - :math:`\kappa`: [0, :math:`\Delta x`] where :math:`\Delta x` represents the x-value scan range. """ __series__ = [ curve.SeriesDef( fit_func=lambda x, a, kappa, freq, b: curve.fit_function. sqrt_lorentzian(x, amp=a, kappa=kappa, x0=freq, baseline=b), plot_color="blue", model_description= r"a |\kappa| / sqrt(kappa^2 + 4 * (x - x_0)^2) + b", ) ] @classmethod def _default_options(cls) -> Options: options = super()._default_options() options.curve_drawer.set_options( xlabel="Frequency", ylabel="Signal (arb. units)", xval_unit="Hz", ) options.result_parameters = [curve.ParameterRepr("freq", "f01", "Hz")] options.normalization = True return options def _generate_fit_guesses( self, user_opt: curve.FitOptions, curve_data: curve.CurveData, ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Create algorithmic guess with 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. """ max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True) user_opt.bounds.set_if_empty( a=(-2 * max_abs_y, 2 * max_abs_y), kappa=(0, np.ptp(curve_data.x)), freq=(min(curve_data.x), max(curve_data.x)), b=(-max_abs_y, max_abs_y), ) user_opt.p0.set_if_empty( b=curve.guess.constant_spectral_offset(curve_data.y)) y_ = curve_data.y - user_opt.p0["b"] _, peak_idx = curve.guess.max_height(y_, absolute=True) fwhm = curve.guess.full_width_half_max(curve_data.x, y_, peak_idx) user_opt.p0.set_if_empty( a=(curve_data.y[peak_idx] - user_opt.p0["b"]), freq=curve_data.x[peak_idx], kappa=fwhm, ) return user_opt def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - a reduced chi-squared less than 3, - a peak within the scanned frequency range, - a standard deviation that is not larger than the scanned frequency range, - a standard deviation that is wider than the smallest frequency increment, - a signal-to-noise ratio, defined as the amplitude of the peak divided by the square root of the median y-value less the fit offset, greater than a threshold of two, and - a standard error on the kappa of the Lorentzian that is smaller than the kappa. """ freq_increment = np.mean(np.diff(fit_data.x_data)) fit_a = fit_data.fitval("a") fit_b = fit_data.fitval("b") fit_freq = fit_data.fitval("freq") fit_kappa = fit_data.fitval("kappa") snr = abs(fit_a.n) / np.sqrt(abs(np.median(fit_data.y_data) - fit_b.n)) fit_width_ratio = fit_kappa.n / np.ptp(fit_data.x_data) criteria = [ fit_data.x_range[0] <= fit_freq.n <= fit_data.x_range[1], 1.5 * freq_increment < fit_kappa.n, fit_width_ratio < 0.25, fit_data.reduced_chisq < 3, curve.is_error_not_significant(fit_kappa), snr > 2, ] if all(criteria): return "good" return "bad"
class RamseyXYAnalysis(curve.CurveAnalysis): r"""The Ramsey XY analysis is based on a fit to a cosine function and a sine function. # section: fit_model Analyse a Ramsey XY experiment by fitting the X and Y series to a cosine and sine function, respectively. The two functions share the frequency and amplitude parameters (i.e. beta). .. math:: y_X = {\rm amp}e^{x/\tau}\cos\left(2\pi\cdot{\rm freq}_i\cdot x\right) + {\rm base} y_Y = {\rm amp}e^{x/\tau}\sin\left(2\pi\cdot{\rm freq}_i\cdot x\right) + {\rm base} # section: fit_parameters defpar \rm amp: desc: Amplitude of both series. init_guess: The maximum y value less the minimum y value. 0.5 is also tried. bounds: [-2, 2] scaled to the maximum signal value. defpar \tau: desc: The exponential decay of the curve. init_guess: The initial guess is obtained by fitting an exponential to the square root of (X data)**2 + (Y data)**2. bounds: [0, inf]. defpar \rm base: desc: Base line of both series. init_guess: The average of the data. 0.5 is also tried. bounds: [-1, 1] scaled to the maximum signal value. defpar \rm freq: desc: Frequency of both series. This is the parameter of interest. init_guess: The frequency with the highest power spectral density. bounds: [0, inf]. defpar \rm phase: desc: Common phase offset. init_guess: Linearly spaced between the maximum and minimum scanned beta. bounds: [-min scan range, max scan range]. """ __series__ = [ curve.SeriesDef( fit_func=lambda x, amp, tau, freq, base, phase: fit_function.cos_decay( x, amp=amp, tau=tau, freq=freq, phase=phase, baseline=base ), plot_color="blue", name="X", filter_kwargs={"series": "X"}, plot_symbol="o", model_description=r"{\rm amp} e^{-x/\tau} \cos\left(2 \pi\cdot {\rm freq}\cdot x " r"+ {\rm phase}) + {\rm base}", ), curve.SeriesDef( fit_func=lambda x, amp, tau, freq, base, phase: fit_function.sin_decay( x, amp=amp, tau=tau, freq=freq, phase=phase, baseline=base ), plot_color="green", name="Y", filter_kwargs={"series": "Y"}, plot_symbol="^", model_description=r"{\rm amp} e^{-x/\tau} \sin\left(2 \pi\cdot {\rm freq}\cdot x " r"+ {\rm phase}\right) + {\rm base}", ), ] @classmethod def _default_options(cls): """Return the default analysis options. See :meth:`~qiskit_experiment.curve_analysis.CurveAnalysis._default_options` for descriptions of analysis options. """ default_options = super()._default_options() default_options.curve_drawer.set_options( xlabel="Delay", ylabel="Signal (arb. units)", xval_unit="s", ) default_options.result_parameters = ["freq"] return default_options def _generate_fit_guesses( self, user_opt: curve.FitOptions, curve_data: curve.CurveData, ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Create algorithmic guess with 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. """ max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True) user_opt.bounds.set_if_empty( amp=(-2 * max_abs_y, 2 * max_abs_y), tau=(0, np.inf), base=(-max_abs_y, max_abs_y), phase=(-np.pi, np.pi), ) # Default guess values freq_guesses, base_guesses = [], [] for series in ["X", "Y"]: data = curve_data.get_subset_of(series) freq_guesses.append(curve.guess.frequency(data.x, data.y)) base_guesses.append(curve.guess.constant_sinusoidal_offset(data.y)) freq_val = float(np.average(freq_guesses)) user_opt.p0.set_if_empty(base=np.average(base_guesses)) # Guess the exponential decay by combining both curves data_x = curve_data.get_subset_of("X") data_y = curve_data.get_subset_of("Y") decay_data = (data_x.y - user_opt.p0["base"]) ** 2 + (data_y.y - user_opt.p0["base"]) ** 2 user_opt.p0.set_if_empty( tau=-curve.guess.exp_decay(data_x.x, decay_data), amp=0.5, phase=0.0, ) opt_fp = user_opt.copy() opt_fp.p0.set_if_empty(freq=freq_val) opt_fm = user_opt.copy() opt_fm.p0.set_if_empty(freq=-freq_val) return [opt_fp, opt_fm] def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - a reduced chi-squared lower than three, - an error on the frequency smaller than the frequency. """ fit_freq = fit_data.fitval("freq") criteria = [ fit_data.reduced_chisq < 3, curve.is_error_not_significant(fit_freq), ] if all(criteria): return "good" return "bad"
class DragCalAnalysis(curve.CurveAnalysis): r"""Drag calibration analysis based on a fit to a cosine function. # section: fit_model Analyse a Drag calibration experiment by fitting three series each to a cosine function. The three functions share the phase parameter (i.e. beta) but each have their own amplitude, baseline, and frequency parameters (which therefore depend on the number of repetitions of xp-xm). Several initial guesses are tried if the user does not provide one. .. math:: y_i = {\rm amp} \cos\left(2 \pi\cdot {\rm freq}_i\cdot x - 2 \pi\cdot {\rm freq}_i\cdot \beta\right) + {\rm base} Note that the aim of the Drag calibration is to find the :math:`\beta` that minimizes the phase shifts. This implies that the optimal :math:`\beta` occurs when all three :math:`y` curves are minimum, i.e. they produce the ground state. Therefore, .. math:: y_i = 0 \quad \Longrightarrow \quad -{\rm amp} \cos(2 \pi\cdot X_i) = {\rm base} Here, we abbreviated :math:`{\rm freq}_i\cdot x - {\rm freq}_i\cdot \beta` by :math:`X_i`. For a signal between 0 and 1 the :math:`{\rm base}` will typically fit to 0.5. However, the equation has an ambiguity if the amplitude is not properly bounded. Indeed, - if :math:`{\rm amp} < 0` then we require :math:`2 \pi\cdot X_i = 0` mod :math:`2\pi`, and - if :math:`{\rm amp} > 0` then we require :math:`2 \pi\cdot X_i = \pi` mod :math:`2\pi`. This will result in an ambiguity in :math:`\beta` which we avoid by bounding the amplitude from above by 0. # section: fit_parameters defpar \rm amp: desc: Amplitude of all series. init_guess: The maximum y value less the minimum y value. 0.5 is also tried. bounds: [-2, 2] scaled to the maximum signal value. defpar \rm base: desc: Base line of all series. init_guess: The average of the data. 0.5 is also tried. bounds: [-1, 1] scaled to the maximum signal value. defpar {\rm freq}_i: desc: Frequency of the :math:`i` th oscillation. init_guess: The frequency with the highest power spectral density. bounds: [0, inf]. defpar \beta: desc: Common beta offset. This is the parameter of interest. init_guess: Linearly spaced between the maximum and minimum scanned beta. bounds: [-min scan range, max scan range]. """ __series__ = [ curve.SeriesDef( fit_func=lambda x, amp, freq0, freq1, freq2, beta, base: cos( x, amp=amp, freq=freq0, phase=-2 * np.pi * freq0 * beta, baseline=base ), plot_color="blue", name="series-0", filter_kwargs={"series": 0}, plot_symbol="o", model_description=r"{\rm amp} \cos\left(2 \pi\cdot {\rm freq}_0\cdot x " r"- 2 \pi\cdot {\rm freq}_0\cdot \beta\right) + {\rm base}", ), curve.SeriesDef( fit_func=lambda x, amp, freq0, freq1, freq2, beta, base: cos( x, amp=amp, freq=freq1, phase=-2 * np.pi * freq1 * beta, baseline=base ), plot_color="green", name="series-1", filter_kwargs={"series": 1}, plot_symbol="^", model_description=r"{\rm amp} \cos\left(2 \pi\cdot {\rm freq}_1\cdot x " r"- 2 \pi\cdot {\rm freq}_1\cdot \beta\right) + {\rm base}", ), curve.SeriesDef( fit_func=lambda x, amp, freq0, freq1, freq2, beta, base: cos( x, amp=amp, freq=freq2, phase=-2 * np.pi * freq2 * beta, baseline=base ), plot_color="red", name="series-2", filter_kwargs={"series": 2}, plot_symbol="v", model_description=r"{\rm amp} \cos\left(2 \pi\cdot {\rm freq}_2\cdot x " r"- 2 \pi\cdot {\rm freq}_2\cdot \beta\right) + {\rm base}", ), ] @classmethod def _default_options(cls): """Return the default analysis options. See :meth:`~qiskit_experiment.curve_analysis.CurveAnalysis._default_options` for descriptions of analysis options. """ default_options = super()._default_options() default_options.result_parameters = ["beta"] default_options.xlabel = "Beta" default_options.ylabel = "Signal (arb. units)" return default_options def _generate_fit_guesses( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Compute the initial guesses. Args: user_opt: Fit options filled with user provided guess and bounds. Returns: List of fit options that are passed to the fitter function. """ # Use a fast Fourier transform to guess the frequency. x_data = self._data("series-0").x min_beta, max_beta = min(x_data), max(x_data) freqs_guesses = {} for i in range(3): curve_data = self._data(f"series-{i}") freqs_guesses[f"freq{i}"] = curve.guess.frequency(curve_data.x, curve_data.y) user_opt.p0.set_if_empty(**freqs_guesses) max_abs_y, _ = curve.guess.max_height(self._data().y, absolute=True) freq_bound = max(10 / user_opt.p0["freq0"], max(x_data)) user_opt.bounds.set_if_empty( amp=(-2 * max_abs_y, 0), freq0=(0, np.inf), freq1=(0, np.inf), freq2=(0, np.inf), beta=(-freq_bound, freq_bound), base=(-max_abs_y, max_abs_y), ) user_opt.p0.set_if_empty(base=0.5) # Drag curves can sometimes be very flat, i.e. averages of y-data # and min-max do not always make good initial guesses. We therefore add # 0.5 to the initial guesses. Note that we also set amp=-0.5 because the cosine function # becomes +1 at zero phase, i.e. optimal beta, in which y data should become zero # in discriminated measurement level. options = [] for amp_guess in (0.5, -0.5): for beta_guess in np.linspace(min_beta, max_beta, 20): new_opt = user_opt.copy() new_opt.p0.set_if_empty(amp=amp_guess, beta=beta_guess) options.append(new_opt) return options def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - a reduced chi-squared lower than three, - a DRAG parameter value within the first period of the lowest number of repetitions, - an error on the drag beta smaller than the beta. """ fit_beta = fit_data.fitval("beta").value fit_beta_err = fit_data.fitval("beta").stderr fit_freq0 = fit_data.fitval("freq0").value criteria = [ fit_data.reduced_chisq < 3, fit_beta < 1 / fit_freq0, fit_beta_err < abs(fit_beta), ] if all(criteria): return "good" return "bad"
class DragCalAnalysis(curve.CurveAnalysis): r"""Drag calibration analysis based on a fit to a cosine function. # section: fit_model Analyse a Drag calibration experiment by fitting three series each to a cosine function. The three functions share the phase parameter (i.e. beta), amplitude, and baseline. The frequencies of the oscillations are related through the number of repetitions of the Drag gates. Several initial guesses are tried if the user does not provide one. The fit function is .. math:: y_i = {\rm amp} \cos\left(2 \pi\cdot {\rm reps}_i \cdot {\rm freq}\cdot x - 2 \pi\cdot {\rm reps}_i \cdot {\rm freq}\cdot \beta\right) + {\rm base} Here, the fit parameter :math:`freq` is the frequency of the oscillation of a single pair of Drag plus and minus rotations and :math:`{\rm reps}_i` is the number of times that the Drag plus and minus rotations are repeated in curve :math:`i`. Note that the aim of the Drag calibration is to find the :math:`\beta` that minimizes the phase shifts. This implies that the optimal :math:`\beta` occurs when all three :math:`y` curves are minimum, i.e. they produce the ground state. This occurs when .. math:: {\rm reps}_i * {\rm freq} * (x - \beta) = N is satisfied with :math:`N` an integer. Note, however, that this condition produces a minimum only when the amplitude is negative. To ensure this is the case, we bound the amplitude to be less than 0. # section: fit_parameters defpar \rm amp: desc: Amplitude of all series. init_guess: The maximum y value scaled by -1, -0.5, and -0.25. bounds: [-2, 0] scaled to the maximum signal value. defpar \rm base: desc: Base line of all series. init_guess: Half the maximum y-value of the data. bounds: [-1, 1] scaled to the maximum y-value. defpar {\rm freq}: desc: Frequency of oscillation as a function of :math:`\beta` for a single pair of DRAG plus and minus pulses. init_guess: For the curve with the most Drag pulse repetitions, the peak frequency of the power spectral density is found and then divided by the number of repetitions. bounds: [0, inf]. defpar \beta: desc: Common beta offset. This is the parameter of interest. init_guess: Linearly spaced between the maximum and minimum scanned beta. bounds: [-min scan range, max scan range]. """ __series__ = [ curve.SeriesDef( fit_func=lambda x, amp, freq, reps0, reps1, reps2, beta, base: cos( x, amp=amp, freq=reps0 * freq, phase=-2 * np.pi * reps0 * freq * beta, baseline=base), plot_color="blue", name="series-0", filter_kwargs={"series": 0}, plot_symbol="o", model_description= r"{\rm amp} \cos\left(2 \pi\cdot {\rm reps}_0\cdot {\rm freq} [x " r"- \beta]\right) + {\rm base}", ), curve.SeriesDef( fit_func=lambda x, amp, freq, reps0, reps1, reps2, beta, base: cos( x, amp=amp, freq=reps1 * freq, phase=-2 * np.pi * reps1 * freq * beta, baseline=base), plot_color="green", name="series-1", filter_kwargs={"series": 1}, plot_symbol="^", model_description= r"{\rm amp} \cos\left(2 \pi\cdot {\rm reps}_1\cdot {\rm freq} [x " r"- \beta]\right) + {\rm base}", ), curve.SeriesDef( fit_func=lambda x, amp, freq, reps0, reps1, reps2, beta, base: cos( x, amp=amp, freq=reps2 * freq, phase=-2 * np.pi * reps2 * freq * beta, baseline=base), plot_color="red", name="series-2", filter_kwargs={"series": 2}, plot_symbol="v", model_description= r"{\rm amp} \cos\left(2 \pi\cdot {\rm reps}_2\cdot {\rm freq} [x " r"- \beta]\right) + {\rm base}", ), ] @classmethod def _default_options(cls): """Return the default analysis options. See :meth:`~qiskit_experiment.curve_analysis.CurveAnalysis._default_options` for descriptions of analysis options. """ default_options = super()._default_options() default_options.curve_drawer.set_options( xlabel="Beta", ylabel="Signal (arb. units)", ) default_options.result_parameters = ["beta"] default_options.fixed_parameters = {"reps0": 1, "reps1": 3, "reps2": 5} default_options.normalization = True return default_options def _generate_fit_guesses( self, user_opt: curve.FitOptions, curve_data: curve.CurveData, ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Create algorithmic guess with 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. """ # Use a fast Fourier transform to guess the frequency. x_data = curve_data.get_subset_of("series-0").x min_beta, max_beta = min(x_data), max(x_data) # Use the highest-frequency curve to estimate the oscillation frequency. series_label, reps_label = max( ("series-0", "reps0"), ("series-1", "reps1"), ("series-2", "reps2"), key=lambda x: self.options.fixed_parameters[x[1]], ) curve_data = curve_data.get_subset_of(series_label) reps2 = self.options.fixed_parameters[reps_label] freqs_guess = curve.guess.frequency(curve_data.x, curve_data.y) / reps2 user_opt.p0.set_if_empty(freq=freqs_guess) avg_x = (max(x_data) + min(x_data)) / 2 span_x = max(x_data) - min(x_data) beta_bound = max(5 / user_opt.p0["freq"], span_x) ptp_y = np.ptp(curve_data.y) user_opt.bounds.set_if_empty( amp=(-2 * ptp_y, 0), freq=(0, np.inf), beta=(avg_x - beta_bound, avg_x + beta_bound), base=(min(curve_data.y) - ptp_y, max(curve_data.y) + ptp_y), ) base_guess = (max(curve_data.y) - min(curve_data.y)) / 2 user_opt.p0.set_if_empty(base=(user_opt.p0["amp"] or base_guess)) # Drag curves can sometimes be very flat, i.e. averages of y-data # and min-max do not always make good initial guesses. We therefore add # 0.5 to the initial guesses. Note that we also set amp=-0.5 because the cosine function # becomes +1 at zero phase, i.e. optimal beta, in which y data should become zero # in discriminated measurement level. options = [] for amp_factor in (-1, -0.5, -0.25): for beta_guess in np.linspace(min_beta, max_beta, 20): new_opt = user_opt.copy() new_opt.p0.set_if_empty(amp=ptp_y * amp_factor, beta=beta_guess) options.append(new_opt) return options def _run_curve_fit( self, curve_data: curve.CurveData, series: List[curve.SeriesDef], ) -> Union[None, curve.FitData]: r"""Perform curve fitting on given data collection and fit models. .. note:: This class post-processes the fit result from a Drag analysis. The Drag analysis should return the beta value that is closest to zero. Since the oscillating term is of the form .. math:: \cos(2 \pi\cdot {\rm reps}_i \cdot {\rm freq}\cdot [x - \beta]) There is a periodicity in beta. This post processing finds the beta that is closest to zero by performing the minimization using the modulo function. .. math:: n_\text{min} = \min_{n}|\beta_\text{fit} + n / {\rm freq}| and assigning the new beta value to .. math:: \beta = \beta_\text{fit} + n_\text{min} / {\rm freq}. Args: curve_data: Formatted data to fit. series: A list of fit models. Returns: The best fitting outcome with minimum reduced chi-squared value. """ fit_result = super()._run_curve_fit(curve_data, series) beta = fit_result.popt[2] freq = fit_result.popt[1] fit_result.popt[2] = ((beta + 1 / freq / 2) % (1 / freq)) - 1 / freq / 2 return fit_result def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - a reduced chi-squared lower than three, - a DRAG parameter value within the first period of the lowest number of repetitions, - an error on the drag beta smaller than the beta. """ fit_beta = fit_data.fitval("beta") fit_freq = fit_data.fitval("freq") criteria = [ fit_data.reduced_chisq < 3, abs(fit_beta.nominal_value) < 1 / fit_freq.nominal_value / 2, curve.is_error_not_significant(fit_beta), ] if all(criteria): return "good" return "bad"
class RBAnalysis(curve.CurveAnalysis): r"""A class to analyze randomized benchmarking experiments. # 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 Clifford (EPC). # 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:`(y - b) / \alpha^x`. bounds: [0, 1] defpar b: desc: Base line. init_guess: Determined by the average :math:`b` of the standard and interleaved RB. Usually equivalent to :math:`(1/2)^n` where :math:`n` is number of qubit. bounds: [0, 1] defpar \alpha: desc: Depolarizing parameter. init_guess: Determined by the slope of :math:`(y - b)^{-x}` of the first and the second data point. bounds: [0, 1] """ __series__ = [ curve.SeriesDef( fit_func=lambda x, a, alpha, b: curve.fit_function. exponential_decay(x, amp=a, lamb=-1.0, base=alpha, baseline=b), plot_color="blue", model_description=r"a \alpha^x + b", ) ] @classmethod def _default_options(cls): """Default analysis options. Analysis Options: error_dict (Dict[Tuple[Iterable[int], str], float]): Optional. Error estimates for gates from the backend properties. epg_1_qubit (Dict[int, Dict[str, float]]) : Optional. EPG data for the 1-qubit gate involved, assumed to have been obtained from previous experiments. This is used to estimate the 2-qubit EPG. gate_error_ratio (Dict[str, float]): An estimate for the ratios between errors on different gates. """ default_options = super()._default_options() default_options.xlabel = "Clifford Length" default_options.ylabel = "P(0)" default_options.result_parameters = ["alpha"] default_options.error_dict = None default_options.epg_1_qubit = None default_options.gate_error_ratio = None return default_options def _generate_fit_guesses( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Compute the initial guesses. Args: user_opt: Fit options filled with user provided guess and bounds. Returns: List of fit options that are passed to the fitter function. """ curve_data = self._data() user_opt.bounds.set_if_empty( a=(0, 1), alpha=(0, 1), b=(0, 1), ) return self._initial_guess(user_opt, curve_data.x, curve_data.y, self._num_qubits) @staticmethod def _initial_guess(opt: curve.FitOptions, x_values: np.ndarray, y_values: np.ndarray, num_qubits: int) -> curve.FitOptions: """Create initial guess with experiment data.""" opt.p0.set_if_empty(b=1 / 2**num_qubits) # Use the first two points to guess the decay param dcliff = x_values[1] - x_values[0] dy = (y_values[1] - opt.p0["b"]) / (y_values[0] - opt.p0["b"]) alpha_guess = dy**(1 / dcliff) opt.p0.set_if_empty(alpha=alpha_guess if alpha_guess < 1.0 else 0.99) if y_values[0] > opt.p0["b"]: opt.p0.set_if_empty(a=(y_values[0] - opt.p0["b"]) / (opt.p0["alpha"]**x_values[0])) else: opt.p0.set_if_empty(a=0.95) return opt def _format_data(self, data: curve.CurveData) -> curve.CurveData: """Data format with averaging with sampling strategy.""" # take average over the same x value by regenerating sigma from variance of y values series, xdata, ydata, sigma, shots = multi_mean_xy_data( series=data.data_index, xdata=data.x, ydata=data.y, sigma=data.y_err, shots=data.shots, method="sample", ) # sort by x value in ascending order series, xdata, ydata, sigma, shots = data_sort( series=series, xdata=xdata, ydata=ydata, sigma=sigma, shots=shots, ) return curve.CurveData( label="fit_ready", x=xdata, y=ydata, y_err=sigma, shots=shots, data_index=series, ) def _extra_database_entry( self, fit_data: curve.FitData) -> List[AnalysisResultData]: """Calculate EPC.""" extra_entries = [] # Calculate EPC alpha = fit_data.fitval("alpha") scale = (2**self._num_qubits - 1) / (2**self._num_qubits) epc = scale * (1 - alpha) extra_entries.append( AnalysisResultData( name="EPC", value=epc, chisq=fit_data.reduced_chisq, quality=self._evaluate_quality(fit_data), )) # Calculate EPG if not self.options.gate_error_ratio: # we attempt to get the ratio from the backend properties if not self.options.error_dict: gate_error_ratio = RBUtils.get_error_dict_from_backend( backend=self._backend, qubits=self._physical_qubits) else: gate_error_ratio = self.options.error_dict else: gate_error_ratio = self.options.gate_error_ratio count_ops = [] for meta in self._data(label="raw_data").metadata: count_ops += meta.get("count_ops", []) if len(count_ops) > 0 and gate_error_ratio is not None: gates_per_clifford = RBUtils.gates_per_clifford(count_ops) num_qubits = len(self._physical_qubits) if num_qubits == 1: epg_dict = RBUtils.calculate_1q_epg( epc, self._physical_qubits, gate_error_ratio, gates_per_clifford, ) elif num_qubits == 2: epg_1_qubit = self.options.epg_1_qubit epg_dict = RBUtils.calculate_2q_epg( epc, self._physical_qubits, gate_error_ratio, gates_per_clifford, epg_1_qubit=epg_1_qubit, ) else: # EPG calculation is not supported for more than 3 qubits RB epg_dict = None if epg_dict: for qubits, gate_dict in epg_dict.items(): for gate, value in gate_dict.items(): extra_entries.append( AnalysisResultData( f"EPG_{gate}", value, chisq=fit_data.reduced_chisq, quality=self._evaluate_quality(fit_data), device_components=[Qubit(i) for i in qubits], )) return extra_entries
class DecayAnalysis(curve.CurveAnalysis): r"""A class to analyze general exponential decay curve. # section: fit_model The fit is based on the following decay function. .. math:: F(x) = {\rm amp} \cdot e^{-x/\tau} + {\rm base} # section: fit_parameters defpar \rm amp: desc: Height of the decay curve. init_guess: Determined by :py:func:`~qiskit_experiments.curve_analysis.guess.min_height`. bounds: None defpar \rm base: desc: Base line of the decay curve. init_guess: Determined by the difference of minimum and maximum points. bounds: None defpar \tau: desc: This is the fit parameter of main interest. init_guess: Determined by :py:func:`~qiskit_experiments.curve_analysis.guess.exp_decay`. bounds: None """ __series__ = [ curve.SeriesDef( fit_func=lambda x, amp, base, tau: curve.fit_function. exponential_decay( x, amp=amp, lamb=1 / tau, baseline=base, ), plot_color="blue", model_description=r"amp \exp(-x/tau) + base", ) ] def _generate_fit_guesses( self, user_opt: curve.FitOptions, curve_data: curve.CurveData, ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Create algorithmic guess with 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.p0.set_if_empty(base=curve.guess.min_height(curve_data.y)[0]) alpha = curve.guess.exp_decay(curve_data.x, curve_data.y) if alpha != 0.0: user_opt.p0.set_if_empty( tau=-1 / alpha, amp=curve.guess.max_height(curve_data.y)[0] - user_opt.p0["base"], ) else: # Likely there is no slope. Cannot fit constant line with this model. # Set some large enough number against to the scan range. user_opt.p0.set_if_empty( tau=100 * np.max(curve_data.x), amp=curve.guess.max_height(curve_data.y)[0] - user_opt.p0["base"], ) return user_opt def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - a reduced chi-squared lower than three - tau error is less than its value """ tau = fit_data.fitval("tau") criteria = [ fit_data.reduced_chisq < 3, curve.is_error_not_significant(tau), ] if all(criteria): return "good" return "bad"
class ErrorAmplificationAnalysis(curve.CurveAnalysis): r"""Error amplification analysis class based on a fit to a cosine function. # section: fit_model Analyse an error amplifying calibration experiment by fitting the data to a cosine function. The user must also specify the intended rotation angle per gate, here labeled, :math:`{\rm apg}`. The parameter of interest in the fit is the deviation from the intended rotation angle per gate labeled :math:`{\rm d}\theta`. The fit function is .. math:: y = \frac{{\rm amp}}{2}\cos\left(x[{\rm d}\theta + {\rm apg} ] \ -{\rm phase\_offset}\right)+{\rm base} To understand how the error is measured we can transformed the function above into .. math:: y = \frac{{\rm amp}}{2} \left(\ \cos\right({\rm d}\theta \cdot x\left)\ \cos\right({\rm apg} \cdot x - {\rm phase\_offset}\left) -\ \sin\right({\rm d}\theta \cdot x\left)\ \sin\right({\rm apg} \cdot x - {\rm phase\_offset}\left) \right) + {\rm base} When :math:`{\rm apg} \cdot x - {\rm phase\_offset} = (2n + 1) \pi/2` is satisfied the fit model above simplifies to .. math:: y = \mp \frac{{\rm amp}}{2} \sin\left({\rm d}\theta \cdot x\right) + {\rm base} In the limit :math:`{\rm d}\theta \ll 1`, the error can be estimated from the curve data .. math:: {\rm d}\theta \simeq \mp \frac{2(y - {\rm base})}{x \cdot {\rm amp}} # section: fit_parameters defpar \rm amp: desc: Amplitude of the oscillation. init_guess: The maximum y value less the minimum y value. bounds: [-2, 2] scaled to the maximum signal value. defpar \rm base: desc: Base line. init_guess: The average of the data. bounds: [-1, 1] scaled to the maximum signal value. defpar d\theta: desc: The angle offset in the gate that we wish to measure. init_guess: Multiple initial guesses are tried ranging from -a to a where a is given by :code:`max(abs(angle_per_gate), np.pi / 2)`. Extra guesses are added based on curve data when either :math:`\rm amp` or :math:`\rm base` is :math:`\pi/2`. See fit model for details. bounds: [-0.8 pi, 0.8 pi]. The bounds do not include plus and minus pi since these values often correspond to symmetry points of the fit function. Furthermore, this type of analysis is intended for values of :math:`d\theta` close to zero. """ __series__ = [ curve.SeriesDef( # pylint: disable=line-too-long fit_func=lambda x, amp, d_theta, phase_offset, base, angle_per_gate: curve.fit_function.cos( x, amp=0.5 * amp, freq=(d_theta + angle_per_gate) / (2 * np.pi), phase=-phase_offset, baseline=base, ), plot_color="blue", model_description= r"\frac{{\rm amp}}{2}\cos\left(x[{\rm d}\theta + {\rm apg} ] " r"+ {\rm phase\_offset}\right)+{\rm base}", ) ] @classmethod def _default_options(cls): r"""Return the default analysis options. See :meth:`~qiskit_experiment.curve_analysis.CurveAnalysis._default_options` for descriptions of analysis options. Analysis Options: max_good_angle_error (float): The maximum angle error for which the fit is considered as good. Defaults to :math:`\pi/2`. """ default_options = super()._default_options() default_options.curve_drawer.set_options( xlabel="Number of gates (n)", ylabel="Population", ylim=(0, 1.0), ) default_options.result_parameters = ["d_theta"] default_options.max_good_angle_error = np.pi / 2 return default_options def _generate_fit_guesses( self, user_opt: curve.FitOptions, curve_data: curve.CurveData, ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Create algorithmic guess with 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. """ fixed_params = self.options.fixed_parameters max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True) max_y, min_y = np.max(curve_data.y), np.min(curve_data.y) user_opt.bounds.set_if_empty(d_theta=(-0.8 * np.pi, 0.8 * np.pi), base=(-max_abs_y, max_abs_y)) user_opt.p0.set_if_empty(base=(max_y + min_y) / 2) if "amp" in user_opt.p0: user_opt.p0.set_if_empty(amp=max_y - min_y) user_opt.bounds.set_if_empty(amp=(0, 2 * max_abs_y)) amp = user_opt.p0["amp"] else: # Fixed parameter amp = fixed_params.get("amp", 1.0) # Base the initial guess on the intended angle_per_gate and phase offset. apg = user_opt.p0.get("angle_per_gate", fixed_params.get("angle_per_gate", 0.0)) phi = user_opt.p0.get("phase_offset", fixed_params.get("phase_offset", 0.0)) # Prepare logical guess for specific condition (often satisfied) d_theta_guesses = [] offsets = apg * curve_data.x + phi for i in range(curve_data.x.size): xi = curve_data.x[i] yi = curve_data.y[i] if np.isclose(offsets[i] % np.pi, np.pi / 2) and xi > 0: # Condition satisfied: i.e. cos(apg x - phi) = 0 err = -np.sign(np.sin(offsets[i])) * ( yi - user_opt.p0["base"]) / (0.5 * amp) # Validate estimate. This is just the first order term of Maclaurin expansion. if np.abs(err) < 0.5: d_theta_guesses.append(err / xi) else: # Terminate guess generation because larger d_theta x will start to # reduce net y value and underestimate the rotation. break # Add naive guess for more coverage guess_range = max(abs(apg), np.pi / 2) d_theta_guesses.extend(np.linspace(-guess_range, guess_range, 11)) options = [] for d_theta_guess in d_theta_guesses: new_opt = user_opt.copy() new_opt.p0.set_if_empty(d_theta=d_theta_guess) options.append(new_opt) return options def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - a reduced chi-squared lower than three, - a measured angle error that is smaller than the allowed maximum good angle error. This quantity is set in the analysis options. """ fit_d_theta = fit_data.fitval("d_theta") criteria = [ fit_data.reduced_chisq < 3, abs(fit_d_theta.nominal_value) < abs( self.options.max_good_angle_error), ] if all(criteria): return "good" return "bad"
class ErrorAmplificationAnalysis(curve.CurveAnalysis): r"""Error amplification analysis class based on a fit to a cosine function. # section: fit_model Analyse an error amplifying calibration experiment by fitting the data to a cosine function. The user must also specify the intended rotation angle per gate, here labeled, :math:`{\rm apg}`. The parameter of interest in the fit is the deviation from the intended rotation angle per gate labeled :math:`{\rm d}\theta`. The fit function is .. math:: y = \frac{{\rm amp}}{2}\cos\left(x[{\rm d}\theta + {\rm apg} ] \ +{\rm phase\_offset}\right)+{\rm base} # section: fit_parameters defpar \rm amp: desc: Amplitude of the oscillation. init_guess: The maximum y value less the minimum y value. bounds: [-2, 2] scaled to the maximum signal value. defpar \rm base: desc: Base line. init_guess: The average of the data. bounds: [-1, 1] scaled to the maximum signal value. defpar d\theta: desc: The angle offset in the gate that we wish to measure. init_guess: Multiple initial guesses are tried ranging from -a to a where a is given by :code:`max(abs(angle_per_gate), np.pi / 2)`. bounds: [-pi, pi]. # section: note Different analysis classes may subclass this class to fix some of the fit parameters. """ __series__ = [ curve.SeriesDef( # pylint: disable=line-too-long fit_func=lambda x, amp, d_theta, phase_offset, base, angle_per_gate: curve.fit_function.cos( x, amp=0.5 * amp, freq=(d_theta + angle_per_gate) / (2 * np.pi), phase=phase_offset, baseline=base, ), plot_color="blue", model_description= r"\frac{{\rm amp}}{2}\cos\left(x[{\rm d}\theta + {\rm apg} ] " r"+ {\rm phase\_offset}\right)+{\rm base}", ) ] @classmethod def _default_options(cls): r"""Return the default analysis options. See :meth:`~qiskit_experiment.curve_analysis.CurveAnalysis._default_options` for descriptions of analysis options. Analysis Options: angle_per_gate (float): The ideal angle per repeated gate. The user must set this option as it defaults to None. phase_offset (float): A phase offset for the analysis. This phase offset will be :math:`\pi/2` if the square-root of X gate is added before the repeated gates. This is decided for the user in :meth:`set_schedule` depending on whether the sx gate is included in the experiment. number_of_guesses (int): The number of initial guesses to try. max_good_angle_error (float): The maximum angle error for which the fit is considered as good. Defaults to :math:`\pi/2`. """ default_options = super()._default_options() default_options.result_parameters = ["d_theta"] default_options.xlabel = "Number of gates (n)" default_options.ylabel = "Population" default_options.angle_per_gate = None default_options.phase_offset = 0.0 default_options.number_guesses = 21 default_options.max_good_angle_error = np.pi / 2 return default_options def _generate_fit_guesses( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Compute the initial guesses. Args: user_opt: Fit options filled with user provided guess and bounds. Returns: List of fit options that are passed to the fitter function. Raises: CalibrationError: When ``angle_per_gate`` is missing. """ n_guesses = self._get_option("number_guesses") curve_data = self._data() max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True) max_y, min_y = np.max(curve_data.y), np.min(curve_data.y) user_opt.bounds.set_if_empty(d_theta=(-np.pi, np.pi), base=(-max_abs_y, max_abs_y)) user_opt.p0.set_if_empty(base=(max_y + min_y) / 2) if "amp" in user_opt.p0: user_opt.p0.set_if_empty(amp=max_y - min_y) user_opt.bounds.set_if_empty(amp=(-2 * max_abs_y, 2 * max_abs_y)) # Base the initial guess on the intended angle_per_gate. angle_per_gate = self._get_option("angle_per_gate") if angle_per_gate is None: raise CalibrationError( "The angle_per_gate was not specified in the analysis options." ) guess_range = max(abs(angle_per_gate), np.pi / 2) options = [] for d_theta_guess in np.linspace(-guess_range, guess_range, n_guesses): new_opt = user_opt.copy() new_opt.p0.set_if_empty(d_theta=d_theta_guess) options.append(new_opt) return options def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - a reduced chi-squared lower than three, - a measured angle error that is smaller than the allowed maximum good angle error. This quantity is set in the analysis options. """ fit_d_theta = fit_data.fitval("d_theta").value max_good_angle_error = self._get_option("max_good_angle_error") criteria = [ fit_data.reduced_chisq < 3, abs(fit_d_theta) < abs(max_good_angle_error), ] if all(criteria): return "good" return "bad"
class OscillationAnalysis(curve.CurveAnalysis): r"""Oscillation analysis class based on a fit of the data to a cosine function. # section: fit_model Analyse oscillating data by fitting it to a cosine function .. math:: y = {\rm amp} \cos\left(2 \pi\cdot {\rm freq}\cdot x + {\rm phase}\right) + {\rm base} # section: fit_parameters defpar \rm amp: desc: Amplitude of the oscillation. init_guess: Calculated by :func:`~qiskit_experiments.curve_analysis.guess.max_height`. bounds: [-2, 2] scaled to the maximum signal value. defpar \rm base: desc: Base line. init_guess: Calculated by :func:`~qiskit_experiments.curve_analysis.\ guess.constant_sinusoidal_offset`. bounds: [-1, 1] scaled to the maximum signal value. defpar \rm freq: desc: Frequency of the oscillation. This is the fit parameter of interest. init_guess: Calculated by :func:`~qiskit_experiments.curve_analysis.\ guess.frequency`. bounds: [0, inf]. defpar \rm phase: desc: Phase of the oscillation. init_guess: Zero. bounds: [-pi, pi]. """ __series__ = [ curve.SeriesDef( fit_func=lambda x, amp, freq, phase, base: curve.fit_function.cos( x, amp=amp, freq=freq, phase=phase, baseline=base ), plot_color="blue", model_description=r"{\rm amp} \cos\left(2 \pi\cdot {\rm freq}\cdot x " r"+ {\rm phase}\right) + {\rm base}", ) ] def _generate_fit_guesses( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Compute the initial guesses. Args: user_opt: Fit options filled with user provided guess and bounds. Returns: List of fit options that are passed to the fitter function. """ curve_data = self._data() max_abs_y, _ = curve.guess.max_height(curve_data.y, absolute=True) user_opt.bounds.set_if_empty( amp=(-2 * max_abs_y, 2 * max_abs_y), freq=(0, np.inf), phase=(-np.pi, np.pi), base=(-max_abs_y, max_abs_y), ) user_opt.p0.set_if_empty( freq=curve.guess.frequency(curve_data.x, curve_data.y), base=curve.guess.constant_sinusoidal_offset(curve_data.y), ) user_opt.p0.set_if_empty( amp=curve.guess.max_height(curve_data.y - user_opt.p0["base"], absolute=True)[0], ) options = [] for phase_guess in np.linspace(0, np.pi, 5): new_opt = user_opt.copy() new_opt.p0.set_if_empty(phase=phase_guess) options.append(new_opt) return options def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - a reduced chi-squared lower than three, - more than a quarter of a full period, - less than 10 full periods, and - an error on the fit frequency lower than the fit frequency. """ fit_freq = fit_data.fitval("freq").value fit_freq_err = fit_data.fitval("freq").stderr criteria = [ fit_data.reduced_chisq < 3, 1.0 / 4.0 < fit_freq < 10.0, (fit_freq_err is None or (fit_freq_err < fit_freq)), ] if all(criteria): return "good" return "bad"
class DumpedOscillationAnalysis(curve.CurveAnalysis): r"""A class to analyze general exponential decay curve with sinusoidal oscillation. # section: fit_model This class is based on the fit model of sinusoidal signal with exponential decay. This model is often used for the oscillation signal in the dissipative system. .. math:: F(x) = {\rm amp} \cdot e^{-x/\tau} \cos(2\pi \cdot {\rm freq} \cdot t + \phi) + {\rm base} # section: fit_parameters defpar \rm amp: desc: Amplitude. Height of the decay curve. init_guess: 0.5 bounds: [0, 1.5], defpar \rm base: desc: Offset. Base line of the decay curve. init_guess: Determined by :py:func:`~qiskit_experiments.curve_analysis.\ guess.constant_sinusoidal_offset` bounds: [0, 1.5] defpar \tau: desc: Represents the rate of decay. init_guess: Determined by :py:func:`~qiskit_experiments.curve_analysis.\ guess.oscillation_exp_decay` bounds: [0, None] defpar \rm freq: desc: Oscillation frequency. init_guess: Determined by :py:func:`~qiskit_experiments.curve_analysis.guess.frequency` bounds: [0, 10 freq] defpar \phi: desc: Phase. Relative shift of the sinusoidal function from the origin. init_guess: Set multiple guesses within [-pi, pi] bounds: [-pi, pi] """ __series__ = [ curve.SeriesDef( fit_func=lambda x, amp, base, tau, freq, phi: curve.fit_function.cos_decay( x, amp=amp, tau=tau, freq=freq, phase=phi, baseline=base, ), plot_color="blue", model_description=r"amp \exp(-x/tau) \cos(2pi freq x + phi) + base", ) ] def _generate_fit_guesses( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Compute the initial guesses. Args: user_opt: Fit options filled with user provided guess and bounds. Returns: List of fit options that are passed to the fitter function. """ curve_data = self._data() user_opt.p0.set_if_empty( amp=0.5, base=curve.guess.constant_sinusoidal_offset(curve_data.y), ) # frequency resolution of this scan df = 1 / ((curve_data.x[1] - curve_data.x[0]) * len(curve_data.x)) if user_opt.p0["freq"] is not None: # If freq guess is provided freq_guess = user_opt.p0["freq"] freqs = [freq_guess] else: freq_guess = curve.guess.frequency(curve_data.x, curve_data.y - user_opt.p0["base"]) # The FFT might be up to 1/2 bin off if freq_guess > df: freqs = [freq_guess - df, freq_guess, freq_guess + df] else: freqs = [0.0, freq_guess] # Set guess for decay parameter based on estimated frequency if freq_guess > df: alpha = curve.guess.oscillation_exp_decay( curve_data.x, curve_data.y - user_opt.p0["base"], freq_guess=freq_guess ) else: # Very low frequency. Assume standard exponential decay alpha = curve.guess.exp_decay(curve_data.x, curve_data.y) if alpha != 0.0: user_opt.p0.set_if_empty(tau=-1 / alpha) else: # Likely there is no slope. Cannot fit constant line with this model. # Set some large enough number against to the scan range. user_opt.p0.set_if_empty(tau=100 * np.max(curve_data.x)) user_opt.bounds.set_if_empty( amp=[0, 1.5], base=[0, 1.5], tau=(0, np.inf), freq=(0, 10 * freq_guess), phi=(-np.pi, np.pi), ) # more robust estimation options = [] for freq in freqs: for phi in np.linspace(-np.pi, np.pi, 5)[:-1]: new_opt = user_opt.copy() new_opt.p0.set_if_empty(freq=freq, phi=phi) options.append(new_opt) return options def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - a reduced chi-squared lower than three - relative error of tau is less than its value - relative error of freq is less than its value """ tau = fit_data.fitval("tau") freq = fit_data.fitval("freq") criteria = [ fit_data.reduced_chisq < 3, tau.stderr is None or tau.stderr < tau.value, freq.stderr is None or freq.stderr < freq.value, ] if all(criteria): return "good" return "bad"
class CrossResonanceHamiltonianAnalysis(curve.CurveAnalysis): r"""A class to analyze cross resonance Hamiltonian tomography experiment. # section: fit_model The following equations are used to approximate the dynamics of the target qubit Bloch vector. .. math:: \begin{align} F_{x, c}(t) &= \frac{1}{\Omega_c^2} \left( - p_{z, c} p_{x, c} + p_{z, c} p_{x, c} \cos(\Omega_c t') + \Omega_c p_{y, c} \sin(\Omega_c t') \right) + b \tag{1} \\ F_{y, c}(t) &= \frac{1}{\Omega_c^2} \left( p_{z, c} p_{y, c} - p_{z, c} p_{y, c} \cos(\Omega_c t') - \Omega_c p_{x, c} \sin(\Omega_c t') \right) + b \tag{2} \\ F_{z, c}(t) &= \frac{1}{\Omega_c^2} \left( p_{z, c}^2 + (p_{x, c}^2 + p_{y, c}^2) \cos(\Omega_c t') \right) + b \tag{3} \end{align} where :math:`t' = t + t_{\rm offset}` with :math:`t` is pulse duration to scan and :math:`t_{\rm offset}` is an extra fit parameter that may represent the edge effect. The :math:`\Omega_c = \sqrt{p_{x, c}^2+p_{y, c}^2+p_{z, c}^2}` and :math:`p_{x, c}, p_{y, c}, p_{z, c}, b` are also fit parameters. The subscript :math:`c` represents the state of control qubit :math:`c \in \{0, 1\}`. The fit functions :math:`F_{x, c}, F_{y, c}, F_{z, c}` approximate the Pauli expectation values :math:`\langle \sigma_{x, c} (t) \rangle, \langle \sigma_{y, c} (t) \rangle, \langle \sigma_{z, c} (t) \rangle` of the target qubit, respectively. Based on the fit result, cross resonance Hamiltonian coefficients can be written as .. math:: ZX &= \frac{p_{x, 0} - p_{x, 1}}{2} \\ ZY &= \frac{p_{y, 0} - p_{y, 1}}{2} \\ ZZ &= \frac{p_{z, 0} - p_{z, 1}}{2} \\ IX &= \frac{p_{x, 0} + p_{x, 1}}{2} \\ IY &= \frac{p_{y, 0} + p_{y, 1}}{2} \\ IZ &= \frac{p_{z, 0} + p_{z, 1}}{2} In this analysis, the initial guess is generated by the following equations. .. math:: p_x &= \omega \cos(\theta) \cos(\phi) \\ p_y &= \omega \cos(\theta) \sin(\phi) \\ p_z &= \omega \sin(\theta) where :math:`\omega` is the mean oscillation frequency of eigenvalues, :math:`\theta = \cos^{-1}\sqrt{\frac{\max F_z - \min F_z}{2}}` and :math:`\phi \in [-\pi, \pi]`. # section: fit_parameters defpar t_{\rm off}: desc: Offset to the pulse duration. For example, if pulse envelope is a flat-topped Gaussian, two Gaussian edges may become an offset duration. init_guess: Computed as :math:`N \sqrt{2 \pi} \sigma` where the :math:`N` is number of pulses and :math:`\sigma` is Gaussian sigma of rising and falling edges. Note that this implicitly assumes the :py:class:`~qiskit.pulse.library\ .parametric_pulses.GaussianSquare` pulse envelope. bounds: [0, None] defpar p_{x, 0}: desc: Fit parameter of oscillations when control qubit state is 0. init_guess: See fit model section. bounds: None defpar p_{y, 0}: desc: Fit parameter of oscillations when control qubit state is 0. init_guess: See fit model section. bounds: None defpar p_{z, 0}: desc: Fit parameter of oscillations when control qubit state is 0. init_guess: See fit model section. bounds: None defpar p_{x, 1}: desc: Fit parameter of oscillations when control qubit state is 1. init_guess: See fit model section. bounds: None defpar p_{y, 1}: desc: Fit parameter of oscillations when control qubit state is 1. init_guess: See fit model section. bounds: None defpar p_{z, 1}: desc: Fit parameter of oscillations when control qubit state is 1. init_guess: See fit model section. bounds: None defpar b: desc: Vertical offset of oscillations. This may indicate the state preparation and measurement error. init_guess: 0 bounds: None # section: see_also qiskit_experiments.library.characterization.cr_hamiltonian.CrossResonanceHamiltonian """ __series__ = [ curve.SeriesDef( name="x|c=0", fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_x( x + t_off, px=px0, py=py0, pz=pz0, baseline=b ), filter_kwargs={"control_state": 0, "meas_basis": "x"}, plot_color="blue", plot_symbol="o", canvas=0, ), curve.SeriesDef( name="y|c=0", fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_y( x + t_off, px=px0, py=py0, pz=pz0, baseline=b ), filter_kwargs={"control_state": 0, "meas_basis": "y"}, plot_color="blue", plot_symbol="o", canvas=1, ), curve.SeriesDef( name="z|c=0", fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_z( x + t_off, px=px0, py=py0, pz=pz0, baseline=b ), filter_kwargs={"control_state": 0, "meas_basis": "z"}, plot_color="blue", plot_symbol="o", canvas=2, ), curve.SeriesDef( name="x|c=1", fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_x( x + t_off, px=px1, py=py1, pz=pz1, baseline=b ), filter_kwargs={"control_state": 1, "meas_basis": "x"}, plot_color="red", plot_symbol="^", canvas=0, ), curve.SeriesDef( name="y|c=1", fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_y( x + t_off, px=px1, py=py1, pz=pz1, baseline=b ), filter_kwargs={"control_state": 1, "meas_basis": "y"}, plot_color="red", plot_symbol="^", canvas=1, ), curve.SeriesDef( name="z|c=1", fit_func=lambda x, t_off, px0, px1, py0, py1, pz0, pz1, b: curve.fit_function.bloch_oscillation_z( x + t_off, px=px1, py=py1, pz=pz1, baseline=b ), filter_kwargs={"control_state": 1, "meas_basis": "z"}, plot_color="red", plot_symbol="^", canvas=2, ), ] @classmethod def _default_options(cls): """Return the default analysis options.""" default_options = super()._default_options() default_options.data_processor = dp.DataProcessor( input_key="counts", data_actions=[dp.Probability("1"), dp.BasisExpectationValue()], ) default_options.curve_plotter = "mpl_multiv_canvas" default_options.xlabel = "Flat top width" default_options.ylabel = "<X(t)>,<Y(t)>,<Z(t)>" default_options.xval_unit = "s" default_options.style = curve.visualization.PlotterStyle( figsize=(8, 10), legend_loc="lower right", fit_report_rpos=(0.28, -0.10), ) default_options.ylim = (-1, 1) return default_options def _t_off_initial_guess(self) -> float: """Return initial guess for time offset. This method assumes the :py:class:`~qiskit.pulse.library.parametric_pulses.GaussianSquare` envelope with the Gaussian rising and falling edges with the parameter ``sigma``. This is intended to be overridden by a child class so that rest of the analysis class logic can be reused for the fitting that assumes other pulse envelopes. Returns: An initial guess for time offset parameter ``t_off`` in SI units. Raises: AnalysisError: When the backend doesn't report the time resolution of waveforms. """ n_pulses = self._extra_metadata().get("n_cr_pulses", 1) sigma = self._experiment_options().get("sigma", 0) # Convert sigma unit into SI try: prefactor = self._backend.configuration().dt except AttributeError as ex: raise AnalysisError("Backend configuration does not provide time resolution.") from ex return np.sqrt(2 * np.pi) * prefactor * sigma * n_pulses def _generate_fit_guesses( self, user_opt: curve.FitOptions ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Compute the initial guesses. Args: user_opt: Fit options filled with user provided guess and bounds. Returns: List of fit options that are passed to the fitter function. """ user_opt.bounds.set_if_empty(t_off=(0, np.inf), b=(-1, 1)) user_opt.p0.set_if_empty(t_off=self._t_off_initial_guess(), b=1e-9) guesses = defaultdict(list) for control in (0, 1): x_data = self._data(series_name=f"x|c={control}") y_data = self._data(series_name=f"y|c={control}") z_data = self._data(series_name=f"z|c={control}") omega_xyz = [] for data in (x_data, y_data, z_data): ymin, ymax = np.percentile(data.y, [10, 90]) if ymax - ymin < 0.2: # oscillation amplitude might be almost zero, # then exclude from average because of lower SNR continue fft_freq = curve.guess.frequency(data.x, data.y) omega_xyz.append(fft_freq) if omega_xyz: omega = 2 * np.pi * np.average(omega_xyz) else: omega = 1e-3 zmin, zmax = np.percentile(z_data.y, [10, 90]) theta = np.arccos(np.sqrt((zmax - zmin) / 2)) # The FFT might be up to 1/2 bin off df = 2 * np.pi / ((z_data.x[1] - z_data.x[0]) * len(z_data.x)) for omega_shifted in [omega, omega - df / 2, omega + df / 2]: for phi in np.linspace(-np.pi, np.pi, 5): px = omega_shifted * np.cos(theta) * np.cos(phi) py = omega_shifted * np.cos(theta) * np.sin(phi) pz = omega_shifted * np.sin(theta) guesses[control].append( { f"px{control}": px, f"py{control}": py, f"pz{control}": pz, } ) if omega < df: # empirical guess for low frequency case guesses[control].append( { f"px{control}": omega, f"py{control}": omega, f"pz{control}": 0, } ) fit_options = [] # combine all guesses in Cartesian product for p0s, p1s in product(guesses[0], guesses[1]): new_opt = user_opt.copy() new_opt.p0.set_if_empty(**p0s, **p1s) fit_options.append(new_opt) return fit_options def _evaluate_quality(self, fit_data: curve.FitData) -> Union[str, None]: """Algorithmic criteria for whether the fit is good or bad. A good fit has: - If chi-squared value is less than 3. """ if fit_data.reduced_chisq < 3: return "good" return "bad" def _extra_database_entry(self, fit_data: curve.FitData) -> List[AnalysisResultData]: """Calculate Hamiltonian coefficients from fit values.""" extra_entries = [] for control in ("z", "i"): for target in ("x", "y", "z"): p0_val = fit_data.fitval(f"p{target}0") p1_val = fit_data.fitval(f"p{target}1") if control == "z": coef_val = 0.5 * (p0_val - p1_val) / (2 * np.pi) else: coef_val = 0.5 * (p0_val + p1_val) / (2 * np.pi) extra_entries.append( AnalysisResultData( name=f"omega_{control}{target}", value=coef_val, chisq=fit_data.reduced_chisq, device_components=[Qubit(q) for q in self._physical_qubits], extra={"unit": "Hz"}, ) ) return extra_entries
class RBAnalysis(curve.CurveAnalysis): r"""A class to analyze randomized benchmarking experiments. # 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 Clifford (EPC). When analysis option ``gate_error_ratio`` is provided, this analysis also estimates errors of individual gates assembling a Clifford gate. In computation of two-qubit EPC, this analysis can also decompose the contribution from the underlying single qubit depolarizing channels when ``epg_1_qubit`` analysis option is provided [1]. # 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:`~rb_decay`. bounds: [0, 1] # section: reference .. ref_arxiv:: 1 1712.06550 """ __series__ = [ curve.SeriesDef( fit_func=lambda x, a, alpha, b: curve.fit_function. exponential_decay(x, amp=a, lamb=-1.0, base=alpha, baseline=b), plot_color="blue", model_description=r"a \alpha^x + b", ) ] def __init__(self): super().__init__() self._gate_counts_per_clifford = None self._physical_qubits = None @classmethod def _default_options(cls): """Default analysis options. Analysis Options: gate_error_ratio (Optional[Dict[str, float]]): A dictionary with gate name keys and error ratio values used when calculating EPG from the estimated EPC. The default value will use standard gate error ratios. If you don't know accurate error ratio between your basis gates, you can skip analysis of EPGs by setting this options to ``None``. epg_1_qubit (List[DbAnalysisResultV1]): Analysis results from previous RB experiments for individual single qubit gates. If this is provided, EPC of 2Q RB is corected to exclude the deporalization of underlying 1Q channels. """ default_options = super()._default_options() default_options.curve_drawer.set_options( xlabel="Clifford Length", ylabel="P(0)", ) default_options.plot_raw_data = True default_options.result_parameters = ["alpha"] default_options.gate_error_ratio = "default" default_options.epg_1_qubit = None return default_options def set_options(self, **fields): if "error_dict" in fields: warnings.warn( "Option 'error_dict' has been removed and merged into 'gate_error_ratio'.", DeprecationWarning, ) fields["gate_error_ratio"] = fields.pop("error_dict") super().set_options(**fields) def _generate_fit_guesses( self, user_opt: curve.FitOptions, curve_data: curve.CurveData, ) -> Union[curve.FitOptions, List[curve.FitOptions]]: """Create algorithmic guess with 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) a_guess = 1 - b_guess alpha_guess = curve.guess.rb_decay(curve_data.x, curve_data.y, a=a_guess, b=b_guess) user_opt.p0.set_if_empty( b=b_guess, a=a_guess, alpha=alpha_guess, ) return user_opt def _format_data( self, curve_data: curve.CurveData, ) -> curve.CurveData: """Postprocessing for the processed dataset. Args: curve_data: Processed dataset created from experiment results. Returns: Formatted data. """ # TODO Eventually move this to data processor, then create RB data processor. # take average over the same x value by keeping sigma data_allocation, xdata, ydata, sigma, shots = curve.data_processing.multi_mean_xy_data( series=curve_data.data_allocation, xdata=curve_data.x, ydata=curve_data.y, sigma=curve_data.y_err, shots=curve_data.shots, method="sample", ) # sort by x value in ascending order data_allocation, xdata, ydata, sigma, shots = curve.data_processing.data_sort( series=data_allocation, xdata=xdata, ydata=ydata, sigma=sigma, shots=shots, ) return curve.CurveData( x=xdata, y=ydata, y_err=sigma, shots=shots, data_allocation=data_allocation, labels=curve_data.labels, ) def _create_analysis_results( self, fit_data: curve.FitData, 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.fitval("alpha") scale = (2**num_qubits - 1) / (2**num_qubits) epc = scale * (1 - alpha) outcomes.append( AnalysisResultData( name="EPC", 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_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_{gate}", value=epg_val, chisq=fit_data.reduced_chisq, quality=quality, extra=metadata, )) return outcomes def _initialize( self, experiment_data: ExperimentData, ): """Initialize curve analysis with experiment data. This method is called ahead of other processing. Args: experiment_data: Experiment data to analyze. Raises: AnalysisError: When circuit metadata for ops count is missing. """ super()._initialize(experiment_data) if self.options.gate_error_ratio is not None: # If gate error ratio is not False, EPG analysis is enabled. # Here analysis prepares gate error ratio and gate counts for EPC to EPG conversion. # If gate count dictionary is not set it will compute counts from circuit metadata. avg_gpc = defaultdict(float) n_circs = len(experiment_data.data()) for circ_result in experiment_data.data(): try: count_ops = circ_result["metadata"]["count_ops"] except KeyError as ex: raise AnalysisError( "'count_ops' key is not found in the circuit metadata. " "This analysis cannot compute error per gates. " "Please disable this with 'gate_error_ratio=False'." ) from ex nclif = circ_result["metadata"]["xval"] for (qinds, gate), count in count_ops: formatted_key = tuple(sorted(qinds)), gate avg_gpc[formatted_key] += count / nclif / n_circs self._gate_counts_per_clifford = dict(avg_gpc) if self.options.gate_error_ratio == "default": # Gate error dict is computed for gates appearing in counts dictionary # Error ratio among gates is determined based on the predefined lookup table. # This is not always accurate for every quantum backends. gate_error_ratio = {} for qinds, gate in self._gate_counts_per_clifford.keys(): if set(qinds) != set( experiment_data.metadata["physical_qubits"]): continue gate_error_ratio[gate] = _lookup_epg_ratio( gate, len(qinds)) self.set_options(gate_error_ratio=gate_error_ratio) # Get qubit number self._physical_qubits = experiment_data.metadata["physical_qubits"]