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"
Exemple #3
0
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"
Exemple #6
0
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"
Exemple #8
0
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
Exemple #9
0
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"
Exemple #11
0
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"
Exemple #14
0
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
Exemple #15
0
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"]