diff --git a/qiskit_experiments/library/__init__.py b/qiskit_experiments/library/__init__.py index 63c8ecfbaf..b0d0c21c4d 100644 --- a/qiskit_experiments/library/__init__.py +++ b/qiskit_experiments/library/__init__.py @@ -76,6 +76,7 @@ ~characterization.LocalReadoutError ~characterization.CorrelatedReadoutError ~characterization.ResonatorSpectroscopy + ~characterization.ZZRamsey .. _calibration: @@ -148,6 +149,7 @@ class instance to manage parameters and pulse schedules. ResonatorSpectroscopy, LocalReadoutError, CorrelatedReadoutError, + ZZRamsey, ) from .randomized_benchmarking import StandardRB, InterleavedRB from .tomography import StateTomography, ProcessTomography diff --git a/qiskit_experiments/library/characterization/__init__.py b/qiskit_experiments/library/characterization/__init__.py index d8ca21f792..b41a49cca1 100644 --- a/qiskit_experiments/library/characterization/__init__.py +++ b/qiskit_experiments/library/characterization/__init__.py @@ -47,6 +47,7 @@ LocalReadoutError CorrelatedReadoutError ResonatorSpectroscopy + ZZRamsey Analysis @@ -71,6 +72,7 @@ ResonatorSpectroscopyAnalysis LocalReadoutErrorAnalysis CorrelatedReadoutErrorAnalysis + ZZRamseyAnalysis """ @@ -90,6 +92,7 @@ ResonatorSpectroscopyAnalysis, LocalReadoutErrorAnalysis, CorrelatedReadoutErrorAnalysis, + ZZRamseyAnalysis, ) from .t1 import T1 @@ -110,3 +113,4 @@ from .local_readout_error import LocalReadoutError from .correlated_readout_error import CorrelatedReadoutError from .resonator_spectroscopy import ResonatorSpectroscopy +from .zz_ramsey import ZZRamsey diff --git a/qiskit_experiments/library/characterization/analysis/__init__.py b/qiskit_experiments/library/characterization/analysis/__init__.py index 58667e5fe3..f93c08152f 100644 --- a/qiskit_experiments/library/characterization/analysis/__init__.py +++ b/qiskit_experiments/library/characterization/analysis/__init__.py @@ -28,3 +28,4 @@ from .local_readout_error_analysis import LocalReadoutErrorAnalysis from .correlated_readout_error_analysis import CorrelatedReadoutErrorAnalysis from .resonator_spectroscopy_analysis import ResonatorSpectroscopyAnalysis +from .zz_ramsey_analysis import ZZRamseyAnalysis diff --git a/qiskit_experiments/library/characterization/analysis/zz_ramsey_analysis.py b/qiskit_experiments/library/characterization/analysis/zz_ramsey_analysis.py new file mode 100644 index 0000000000..c9f06c061c --- /dev/null +++ b/qiskit_experiments/library/characterization/analysis/zz_ramsey_analysis.py @@ -0,0 +1,248 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +Analysis class for ZZ Ramsey experiment +""" + +from typing import List, Tuple, Union + +import lmfit +import numpy as np + +from qiskit.providers.options import Options + +import qiskit_experiments.curve_analysis as curve +from qiskit_experiments.curve_analysis import CurveAnalysis, CurveData, CurveFitResult, FitOptions +from qiskit_experiments.curve_analysis.utils import is_error_not_significant + + +class ZZRamseyAnalysis(CurveAnalysis): + # Disable long line check because we can't break the long math lines + # pylint: disable=line-too-long + r"""A class to analyze a :math:`ZZ` Ramsey experiment. + + # section: fit_model + + Analyze a :math:`ZZ` Ramsey experiment by fitting the :code:`'0'` and + :code:`'1'` series to sinusoidal functions as defined in the + :class:`ZZRamsey` experiment. The two functions share the frequency, + amplitude, decay constant, baseline, and phase parameters. + + .. math:: + + y_0 = - {\rm amp} \cdot e^{-x/\tau} \cos\left(2 \pi\cdot {\rm freq - zz / 2}\cdot x + {\rm phase}\right) + {\rm base} \\ + + y_1 = - {\rm amp} \cdot e^{-x/\tau} \cos\left(2 \pi\cdot {\rm freq + zz / 2}\cdot x + {\rm phase}\right) + {\rm base} + + :math:`freq` is the same as the virtual frequency :math:`f` mentioned + in :class:`ZZRamsey`. + + # section: fit_parameters + + defpar \rm amp: + desc: Amplitude of the sinusoidal curves. + init_guess: Half of the maximum y value less the minimum y value. + bounds: [0, the peak to peak range of the data] + defpar \tau: + desc: The exponential decay of the curve amplitudes. + init_guess: Inferred by comparing the peak to peak amplitude for + longer delay values with that of shorter delay values and + assuming an exponential decay in amplitude. + bounds: [1/4 of the typical time spacing, + 10 times the maximum delay time]. + defpar \rm base: + desc: Base line of both series. + init_guess: The average of the data, excluding outliers + bounds: [the minimum amplitude less the peak to peak of the data, + the maximum amplitude plus the peak to peak of the data] + defpar \rm freq: + desc: Average frequency of both series. + init_guess: The average of the frequencies with the highest power + spectral density for each series. + bounds: [0, the Nyquist frequency of the data]. + defpar \rm zz: + desc: The :math:`ZZ` value for the qubit pair. In terms of the fit, + this is frequency difference between series 1 and series 0. + init_guess: The difference between the frequencies with the highest + power spectral density for each series + bounds: [-inf, inf]. + defpar \rm phase: + desc: Common phase offset. + init_guess: Zero + bounds: [-pi, pi]. + """ + # pylint: enable=line-too-long + + def __init__(self): + super().__init__( + models=[ + lmfit.models.ExpressionModel( + expr="-amp * exp(-x / tau) * cos(2 * pi * (freq - zz / 2) * x + phase) + base", + name="0", + data_sort_key={"series": "0"}, + ), + lmfit.models.ExpressionModel( + expr="-amp * exp(-x / tau) * cos(2 * pi * (freq + zz / 2) * x + phase) + base", + name="1", + data_sort_key={"series": "1"}, + ), + ] + ) + + @classmethod + def _default_options(cls) -> Options: + """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 = ["zz"] + default_options.plotter.set_figure_options( + xlabel="Delay", + xval_unit="s", + ylabel="P(1)", + ) + + return default_options + + def _generate_fit_guesses( + self, + user_opt: FitOptions, + curve_data: CurveData, + ) -> Union[FitOptions, List[FitOptions]]: + """Compute the initial guesses. + + Args: + user_opt: Fit options filled with user provided guess and bounds. + curve_data: Preprocessed data to be fit. + + Returns: + List of fit options that are passed to the fitter function. + """ + y_max = np.max(curve_data.y) + y_min = np.min(curve_data.y) + y_ptp = y_max - y_min + x_max = np.max(curve_data.x) + + data_0 = curve_data.get_subset_of("0") + data_1 = curve_data.get_subset_of("1") + + def typical_step(arr): + """Find the typical step size of an array""" + steps = np.diff(np.sort(arr)) + # If points are not unique, there will be 0's that don't count as + # steps + steps = steps[steps != 0] + return np.median(steps) + + x_step = max(typical_step(data_0.x), typical_step(data_1.x)) + + user_opt.bounds.set_if_empty( + amp=(0, y_max - y_min), + tau=(x_step / 4, 10 * x_max), + base=(y_min - y_ptp, y_max + y_ptp), + phase=(-np.pi, np.pi), + freq=(0, 1 / 2 / x_step), + ) + + freq_guesses = [ + curve.guess.frequency(data_0.x, data_0.y), + curve.guess.frequency(data_1.x, data_1.y), + ] + base_guesses = [ + curve.guess.constant_sinusoidal_offset(data_0.y), + curve.guess.constant_sinusoidal_offset(data_1.y), + ] + + def rough_sinusoidal_decay_constant( + x_data: np.ndarray, y_data: np.ndarray, bounds: Tuple[float, float] + ) -> float: + """Estimate the decay constant of y_data vs x_data + + This function assumes the data is roughly evenly spaced and that + the y_data goes through a few periods so that the peak to peak + value early in the data can be compared to the peak to peak later + in the data to estimate the decay constant. + + Args: + x_data: x-axis data + y_data: y-axis data + bounds: minimum and maximum allowed decay constant + + Returns: + The bounded guess of the decay constant + """ + x_median = np.median(x_data) + i_left = x_data < x_median + i_right = x_data > x_median + + y_left = np.ptp(y_data[i_left]) + y_right = np.ptp(y_data[i_right]) + x_left = np.average(x_data[i_left]) + x_right = np.average(x_data[i_right]) + + # Now solve y_left = exp(-x_left / tau) and + # y_right = exp(-x_right / tau) for tau + denom = np.log(y_right / y_left) + if denom < 0: + tau = (x_left - x_right) / denom + else: + # If amplitude is constant or growing from left to right, bound + # to the maximum allowed tau + tau = bounds[1] + + return max(min(tau, bounds[1]), bounds[0]) + + user_opt.p0.set_if_empty( + tau=rough_sinusoidal_decay_constant(curve_data.x, curve_data.y, user_opt.bounds["tau"]), + amp=y_ptp / 2, + phase=0.0, + freq=float(np.average(freq_guesses)), + base=np.average(base_guesses), + zz=freq_guesses[1] - freq_guesses[0], + ) + + return user_opt + + def _evaluate_quality(self, fit_data: CurveFitResult) -> 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 + + Args: + fit_data: The fit result of the analysis + + Returns: + The automated fit quality assessment as a string + """ + freq = fit_data.ufloat_params["freq"] + zz = fit_data.ufloat_params["zz"] + amp = fit_data.ufloat_params["amp"] + base = fit_data.ufloat_params["base"] + + rough_freq_magnitude = 1 / (fit_data.x_range[1] - fit_data.x_range[0]) + + criteria = [ + is_error_not_significant(amp, fraction=0.2), + is_error_not_significant(base, absolute=0.2 * amp.nominal_value), + is_error_not_significant(freq, absolute=0.2 * rough_freq_magnitude), + is_error_not_significant(zz, absolute=0.2 * rough_freq_magnitude), + ] + + if all(criteria): + return "good" + + return "bad" diff --git a/qiskit_experiments/library/characterization/zz_ramsey.py b/qiskit_experiments/library/characterization/zz_ramsey.py new file mode 100644 index 0000000000..880b70eb44 --- /dev/null +++ b/qiskit_experiments/library/characterization/zz_ramsey.py @@ -0,0 +1,343 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" +ZZ Ramsey experiment +""" + +from typing import List, Tuple, Union + +import numpy as np + +from qiskit import QuantumCircuit +from qiskit.providers.backend import Backend +from qiskit.circuit import Parameter, ParameterExpression + +from qiskit_experiments.framework import BackendTiming, BaseExperiment, Options +from .analysis.zz_ramsey_analysis import ZZRamseyAnalysis + + +class ZZRamsey(BaseExperiment): + r"""Experiment to characterize the static :math:`ZZ` interaction for a qubit pair + + # section: overview + + This experiment assumes a two qubit Hamiltonian of the form + + .. math:: + + H = h \left(\frac{f_0}{2} ZI + \frac{f_1}{2} IZ + \frac{f_{ZZ}}{4} ZZ\right) + + and measures the strength :math:`f_{ZZ}` of the :math:`ZZ` term. + :math:`f_{ZZ}` can be described as the difference between the frequency + of qubit 0 when qubit 1 is excited and the frequency of qubit 0 when + qubit 1 is in the ground state. Because :math:`f_{ZZ}` is symmetric in + qubit index, it can also be expressed with the roles of 0 and 1 + reversed. Experimentally, we measure :math:`f_{ZZ}` by performing + Ramsey sequences on qubit 0 with qubit 1 in the ground state and again + with qubit 1 in the excited state. The standard Ramsey experiment + consists of putting a qubit along the :math:`X` axis of Bloch sphere, + waiting for some time, and then measuring the qubit project along + :math:`X`. By measuring the :math:`X` projection versus time the qubit + frequency can be inferred. See + :class:`~qiskit_experiments.library.characterization.T2Ramsey` and + :class:`~qiskit_experiments.library.characterization.RamseyXY`. + + Because we are interested in the difference in qubit 0 frequency + between the two qubit 1 preparations rather than the absolute + frequencies of qubit 0 for those preparations, we modify the Ramsey + sequences (the circuits for the modified sequences are shown below). + First, we add an X gate on qubit 0 to the middle of the Ramsey delay. + This would have the effect of echoing out the phase accumulation of + qubit 0 (like a Hahn echo sequence as used in + :class:`~qiskit_experiments.library.characterization.T2Hahn`), but we + add a simultaneous X gate to qubit 1 as well. Flipping qubit 1 inverts + the sign of the :math:`f_{ZZ}` term. The net result is that qubit 0 + continues to accumulate phase proportional to :math:`f_{ZZ}` while the + phase due to any ZI term is canceled out. This technique allows + :math:`f_{ZZ}` to be measured using longer delay times than might + otherwise be possible with a qubit with a slow frequency drift (i.e. + the measurement is not sensitive to qubit frequency drift from shot to + shot, only to drift within a single shot). + + The resulting excited state population of qubit 0 versus delay time + exhibits slow sinusoidal oscillations (assuming :math:`f_{ZZ}` is + relatively small). To help with distinguishing between qubit decay and + a slow oscillation, an extra Z rotation is applied before the final + pulse on qubit 0. The angle of this Z rotation is set proportional to + the delay time of the sequence. This angle proportional to time behaves + similarly to measuring at a fixed angle with the qubit rotating at a + constant frequency. This virtual frequency is common to the two qubit 1 + preparations. By looking at the difference in frequency fitted for the + two cases, this virtual frequency (called :math:`f` in the circuits + shown below) is removed, leaving only the :math:`f_{ZZ}` value. The + value of :math:`f` in terms of the experiment options is + ``num_rotations / (max(delays) - min(delays))``. + + This experiment consists of the following two circuits repeated with + different ``delay`` values. + + .. parsed-literal:: + + Modified Ramsey sequence with qubit 1 initially in the ground state + + ┌────┐ ░ ┌─────────────────┐ ░ ┌───┐ ░ ┌─────────────────┐ ░ » + q_0: ┤ √X ├─░─┤ Delay(delay[s]) ├─░─┤ X ├─░─┤ Delay(delay[s]) ├─░─» + └────┘ ░ └─────────────────┘ ░ ├───┤ ░ └─────────────────┘ ░ » + q_1: ───────░─────────────────────░─┤ X ├─░─────────────────────░─» + ░ ░ └───┘ ░ ░ » + c: 1/═════════════════════════════════════════════════════════════» + » + « ┌─────────────────────┐┌────┐ ░ ┌─┐ + «q_0: ┤ Rz(4*delay*dt*f*pi) ├┤ √X ├─░─┤M├ + « └────────┬───┬────────┘└────┘ ░ └╥┘ + «q_1: ─────────┤ X ├────────────────░──╫─ + « └───┘ ░ ║ + «c: 1/═════════════════════════════════╩═ + « 0 + + Modified Ramsey sequence with qubit 1 initially in the excited state + + ┌────┐ ░ ┌─────────────────┐ ░ ┌───┐ ░ ┌─────────────────┐ ░ » + q_0: ┤ √X ├─░─┤ Delay(delay[s]) ├─░─┤ X ├─░─┤ Delay(delay[s]) ├─░─» + ├───┬┘ ░ └─────────────────┘ ░ ├───┤ ░ └─────────────────┘ ░ » + q_1: ┤ X ├──░─────────────────────░─┤ X ├─░─────────────────────░─» + └───┘ ░ ░ └───┘ ░ ░ » + c: 1/═════════════════════════════════════════════════════════════» + » + « ┌─────────────────────┐┌────┐ ░ ┌─┐ + «q_0: ┤ Rz(4*delay*dt*f*pi) ├┤ √X ├─░─┤M├ + « └─────────────────────┘└────┘ ░ └╥┘ + «q_1: ──────────────────────────────░──╫─ + « ░ ║ + «c: 1/═════════════════════════════════╩═ + « 0 + + # section: analysis_ref + + :py:class:`ZZRamseyAnalysis` + """ + + def __init__( + self, + qubits: (int, int), + backend: Union[Backend, None] = None, + **experiment_options, + ): + """Create new experiment. + + Args: + qubits: The qubits on which to run the Ramsey XY experiment. + backend: Optional, the backend to run the experiment on. + experiment_options: experiment options to set + """ + super().__init__(qubits=qubits, analysis=ZZRamseyAnalysis(), backend=backend) + # Override the default of get_processor() which is "1" * num_qubits. We + # only fit the probability of the target qubit. + self.analysis.set_options(outcome="1") + self.set_experiment_options(**experiment_options) + + @classmethod + def _default_experiment_options(cls) -> Options: + """Default values for the :math:`ZZ` Ramsey experiment. + + Experiment Options: + delays (list[float]): The list of delays that will be scanned in + the experiment, in seconds. If not set, then ``num_delays`` + evenly spaced delays between ``min_delay`` and ``max_delay`` + are used. If ``delays`` is set, ``max_delay``, ``min_delay``, + and ``num_delays`` are ignored. + max_delay (float): Maximum delay time to use. + min_delay (float): Minimum delay time to use. + num_delays (int): Number of circuits to use per control state + preparation. + num_rotations (float): The extra rotation added to qubit 0 uses a + frequency that gives this many rotations in the case where + :math:`f_{ZZ}` is 0. + """ + options = super()._default_experiment_options() + options.delays = None + options.min_delay = 0e-6 + options.max_delay = 10e-6 + options.num_delays = 50 + options.num_rotations = 5 + + return options + + def delays(self) -> List[float]: + """Delay values to use in circuits + + Returns: + The list of delays to use for the different circuits based on the + experiment options. + """ + # This method allows delays to be set by the user as an explicit + # sequence or as a minimum, maximum and number of values for a linearly + # spaced sequence. + options = self.experiment_options + if options.delays is not None: + return options.delays + + return np.linspace(options.min_delay, options.max_delay, options.num_delays).tolist() + + def frequency(self) -> float: + """Frequency of qubit rotation when ZZ is 0 + + This method calculates the simulated frequency applied to both sets of + circuits. The value is chosen to induce `num_rotations` number of + rotation within the time window that the delay is swept through. + + Returns: + The frequency at which the target qubit will rotate when ZZ is zero + based on the current experiment options. + """ + delays = self.delays() + freq = self.experiment_options.num_rotations / (max(delays) - min(delays)) + + return freq + + def _template_circuits( + self, + frequency: Union[None, float, ParameterExpression] = None, + ) -> Tuple[QuantumCircuit, QuantumCircuit]: + """Template circuits for series 0 and 1 parameterized by delay + + The generated circuits have the length of the delay instructions as the + only parameter. + + Args: + dt_value: the value of the backend ``dt`` value. Used to convert + delay values into units of seconds. + delay_unit: the unit of circuit delay instructions. + + Returns: + Circuits for series 0 and 1 + """ + metadata = { + "unit": "s", + } + + delay = Parameter("delay") + + timing = BackendTiming(self.backend) + + frequency = frequency if frequency is not None else self.frequency() + # frequency is always in units of Hz. delay_freq has inverse units to + # the units of `delay`. + # + # If the backend does not have a `dt`, the delays will be treated as in + # units of seconds. Otherwise they will be in units of samples. For the + # samples case, we multiply by `dt` so that `delay_freq` is in inverse + # samples per cycle. + if timing.delay_unit != "s": + delay_freq = timing.dt * frequency + else: + delay_freq = frequency + + # Template circuit for series 0 + # Control qubit starting in |0> state, flipping to |1> in middle + circ0 = QuantumCircuit(2, 1, metadata=metadata.copy()) + circ0.metadata["series"] = "0" + + circ0.sx(0) + + circ0.barrier() + circ0.delay(delay, 0, timing.delay_unit) + circ0.barrier() + + circ0.x(0) + circ0.x(1) + + circ0.barrier() + circ0.delay(delay, 0, timing.delay_unit) + circ0.barrier() + + circ0.rz(2 * np.pi * delay_freq * (2 * delay), 0) + circ0.sx(0) + # Flip control back to 0, so control qubit is in 0 for all circuits + # when qubit 1 is measured. + circ0.x(1) + + circ0.barrier() + circ0.measure(0, 0) + + # Template circuit for series 1 + # Control qubit starting in |1> state, flipping to |0> in middle + circ1 = QuantumCircuit(2, 1, metadata=metadata.copy()) + circ1.metadata["series"] = "1" + + circ1.x(1) + circ1.sx(0) + + circ1.barrier() + circ1.delay(delay, 0, timing.delay_unit) + circ1.barrier() + + circ1.x(0) + circ1.x(1) + + circ1.barrier() + circ1.delay(delay, 0, timing.delay_unit) + circ1.barrier() + + circ1.rz(2 * np.pi * delay_freq * (2 * delay), 0) + circ1.sx(0) + + circ1.barrier() + circ1.measure(0, 0) + + return circ0, circ1 + + def parametrized_circuits(self) -> Tuple[QuantumCircuit, QuantumCircuit]: + r"""Create circuits with parameters for numerical quantities + + This method is primarily intended for generating template circuits that + visualize well. It inserts :class:`qiskit.circuit.Parameter`\ s for + :math:`π` and `dt` as well the target qubit rotation frequency `f`. + + Return: + Parameterized circuits for the case of the control qubit being in 0 + and in 1. + """ + f_param = Parameter("f") + dt = Parameter("dt") + pi = Parameter("pi") + + freq = dt * pi * f_param / np.pi + + timing = BackendTiming(self.backend) + if timing.dt is not None: + freq = freq / timing.dt + + circs = self._template_circuits(frequency=freq) + + return circs + + def circuits(self) -> List[QuantumCircuit]: + """Create circuits + + Returns: + A list of circuits with a variable delay. + """ + timing = BackendTiming(self.backend) + + circ0, circ1 = self._template_circuits() + circs = [] + + for delay in self.delays(): + for circ in (circ0, circ1): + assigned = circ.assign_parameters( + {circ.parameters[0]: timing.round_delay(time=delay / 2)}, inplace=False + ) + assigned.metadata["xval"] = 2 * timing.delay_time(time=delay / 2) + circs.append(assigned) + + return circs diff --git a/releasenotes/notes/zz-220e3c0894dd9076.yaml b/releasenotes/notes/zz-220e3c0894dd9076.yaml new file mode 100644 index 0000000000..e76c95ad6c --- /dev/null +++ b/releasenotes/notes/zz-220e3c0894dd9076.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + A new experiment :class:`.ZZRamsey` has been added to measure the ZZ + coefficient between a pair of qubits. diff --git a/test/library/characterization/test_zz_ramsey.py b/test/library/characterization/test_zz_ramsey.py new file mode 100644 index 0000000000..3041218302 --- /dev/null +++ b/test/library/characterization/test_zz_ramsey.py @@ -0,0 +1,126 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test ZZ Phase experiments.""" + +from itertools import product +from typing import Dict, List + +from test.base import QiskitExperimentsTestCase + +import numpy as np +from ddt import ddt, idata, unpack + +from qiskit import QuantumCircuit + +from qiskit_experiments.library import ZZRamsey +from qiskit_experiments.test.mock_iq_backend import MockIQBackend +from qiskit_experiments.test.mock_iq_helpers import MockIQExperimentHelper + + +class ZZRamseyHelper(MockIQExperimentHelper): + """A mock backend for the ZZRamsey experiment""" + + def __init__(self, zz: float, readout_error: float = 0): + super().__init__() + self.zz_freq = zz + self.readout_error = readout_error + + def compute_probabilities(self, circuits: List[QuantumCircuit]) -> List[Dict[str, float]]: + """Return the probability of the circuit.""" + + probabilities = [] + for circuit in circuits: + series = circuit.metadata["series"] + delay = circuit.metadata["xval"] + + if series == "0": + freq = (-1 * self.zz_freq) / 2 + else: + freq = self.zz_freq / 2 + rz, _, _ = next(i for i in circuit.data if i[0].name == "u1") + phase = float(rz.params[0]) + + prob1 = 0.5 - 0.5 * np.cos(2 * np.pi * freq * delay + phase) + + prob1 = prob1 * (1 - self.readout_error) + (1 - prob1) * self.readout_error + + probabilities.append({"0": 1 - prob1, "1": prob1}) + + return probabilities + + +@ddt +class TestZZRamsey(QiskitExperimentsTestCase): + """Tests for the ZZ Ramsey experiment.""" + + test_tol = 0.05 + + def test_circuits(self): + """Test circuit generation""" + backend = MockIQBackend(ZZRamseyHelper(zz=1e5, readout_error=0.01)) + + t_min = 0 + t_max = 5e-6 + num = 50 + + ramsey_min_max = ZZRamsey( + (0, 1), + backend, + min_delay=t_min, + max_delay=t_max, + num_delays=num, + ) + ramsey_with_delays = ZZRamsey( + (0, 1), + backend, + delays=ramsey_min_max.delays(), + ) + + # Sanity check that circuits are generated + self.assertEqual(len(ramsey_min_max.circuits()), num * 2) + # Test setting min/max and setting exact delays give same results + self.assertEqual(ramsey_min_max.circuits(), ramsey_with_delays.circuits()) + + ramsey_no_backend = ZZRamsey((0, 1), num_delays=50) + self.assertEqual(len(ramsey_no_backend.circuits()), 2 * 50) + + @idata(product([2e5, -3e5], [4, 5])) + @unpack + def test_end_to_end(self, zz_freq, num_rotations): + """Test that we can run on a mock backend and perform a fit.""" + backend = MockIQBackend(ZZRamseyHelper(zz_freq)) + # Use a small number of shots so that chi squared is low. For large + # number of shots, the uncertainty in the data points is very small and + # gives a large chi squared. + backend.options.shots = 40 + + ramsey = ZZRamsey((0, 1), backend, num_rotations=num_rotations) + test_data = ramsey.run() + self.assertExperimentDone(test_data) + + result = test_data.analysis_results("zz") + meas_shift = result.value.n + self.assertLess(abs(meas_shift - zz_freq), abs(self.test_tol * zz_freq)) + self.assertEqual(result.quality, "good") + + def test_experiment_config(self): + """Test config roundtrips""" + exp = ZZRamsey((0, 1)) + loaded_exp = ZZRamsey.from_config(exp.config()) + self.assertNotEqual(exp, loaded_exp) + self.assertTrue(self.json_equiv(exp, loaded_exp)) + + def test_roundtrip_serializable(self): + """Test round trip JSON serialization""" + exp = ZZRamsey((0, 1)) + self.assertRoundTripSerializable(exp, self.json_equiv)