diff --git a/docs/apidocs/calibration_management.rst b/docs/apidocs/calibration_management.rst deleted file mode 100644 index 0854a1127d..0000000000 --- a/docs/apidocs/calibration_management.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit-experiments-calibration-management: - -.. automodule:: qiskit_experiments.calibration_management - :no-members: - :no-inherited-members: - :no-special-members: \ No newline at end of file diff --git a/docs/apidocs/index.rst b/docs/apidocs/index.rst index fac3299b4d..386afbcde6 100644 --- a/docs/apidocs/index.rst +++ b/docs/apidocs/index.rst @@ -17,7 +17,6 @@ Package Modules library data_processing curve_analysis - calibration_management database_service visualization test @@ -28,9 +27,7 @@ Experiment Modules .. toctree:: :maxdepth: 1 - mod_calibration mod_characterization - mod_driven_freq_tuning mod_randomized_benchmarking mod_tomography mod_quantum_volume diff --git a/docs/apidocs/mod_calibration.rst b/docs/apidocs/mod_calibration.rst deleted file mode 100644 index 43f31a0c37..0000000000 --- a/docs/apidocs/mod_calibration.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit-experiments-calibration: - -.. automodule:: qiskit_experiments.library.calibration - :no-members: - :no-inherited-members: - :no-special-members: \ No newline at end of file diff --git a/docs/apidocs/mod_driven_freq_tuning.rst b/docs/apidocs/mod_driven_freq_tuning.rst deleted file mode 100644 index bdc7fa1462..0000000000 --- a/docs/apidocs/mod_driven_freq_tuning.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit-experiments-driven-freq-tuning: - -.. automodule:: qiskit_experiments.library.driven_freq_tuning - :no-members: - :no-inherited-members: - :no-special-members: diff --git a/docs/conf.py b/docs/conf.py index 9e85bd9c17..d3eb18b016 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -100,7 +100,6 @@ "manuals/characterization/t2ramsey": "_images/t2ramsey_4_0.png", "manuals/characterization/tphi": "_images/tphi_5_1.png", "manuals/characterization/t2hahn": "_images/t2hahn_5_0.png", - "manuals/characterization/stark_experiment": "_images/stark_experiment_1_0.png", "**": "_static/images/logo.png", } diff --git a/docs/manuals/characterization/stark_experiment.rst b/docs/manuals/characterization/stark_experiment.rst deleted file mode 100644 index c1683a2879..0000000000 --- a/docs/manuals/characterization/stark_experiment.rst +++ /dev/null @@ -1,365 +0,0 @@ -AC Stark Effect -=============== - -.. caution:: - - The experiments described in this manual are deprecated as of Qiskit - Experiments 0.8 and will be removed in a future release. They rely on Qiskit - Pulse, which is `deprecated in Qiskit SDK - `_, with planned removal in - Qiskit 2.0. - -When a qubit is driven with an off-resonant tone, -the qubit frequency :math:`f_0` is slightly shifted through what is known as the (AC) Stark effect. -This technique is sometimes used to characterize qubit properties in the vicinity of -the base frequency, especially with a fixed frequency qubit architecture which otherwise -doesn't have a knob to control frequency [1]_. - -The important control parameters of the Stark effect are the amplitude -:math:`\Omega` and frequency :math:`f_S` of -the off-resonant tone, which we will call the *Stark tone* in the following. -In the low power limit, the amount of frequency shift :math:`\delta f_S` -that the qubit may experience is described as follows [2]_: - -.. math:: - - \delta f_S \approx \frac{\alpha}{2\Delta\left(\alpha - \Delta\right)} \Omega^2, - -where :math:`\alpha` is the qubit anharmonicity and :math:`\Delta=f_S - f_0` is the -frequency separation of the Stark tone from the qubit frequency :math:`f_0`. -We sometimes call :math:`\delta f_S` the *Stark shift* [3]_. - - -.. _stark_tone_implementation: - -Stark tone implementation in Qiskit ------------------------------------ - -Usually, we fix the Stark tone frequency :math:`f_S` and control the amplitude :math:`\Omega` -to modulate the qubit frequency. -In Qiskit, we often use an abstracted amplitude :math:`\bar{\Omega}`, -instead of the physical amplitude :math:`\Omega` in the experiments. - -Because the Stark shift :math:`\delta f_S` has a quadratic dependence on -the tone amplitude :math:`\Omega`, the resulting shift is not sensitive to its sign. -On the other hand, the sign of the shift depends on the sign of the frequency offset :math:`\Delta`. -In a typical parameter regime of :math:`|\Delta | < | \alpha |`, - -.. math:: - - \text{sign}(\delta f_S) = - \text{sign}(\Delta). - -In other words, positive (negative) Stark shift occurs when the tone frequency :math:`f_S` -is lower (higher) than the qubit frequency :math:`f_0`. -When an experimentalist wants to perform spectroscopy of some qubit parameter -in the vicinity of :math:`f_0`, one must manage the sign of :math:`f_S` -in addition to the magnitude of :math:`\Omega`. - -To alleviate such experimental complexity, an abstracted amplitude :math:`\bar{\Omega}` -with virtual sign is introduced in Qiskit Experiments. -This works as follows: - -.. math:: - - \Delta &= - \text{sign}(\bar{\Omega}) | \Delta |, \\ - \Omega &= | \bar{\Omega} |. - -Stark experiments in Qiskit usually take two control parameters :math:`(\bar{\Omega}, |\Delta|)`, -which are specified by ``stark_amp`` and ``stark_freq_offset`` in the experiment options, respectively. -In this representation, the sign of the Stark shift matches the sign of :math:`\bar{\Omega}`. - -.. math:: - - \text{sign}(\delta f_S) = \text{sign}(\bar{\Omega}) - -This allows an experimentalist to control both the sign and the amount of -the Stark shift with the ``stark_amp`` experiment option. -Note that ``stark_freq_offset`` should be set as a positive number. - - -.. _stark_frequency_consideration: - -Stark tone frequency --------------------- - -As you can see in the equation for :math:`\delta f_S` above, -:math:`\Delta=0` yields a singular point where :math:`\delta f_S` diverges. -This corresponds to a Rabi drive, where the qubit is driven on resonance and -coherent state exchange occurs between :math:`|0\rangle` and :math:`|1\rangle` -instead of the Stark shift. -Another frequency that should be avoided for the Stark tone is :math:`\Delta=\alpha` which -corresponds to the transition from :math:`|1\rangle` to :math:`|2\rangle`. -In the high power limit, :math:`\Delta = \alpha/2` should also be avoided since -this causes the direct excitation from :math:`|0\rangle` to :math:`|2\rangle` -through what is known as a two-photon transition. - -The Stark tone frequency must be sufficiently separated from all of these frequencies -to avoid unwanted state transitions (frequency collisions). -In reality, the choice of the frequency could be even more complicated -due to the transition levels of the nearest neighbor qubits. -The frequency must be carefully chosen to avoid frequency collisions [4]_. - - -.. _stark_channel_consideration: - -Stark tone channel ------------------- - -It may be necessary to supply a pulse channel to apply the Stark tone. -In Qiskit Experiments, the Stark experiments usually have an experiment option ``stark_channel`` -to specify this. -By default, the Stark tone is applied to the same channel as the qubit drive -with a frequency shift. This frequency shift might update the channel frame, -which accumulates unwanted phase against the frequency difference between -the qubit drive :math:`f_0` and Stark tone frequencies :math:`f_S` in addition to -the qubit Stark shift :math:`\delta f_s`. -You can use a dedicated Stark drive channel if available. -Otherwise, you may want to use a control channel associated with the physical -drive port of the qubit. - -In a typical IBM device using the cross-resonance drive architecture, -such channel can be identified with your backend as follows: - -.. note:: - This tutorial requires the :external+qiskit_ibm_runtime:doc:`qiskit-ibm-runtime ` package to model a - backend. You can install it with ``python -m pip install qiskit-ibm-runtime``. - -.. jupyter-execute:: - - from qiskit_ibm_runtime.fake_provider import FakeHanoiV2 - - backend = FakeHanoiV2() - qubit = 0 - - for qpair in backend.coupling_map: - if qpair[0] == qubit: - break - - print(backend.control_channel(qpair)[0]) - -This returns a control channel for which the qubit is the control qubit. -This approach may not work for other device architectures. - - -Characterizing the frequency shift ----------------------------------- - -One can experimentally measure :math:`\delta f_S` with the :class:`.StarkRamseyXY` experiment. -The following pulse sequence illustrates how :math:`\delta f_S` is characterized -by a variant of the Hahn-echo pulse sequence [5]_. - -.. jupyter-execute:: - :hide-code: - - %matplotlib inline - - import warnings - - warnings.filterwarnings( - "ignore", - message=".*Due to the deprecation of Qiskit Pulse.*", - category=DeprecationWarning, - ) - warnings.filterwarnings( - "ignore", - message=".*The entire Qiskit Pulse package is being deprecated.*", - category=DeprecationWarning, - ) - - from qiskit_experiments.library import StarkRamseyXY - from qiskit import schedule, pulse - from qiskit_ibm_runtime.fake_provider import FakeHanoiV2 - from qiskit.visualization.pulse_v2 import IQXSimple - - backend = FakeHanoiV2() - exp = StarkRamseyXY( - physical_qubits=[0], - backend=backend, - stark_amp=0.2, - delays=[100e-9], - stark_channel=pulse.ControlChannel(0), - ) - - circ = exp.circuits()[0] - ram_x_schedule = schedule(circ, backend=backend) - - opt = { - "formatter.general.fig_chart_height": 10, - "formatter.margin.top": 0.1, - "formatter.margin.bottom": 0.2, - "formatter.label_offset.pulse_name": 0.1, - "formatter.text_size.annotate": 14, - } - ram_x_schedule.draw(time_range=(0, 1600), style=IQXSimple(**opt)) - -The qubit is initialized in the :math:`Y`-eigenstate with the first half-pi pulse. -This state may be visualized by a Bloch vector located on the equator of the Bloch sphere, -which is highly sensitive to Z rotation arising from any qubit frequency offset. -This operation is followed by a pi-pulse and another negative half-pi pulse -right before the measurement tone filled in red. -This sequence recovers the initial state when Z rotation is zero or :math:`\delta f_S=0`. - -As you may notice, this sequence is interleaved with two pulses labeled -"StarkV" (Gaussian) and "StarkU" (GaussianSquare) filled in yellow, representing Stark tones. -These pulses are designed to have the same maximum amplitude :math:`\Omega` resulting -in the same :math:`\delta f_S` at this amplitude -- but why do we need two pulses? - -Since :math:`\delta f_S` is amplitude dependent, the Stark pulses cause time-dependent -frequency shifts during the pulse ramps. With a single Stark tone, you are only able to estimate -the average :math:`\delta f_S` over the history of amplitudes :math:`\Omega(t)`, -even though you may want to characterize :math:`\delta f_S` at a particular :math:`\Omega`. -You have to remember that you cannot use a square envelope to set a uniform amplitude, -because the sharp rise and fall of the pulse amplitude has a broad frequency spectrum -which could produce unwanted excitations. - -The pulse sequence shown above is adopted to address such issue. -The Z rotation accumulated by the first pulse is proportional to :math:`\int \Omega_V^2(t) dt`, -while that of the second pulse is :math:`-\int \Omega_U^2(t) dt` because -the qubit state is flipped by the pi-pulse in the middle, -flipping the sense of rotation of the state even though -the actual rotation direction is the same for both pulses. -The only difference between :math:`\Omega_U(t)` and :math:`\Omega_V(t)` is the flat-top part -with constant amplitude :math:`\Omega` and duration :math:`t_w`, -where :math:`\delta f_S` is also constant. -Thanks to this sign flip, the net Z rotation :math:`\theta` accumulated through the two pulses is -proportional to only the flat-top part of the StarkU pulse. - -.. math:: - - \theta = 2 \pi \int \delta f_S(t) dt - \propto \int \Omega_U^2(t) dt - \int \Omega_V^2(t) dt - = \Omega^2 t_w - -This technique allows you to estimate :math:`\delta f_S` at a particular :math:`\Omega`. - -In Qiskit Experiments, the experiment option ``stark_amp`` usually refers to -the height of this GaussianSquare flat-top. - - -Workflow --------- - -In this example, you'll learn how to measure a spectrum of qubit relaxation versus -frequency with fixed frequency transmons. -As you already know, we give an offset to the qubit frequency with a Stark tone, -and the workflow starts from characterizing the amount of the Stark shift against -the Stark amplitude :math:`\bar{\Omega}` that you can experimentally control. - -.. jupyter-input:: - - from qiskit_experiments.library.driven_freq_tuning import StarkRamseyXYAmpScan - - exp = StarkRamseyXYAmpScan((0,), backend=backend) - exp_data = exp.run().block_for_results() - coefficients = exp_data.analysis_results("stark_coefficients").value - -You first need to run the :class:`.StarkRamseyXYAmpScan` experiment that scans :math:`\bar{\Omega}` -and estimates the amount of the resultant frequency shift. -This experiment fits the frequency shift to a polynomial model which is a function of :math:`\bar{\Omega}`. -You can obtain the :class:`.StarkCoefficients` object that contains -all polynomial coefficients to map and reverse-map the :math:`\bar{\Omega}` to a corresponding frequency value. - -This object may be necessary for the following spectroscopy experiment. -Since Stark coefficients are stable for a relatively long time, -you may want to save the coefficient values and load them later when you run the experiment. -If you have an access to the Experiment service, you can just save the experiment result. - -.. jupyter-input:: - - exp_data.save() - -.. jupyter-output:: - - You can view the experiment online at https://quantum.ibm.com/experiments/23095777-be28-4036-9c98-89d3a915b820 - - -Otherwise, you can dump the coefficient object into a file with JSON format. - -.. jupyter-input:: - - import json - from qiskit_experiments.framework import ExperimentEncoder - - with open("coefficients.json", "w") as fp: - json.dump(ret_coeffs, fp, cls=ExperimentEncoder) - -The saved object can be retrieved either from the service or file, as follows. - -.. jupyter-input:: - - # When you have access to Experiment service - from qiskit_experiments.library.driven_freq_tuning import retrieve_coefficients_from_backend - - coefficients = retrieve_coefficients_from_backend(backend, 0) - - # Alternatively you can load from file - from qiskit_experiments.framework import ExperimentDecoder - - with open("coefficients.json", "r") as fp: - coefficients = json.load(fp, cls=ExperimentDecoder) - -Now you can measure the qubit relaxation spectrum. -The :class:`.StarkP1Spectroscopy` experiment also scans :math:`\bar{\Omega}`, -but instead of measuring the frequency shift, it measures the excited state population P1 -after certain delay, :code:`t1_delay` in the experiment options, following the state population. -You can scan the :math:`\bar{\Omega}` values either in the "frequency" or "amplitude" domain, -but the :code:`stark_coefficients` option must be set to perform the frequency sweep. - -.. jupyter-input:: - - from qiskit_experiments.library.driven_freq_tuning import StarkP1Spectroscopy - - exp = StarkP1Spectroscopy((0,), backend=backend) - - exp.set_experiment_options( - t1_delay=20e-6, - min_xval=-20e6, - max_xval=20e6, - xval_type="frequency", - spacing="linear", - stark_coefficients=coefficients, - ) - - exp_data = exp.run().block_for_results() - -You may find notches in the P1 spectrum, which may indicate the existence of TLS's -in the vicinity of your qubit drive frequency. - -.. jupyter-input:: - - exp_data.figure(0) - -.. image:: ./stark_experiment_example.png - -Note that this experiment doesn't yield any analysis result because the landscape of a P1 spectrum -can not be predicted due to the random occurrences of the TLS and frequency collisions. -If you have your own protocol to extract meaningful quantities from the data, -you can write a custom analysis subclass and give it to the experiment instance before execution. -See :class:`.StarkP1SpectAnalysis` for more details. - -This protocol can be parallelized among many qubits unless crosstalk matters. - - -References ----------- - -.. [1] Malcolm Carroll, Sami Rosenblatt, Petar Jurcevic, Isaac Lauer and Abhinav Kandala, - Dynamics of superconducting qubit relaxation times, npj Quantum Inf 8, 132 (2022). - https://arxiv.org/abs/2105.15201 - -.. [2] Easwar Magesan, Jay M. Gambetta, Effective Hamiltonian models of the cross-resonance gate, - Phys. Rev. A 101, 052308 (2020). - https://arxiv.org/abs/1804.04073 - -.. [3] Wikipedia. "Autler–Townes effect" Wikipedia Foundation. - https://en.wikipedia.org/wiki/Autler%E2%80%93Townes_effect - -.. [4] Jared B. Hertzberg, Eric J. Zhang, Sami Rosenblatt, et. al., - Laser-annealing Josephson junctions for yielding scaled-up superconducting quantum processors, - npj Quantum Information 7, 129 (2021). - https://arxiv.org/abs/2009.00781 - -.. [5] J. Stehlik, D. M. Zajac, D. L. Underwood, et.al., - Tunable Coupling Architecture for Fixed-Frequency Transmon Superconducting Qubits, - Phys. Rev. Lett. 127, 080505 (2021). - https://arxiv.org/abs/2101.07746 diff --git a/docs/manuals/characterization/stark_experiment_example.png b/docs/manuals/characterization/stark_experiment_example.png deleted file mode 100644 index 5d99b87fe5..0000000000 Binary files a/docs/manuals/characterization/stark_experiment_example.png and /dev/null differ diff --git a/docs/manuals/measurement/restless_measurements.rst b/docs/manuals/measurement/restless_measurements.rst index 8966bdb4c3..46c49924f0 100644 --- a/docs/manuals/measurement/restless_measurements.rst +++ b/docs/manuals/measurement/restless_measurements.rst @@ -92,26 +92,21 @@ they use always starts with the qubits in the ground state. from qiskit_ibm_runtime.fake_provider import FakePerth - from qiskit_experiments.library import RoughDragCal - from qiskit_experiments.calibration_management import ( - Calibrations, - FixedFrequencyTransmon, - ) + from qiskit_experiments.library import FineSXDrag from qiskit_experiments.data_processing.data_processor import DataProcessor # replace this lines with an IBM Quantum backend to run the experiment. backend = FakePerth() - cals = Calibrations.from_backend(backend, libraries=[FixedFrequencyTransmon()]) # Define the experiment qubit = 2 - cal_drag = RoughDragCal((qubit,), cals, schedule_name='sx', backend=backend) + exp = FineSXDrag((qubit,), backend=backend) # Enable restless measurements by setting the run options and data processor - cal_drag.enable_restless(rep_delay=1e-6) + exp.enable_restless(rep_delay=1e-6) - print(cal_drag.analysis.options.data_processor) - print(cal_drag.run_options) + print(exp.analysis.options.data_processor) + print(exp.run_options) As you can see, a restless data processor is automatically chosen for the experiment. This data processor post-processes the restless measured shots according to the order in which @@ -125,7 +120,7 @@ in a restless mode. With a hardware backend, this would be done by calling the .. jupyter-input:: - drag_data_restless = cal_drag.run() + drag_data_restless = exp.run() As shown by the example, the code is identical to running a normal experiment aside from a call to the method :meth:`~.RestlessMixin.enable_restless`. Note that you can also choose to keep @@ -142,11 +137,11 @@ the standard data processor by providing it to the analysis options and telling # define a standard data processor. standard_processor = DataProcessor("counts", [Probability("1")]) - cal_drag = RoughDragCal((qubit,), cals, schedule_name='sx', backend=backend) - cal_drag.analysis.set_options(data_processor=standard_processor) + exp = FineSXDrag((qubit,), backend=backend) + exp.analysis.set_options(data_processor=standard_processor) # enable restless mode and set override_processor_by_restless to False. - cal_drag.enable_restless(rep_delay=1e-6, override_processor_by_restless=False) + exp.enable_restless(rep_delay=1e-6, override_processor_by_restless=False) If you run the experiment in this setting you will see that the data is often unusable which illustrates the importance of the data processing. As detailed @@ -188,17 +183,14 @@ using the code below. .. jupyter-execute:: - from qiskit import schedule, transpile - from qiskit_experiments.framework import BackendData + from qiskit import transpile - dt = BackendData(backend).dt - inst_map = backend.instruction_schedule_map - meas_length = inst_map.get("measure", (qubit,)).duration * dt + meas_length = backend.target["measure"][(qubit,)].duration # Compute the average duration of all circuits # Remove measurement instructions circuits = [] - for qc in cal_drag.circuits(): + for qc in exp.circuits(): qc.remove_final_measurements(inplace=True) circuits.append(qc) @@ -208,13 +200,13 @@ using the code below. backend, initial_layout=[qubit], scheduling_method="alap", - **cal_drag.transpile_options.__dict__, + **exp.transpile_options.__dict__, ) durations = [c.duration for c in executed_circs] - tau = sum(durations) * dt / (len(durations)) + tau = sum(durations) * backend.dt / (len(durations)) - n_circs = len(cal_drag.circuits()) + n_circs = len(exp.circuits()) # can be obtained from backend.default_rep_delay on a backend from qiskit-ibm-runtime delay_s = 0.0025 diff --git a/docs/tutorials/calibrations.rst b/docs/tutorials/calibrations.rst deleted file mode 100644 index 799f56dfbd..0000000000 --- a/docs/tutorials/calibrations.rst +++ /dev/null @@ -1,521 +0,0 @@ -Calibrations: Schedules and gate parameters from experiments -============================================================ - -.. caution:: - - Support for calibrating pulses is deprecated as of Qiskit Experiments 0.8 - and will be removed in a future version. There is no alternative support - path because Qiskit Pulse is `deprecated in Qiskit SDK - `_ with planned removal in - Qiskit 2.0. - -To produce high fidelity quantum operations, we want to be able to run good gates. The -calibration module in Qiskit Experiments allows users to run experiments to find the -pulse shapes and parameter values that maximize the fidelity of the resulting quantum -operations. Calibration experiments encapsulate the internal processes and allow -experimenters to perform calibration operations in a quicker way. Without the -experiments module, we would need to define pulse schedules and plot the resulting -measurement data manually. - -In this tutorial, we demonstrate how to calibrate single-qubit gates using the -calibration framework in Qiskit Experiments. We will run experiments on our test pulse -backend, :class:`.SingleTransmonTestBackend`, a backend that simulates the underlying -pulses with :mod:`qiskit_dynamics` on a three-level model of a transmon. You can also -run these experiments on any real backend with Pulse enabled (see -:class:`qiskit.providers.models.BackendConfiguration`). - -We will run experiments to -find the qubit frequency, calibrate the amplitude of DRAG pulses, and choose the value -of the DRAG parameter that minimizes leakage. The calibration framework requires -the user to: - -- Set up an instance of :class:`.Calibrations`, - -- Run calibration experiments found in :mod:`qiskit_experiments.library.calibration`. - -Note that the values of the parameters stored in the instance of the :class:`.Calibrations` class -will automatically be updated by the calibration experiments. -This automatic updating can also be disabled using the ``auto_update`` flag. - -.. note:: - This tutorial requires the :mod:`qiskit_dynamics` package to run simulations. - You can install it with ``python -m pip install qiskit-dynamics``. - -.. jupyter-execute:: - :hide-code: - - import warnings - - warnings.filterwarnings( - "ignore", - message=".*Due to the deprecation of Qiskit Pulse.*", - category=DeprecationWarning, - ) - warnings.filterwarnings( - "ignore", - message=".*The entire Qiskit Pulse package is being deprecated.*", - category=DeprecationWarning, - ) - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - -.. jupyter-execute:: - - import pandas as pd - import numpy as np - import qiskit.pulse as pulse - from qiskit.circuit import Parameter - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit import schedule - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - -.. jupyter-execute:: - - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, noise=False, seed=100) - qubit = 0 - cals=Calibrations.from_backend(backend) - print(cals.get_inst_map()) - -The two functions below show how to set up an instance of :class:`.Calibrations`. -To do this the user defines the template schedules to calibrate. -These template schedules are fully parameterized, even the channel indices -on which the pulses are played. Furthermore, the name of the parameter in the channel -index must follow the convention laid out in the documentation -of the calibration module. Note that the parameters in the channel indices -are automatically mapped to the channel index when :meth:`.Calibrations.get_schedule` is called. - -.. jupyter-execute:: - - # A function to instantiate calibrations and add a couple of template schedules. - def setup_cals(backend) -> Calibrations: - - cals = Calibrations.from_backend(backend) - - dur = Parameter("dur") - amp = Parameter("amp") - sigma = Parameter("σ") - beta = Parameter("β") - drive = pulse.DriveChannel(Parameter("ch0")) - - # Define and add template schedules. - with pulse.build(name="xp") as xp: - pulse.play(pulse.Drag(dur, amp, sigma, beta), drive) - - with pulse.build(name="xm") as xm: - pulse.play(pulse.Drag(dur, -amp, sigma, beta), drive) - - with pulse.build(name="x90p") as x90p: - pulse.play(pulse.Drag(dur, Parameter("amp"), sigma, Parameter("β")), drive) - - cals.add_schedule(xp, num_qubits=1) - cals.add_schedule(xm, num_qubits=1) - cals.add_schedule(x90p, num_qubits=1) - - return cals - - # Add guesses for the parameter values to the calibrations. - def add_parameter_guesses(cals: Calibrations): - - for sched in ["xp", "x90p"]: - cals.add_parameter_value(80, "σ", schedule=sched) - cals.add_parameter_value(0.5, "β", schedule=sched) - cals.add_parameter_value(320, "dur", schedule=sched) - cals.add_parameter_value(0.5, "amp", schedule=sched) - -When setting up the calibrations we add three pulses: a :math:`\pi`-rotation, -with a schedule named ``xp``, a schedule ``xm`` identical to ``xp`` -but with a nagative amplitude, and a :math:`\pi/2`-rotation, with a schedule -named ``x90p``. Here, we have linked the amplitude of the ``xp`` and ``xm`` pulses. -Therefore, calibrating the parameters of ``xp`` will also calibrate -the parameters of ``xm``. - -.. jupyter-execute:: - - cals = setup_cals(backend) - add_parameter_guesses(cals) - -A similar setup is achieved by using a pre-built library of gates. -The library of gates provides a standard set of gates and some initial guesses -for the value of the parameters in the template schedules. -This is shown below using the ``FixedFrequencyTransmon`` library which provides the ``x``, -``y``, ``sx``, and ``sy`` pulses. Note that in the example below -we change the default value of the pulse duration to 320 samples - -.. jupyter-execute:: - - from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon - - library = FixedFrequencyTransmon(default_values={"duration": 320}) - cals = Calibrations.from_backend(backend, libraries=[library]) - print(library.default_values()) # check what parameter values this library has - print(cals.get_inst_map()) # check the new cals's InstructionScheduleMap made from the library - print(cals.get_schedule('x',(0,))) # check one of the schedules built from the new calibration - -We are going to run the spectroscopy, Rabi, DRAG, and fine amplitude calibration experiments -one after another and update the parameters after every experiment, keeping track of -parameter values. - -Finding qubits with spectroscopy --------------------------------- - -Here, we are using a backend for which we already know the qubit frequency. -We will therefore use the spectroscopy experiment to confirm that -there is a resonance at the qubit frequency reported by the backend. - -.. jupyter-execute:: - - from qiskit_experiments.library.calibration.rough_frequency import RoughFrequencyCal - -We first show the contents of the calibrations for qubit 0. -Note that the guess values that we added before apply to all qubits on the chip. -We see this in the table below as an empty tuple ``()`` in the qubits column. -Observe that the parameter values of ``y`` do not appear in this table as they are given by the values of ``x``. - -.. jupyter-execute:: - :hide-code: - :hide-output: - - # dataframe styling - pd.set_option('display.precision', 5) - pd.set_option('display.html.border', 1) - pd.set_option('display.max_colwidth', 24) - -.. jupyter-execute:: - - columns_to_show = ["parameter", "qubits", "schedule", "value", "date_time"] - pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()]))[columns_to_show] - -Instantiate the experiment and draw the first circuit in the sweep: - -.. jupyter-execute:: - - freq01_estimate = backend.defaults().qubit_freq_est[qubit] - frequencies = np.linspace(freq01_estimate-15e6, freq01_estimate+15e6, 51) - spec = RoughFrequencyCal((qubit,), cals, frequencies, backend=backend) - spec.set_experiment_options(amp=0.005) - -.. jupyter-execute:: - - circuit = spec.circuits()[0] - circuit.draw(output="mpl", style="iqp") - -We can also visualize the pulse schedule for the circuit: - -.. jupyter-execute:: - - next(iter(circuit.calibrations["Spec"].values())).draw() - circuit.calibrations["Spec"] - -Run the calibration experiment: - -.. jupyter-execute:: - - spec_data = spec.run().block_for_results() - spec_data.figure(0) - - -.. jupyter-execute:: - - print(spec_data.analysis_results("f01")) - - -The instance of ``calibrations`` has been automatically updated with the measured -frequency, as shown below. In addition to the columns shown below, ``calibrations`` also -stores the group to which a value belongs, whether a value is valid or not, and the -experiment id that produced a value. - -.. jupyter-execute:: - - pd.DataFrame(**cals.parameters_table(qubit_list=[qubit]))[columns_to_show] - -.. _Rabi Calibration: - -Calibrating the pulse amplitudes with a Rabi experiment -------------------------------------------------------- - -In the Rabi experiment we apply a pulse at the frequency of the qubit -and scan its amplitude to find the amplitude that creates a rotation -of a desired angle. We do this with the calibration experiment :class:`.RoughXSXAmplitudeCal`. -This is a specialization of the :class:`.Rabi` experiment that will update the calibrations -for both the :math:`X` pulse and the :math:`SX` pulse using a single experiment. - -.. jupyter-execute:: - - from qiskit_experiments.library.calibration import RoughXSXAmplitudeCal - rabi = RoughXSXAmplitudeCal((qubit,), cals, backend=backend, amplitudes=np.linspace(-0.1, 0.1, 51)) - -The rough amplitude calibration is therefore a Rabi experiment in which -each circuit contains a pulse with a gate. Different circuits correspond to pulses -with different amplitudes. - -.. jupyter-execute:: - - rabi.circuits()[0].draw(output="mpl", style="iqp") - -After the experiment completes the value of the amplitudes in the calibrations -will automatically be updated. This behaviour can be controlled using the ``auto_update`` -argument given to the calibration experiment at initialization. - -.. jupyter-execute:: - - rabi_data = rabi.run().block_for_results() - rabi_data.figure(0) - -.. jupyter-execute:: - - print(rabi_data.analysis_results("rabi_rate")) - -.. jupyter-execute:: - - pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))[columns_to_show] - -The table above shows that we have now updated the amplitude of our :math:`\pi` pulse -from 0.5 to the value obtained in the most recent Rabi experiment. -Importantly, since we linked the amplitudes of the ``x`` and ``y`` schedules -we will see that the amplitude of the ``y`` schedule has also been updated -as seen when requesting schedules from the :class:`.Calibrations` instance. -Furthermore, we used the result from the Rabi experiment to also update -the value of the ``sx`` pulse. - -.. jupyter-execute:: - - cals.get_schedule("sx", qubit) - -.. jupyter-execute:: - - cals.get_schedule("x", qubit) - -.. jupyter-execute:: - - cals.get_schedule("y", qubit) - -Saving and loading calibrations -------------------------------- - -The values of the calibrated parameters can be saved to a .csv file -and reloaded at a later point in time. - -.. jupyter-input:: - - cals.save(file_type="csv", overwrite=True, file_prefix="PulseBackend") - -After saving the values of the parameters you may restart your kernel. If you do so, -you will only need to run the following cell to recover the state of your calibrations. -Since the schedules are currently not stored we need to call our ``setup_cals`` function -or use a library to populate an instance of Calibrations with the template schedules. -By contrast, the value of the parameters will be recovered from the file. - -.. jupyter-input:: - - cals = Calibrations.from_backend(backend, library) - cals.load_parameter_values(file_name="PulseBackendparameter_values.csv") - -.. jupyter-execute:: - - pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))[columns_to_show] - -.. _DRAG Calibration: - -Calibrating the value of the DRAG coefficient ---------------------------------------------- - -A Derivative Removal by Adiabatic Gate (DRAG) pulse is designed to minimize leakage -and phase errors to a neighbouring transition. It is a standard pulse with an additional -derivative component. It is designed to reduce the frequency spectrum of a -normal pulse near the :math:`|1\rangle - |2\rangle` transition, -reducing the chance of leakage to the :math:`|2\rangle` state. -The optimal value of the DRAG parameter is chosen to minimize both -leakage and phase errors resulting from the AC Stark shift. -The pulse envelope is :math:`f(t)=\Omega_x(t)+j\beta\frac{\rm d}{{\rm d}t}\Omega_x(t)`. -Here, :math:`\Omega_x(t)` is the envelop of the in-phase component -of the pulse and :math:`\beta` is the strength of the quadrature -which we refer to as the DRAG parameter and seek to calibrate -in this experiment. The DRAG calibration will run several -series of circuits. In a given circuit a Rp(β) - Rm(β) block -is repeated :math:`N` times. Here, Rp is a rotation -with a positive angle and Rm is the same rotation with a -negative amplitude. - -.. jupyter-execute:: - - from qiskit_experiments.library import RoughDragCal - cal_drag = RoughDragCal([qubit], cals, backend=backend, betas=np.linspace(-20, 20, 25)) - cal_drag.set_experiment_options(reps=[3, 5, 7]) - cal_drag.circuits()[5].draw(output="mpl", style="iqp") - -.. jupyter-execute:: - - drag_data = cal_drag.run().block_for_results() - drag_data.figure(0) - -.. jupyter-execute:: - - print(drag_data.analysis_results("beta")) - -.. jupyter-execute:: - - pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="β"))[columns_to_show] - -.. _fine-amplitude-cal: - -Fine calibrations of a pulse amplitude --------------------------------------- - -The amplitude of a pulse can be precisely calibrated using error amplifying gate -sequences. These gate sequences apply the same gate a variable number of times. -Therefore, if each gate has a small error :math:`d\theta` in the rotation angle then a -sequence of :math:`n` gates will have a rotation error of :math:`n` * :math:`d\theta`. -The :class:`.FineAmplitude` experiment and its subclass experiments implements these -sequences to obtain the correction value of imperfect pulses. We will first examine how -to detect imperfect pulses using the characterization version of these experiments, then -update calibrations with a calibration experiment. - -.. jupyter-execute:: - - from qiskit.pulse import InstructionScheduleMap - from qiskit_experiments.library import FineXAmplitude - -Detecting over- and under-rotated pulses -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -We now run the error amplifying experiments with our own pulse schedules on which we -purposefully add over- and under-rotations to observe their effects. To do this, we -create an instruction to schedule map which we populate with the schedules we wish to -work with. This instruction schedule map is then given to the transpile options of the -experiment so that the Qiskit transpiler can attach the pulse schedules to the gates in -the experiments. We base all our pulses on the default :math:`X` pulse of -:class:`.SingleTransmonTestBackend`. - -.. jupyter-execute:: - - x_pulse = backend.defaults().instruction_schedule_map.get('x', (qubit,)).instructions[0][1].pulse - d0, inst_map = pulse.DriveChannel(qubit), pulse.InstructionScheduleMap() - - -We now take the ideal :math:`X` pulse amplitude reported by the backend and add/subtract -a 2% over/underrotation to it by scaling the ideal amplitude and see if the experiment -can detect this over/underrotation. We replace the default :math:`X` pulse in the -instruction schedule map with this over/under-rotated pulse. - -.. jupyter-execute:: - - ideal_amp = x_pulse.amp - over_amp = ideal_amp*1.02 - under_amp = ideal_amp*0.98 - print(f"The reported amplitude of the X pulse is {ideal_amp:.4f} which we set as ideal_amp.") - print(f"we use {over_amp:.4f} amplitude for overrotation pulse and {under_amp:.4f} for underrotation pulse.") - # build the over rotated pulse and add it to the instruction schedule map - with pulse.build(backend=backend, name="x") as x_over: - pulse.play(pulse.Drag(x_pulse.duration, over_amp, x_pulse.sigma, x_pulse.beta), d0) - inst_map.add("x", (qubit,), x_over) - -Let's look at one of the circuits of the :class:`.FineXAmplitude` experiment. To -calibrate the :math:`X` gate, we add an :math:`SX` gate before the :math:`X` gates to -move the ideal population to the equator of the Bloch sphere where the sensitivity to -over/under rotations is the highest. - -.. jupyter-execute:: - - overamp_exp = FineXAmplitude((qubit,), backend=backend) - overamp_exp.set_transpile_options(inst_map=inst_map) - overamp_exp.circuits()[4].draw(output="mpl", style="iqp") - -.. jupyter-execute:: - - # do the experiment - exp_data_over = overamp_exp.run(backend).block_for_results() - exp_data_over.figure(0) - -The ping-pong pattern on the figure indicates an over-rotation which makes the initial -state rotate more than :math:`\pi`. - -We now look at a pulse with an under rotation to see how the :class:`.FineXAmplitude` -experiment detects this error. We will compare the results to the over-rotation above. - -.. jupyter-execute:: - - # build the under rotated pulse and add it to the instruction schedule map - with pulse.build(backend=backend, name="x") as x_under: - pulse.play(pulse.Drag(x_pulse.duration, under_amp, x_pulse.sigma, x_pulse.beta), d0) - inst_map.add("x", (qubit,), x_under) - - # do the experiment - underamp_exp = FineXAmplitude((qubit,), backend=backend) - underamp_exp.set_transpile_options(inst_map=inst_map) - - exp_data_under = underamp_exp.run(backend).block_for_results() - exp_data_under.figure(0) - -Similarly to the over-rotation, the under-rotated pulse creates qubit populations that -do not lie on the equator of the Bloch sphere. However, compared to the ping-pong -pattern of the over rotated pulse, the under rotated pulse produces an inverted -ping-pong pattern. This allows us to determine not only the magnitude of the rotation -error but also its sign. - -.. jupyter-execute:: - - # analyze the results - target_angle = np.pi - dtheta_over = exp_data_over.analysis_results("d_theta").value.nominal_value - scale_over = target_angle / (target_angle + dtheta_over) - dtheta_under = exp_data_under.analysis_results("d_theta").value.nominal_value - scale_under = target_angle / (target_angle + dtheta_under) - print(f"The ideal angle is {target_angle:.2f} rad. We measured a deviation of {dtheta_over:.3f} rad in over-rotated pulse case.") - print(f"Thus, scale the {over_amp:.4f} pulse amplitude by {scale_over:.3f} to obtain {over_amp*scale_over:.5f}.") - print(f"On the other hand, we measured a deviation of {dtheta_under:.3f} rad in under-rotated pulse case.") - print(f"Thus, scale the {under_amp:.4f} pulse amplitude by {scale_under:.3f} to obtain {under_amp*scale_under:.5f}.") - - -Calibrating a :math:`\pi`/2 :math:`X` pulse -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Now we apply the same principles to a different example using the calibration version of -a Fine Amplitude experiment. The amplitude of the :math:`SX` gate, which is an :math:`X` -pulse with half the amplitude, is calibrated with the :class:`.FineSXAmplitudeCal` -experiment. Unlike the :class:`.FineSXAmplitude` experiment, the -:class:`.FineSXAmplitudeCal` experiment does not require other gates than the :math:`SX` -gate since the number of repetitions can be chosen such that the ideal population is -always on the equator of the Bloch sphere. To demonstrate the -:class:`.FineSXAmplitudeCal` experiment, we create a :math:`SX` pulse by dividing the -amplitude of the X pulse by two. We expect that this pulse might have a small rotation -error which we want to correct. - -.. jupyter-execute:: - - from qiskit_experiments.library import FineSXAmplitudeCal - - amp_cal = FineSXAmplitudeCal((qubit,), cals, backend=backend, schedule_name="sx") - amp_cal.circuits()[4].draw(output="mpl", style="iqp") - -Let's run the calibration experiment: - -.. jupyter-execute:: - - exp_data_x90p = amp_cal.run().block_for_results() - exp_data_x90p.figure(0) - -Observe, once again, that the calibrations have automatically been updated. - -.. jupyter-execute:: - - pd.DataFrame(**cals.parameters_table(qubit_list=[qubit, ()], parameters="amp"))[columns_to_show] - -.. jupyter-execute:: - - cals.get_schedule("sx", qubit) - -If we run the experiment again, we expect to see that the updated calibrated gate will -have a smaller :math:`d\theta` error: - -.. jupyter-execute:: - - exp_data_x90p_rerun = amp_cal.run().block_for_results() - exp_data_x90p_rerun.figure(0) - -See also --------- - -* API documentation: :mod:`~qiskit_experiments.calibration_management` and :mod:`~qiskit_experiments.library.calibration` -* Qiskit Textbook: `Calibrating Qubits with Qiskit Pulse `__ - - - diff --git a/docs/tutorials/curve_analysis.rst b/docs/tutorials/curve_analysis.rst index 828ad83740..ccf61cf599 100644 --- a/docs/tutorials/curve_analysis.rst +++ b/docs/tutorials/curve_analysis.rst @@ -335,21 +335,16 @@ analysis classes can be distinguished. Note that :meth:`.ScatterTable.add_row` allows for curve analysis subclasses to set arbitrary values for `series_name`, `series_id`, and `category` as -appropriate. -An analysis class may override some of the default curve analysis methods and -add additional `category` labels or define other `series` not named after a -model. -For example, :class:`.StarkRamseyXYAmpScanAnalysis` defines four `series` -labels in its ``data_subfit_map`` option (``Xpos``, ``Ypos``, ``Xneg``, -``Yneg``) but only two models (``FREQpos``, ``FREQneg``) whose names do not -match the series labels. -It does this by overriding the ``CurveAnalysis._format_data()`` method and adding -its own series to the :class:`.ScatterTable` with series labels to match its -fit model names (by combining ``Xpos`` and ``Ypos`` series data into a -``FREQpos`` series and similary for the series with names ending with ``neg``). -It sets a custom category, `"freq"`, for its series but also sets the -``fit_category`` analysis option to `"freq"` so that normal curve fitting is -performed on this custom ``"freq"`` series data. +appropriate. An analysis class may override some of the default curve analysis +methods and add additional `category` labels or define other `series` not named +after a model. For example, an analysis class can define define some `series` +labels in its ``data_subfit_map`` option that differ from the names of the +``models`` passed to ``CurveAnalysis.__init__()`` by overriding the +``CurveAnalysis._format_data()`` method and adding its own series to the +:class:`.ScatterTable` with series labels to match its fit model names. These +added series can be given a custom ``category`` which matches what is set for +the ``fit_category`` analysis option so that normal curve fitting is performed +on this custom series data instead of the series in ``data_subfit_map``. The (`series`, `category`, `analysis`) triplet can be used to extract data points that belong to a particular categorized series. For example, diff --git a/docs/tutorials/custom_experiment.rst b/docs/tutorials/custom_experiment.rst index 0a1a50b4f9..fcac688927 100644 --- a/docs/tutorials/custom_experiment.rst +++ b/docs/tutorials/custom_experiment.rst @@ -47,8 +47,8 @@ Optionally, to allow configuring experiment and execution options, you can overr .. note:: Qiskit Experiments supports experiments on non-qubit components defined as subclasses of - :class:`.DeviceComponent`, such as the :class:`.Resonator` in the :class:`.ResonatorSpectroscopy` - experiment. If you would like to work on these components in your experiment, you should override + :class:`.DeviceComponent`, such as the :class:`.Resonator`. + If you would like to work on these components in your experiment, you should override ``_metadata()`` to populate ``device_components`` with these components. Here is an example for an experiment that takes in :class:`.Resonator` components: diff --git a/docs/tutorials/data_processor.rst b/docs/tutorials/data_processor.rst index 2468cecbfd..3b42e000d0 100644 --- a/docs/tutorials/data_processor.rst +++ b/docs/tutorials/data_processor.rst @@ -61,12 +61,8 @@ of the data processor. Crucially, the output of one node in the list is the input to the next node in the list. To illustrate the data processing module, we consider an example -in which we measure a rabi oscillation with different data levels. -The code below sets up the Rabi experiment. - -.. note:: - This tutorial requires the :mod:`qiskit_dynamics` package to run simulations. - You can install it with ``python -m pip install qiskit-dynamics``. +in which we measure qubit relaxation with different data levels. +The code below sets up the :class:`.T1` experiment. .. jupyter-execute:: :hide-code: @@ -88,29 +84,21 @@ The code below sets up the Rabi experiment. import numpy as np - from qiskit import pulse - from qiskit.circuit import Parameter - - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend + from qiskit_experiments.test.mock_iq_backend import MockIQBackend + from qiskit_experiments.test.mock_iq_helpers import MockIQT1Helper from qiskit_experiments.data_processing import DataProcessor, nodes - from qiskit_experiments.library import Rabi + from qiskit_experiments.library import T1 - with pulse.build() as sched: - pulse.play( - pulse.Gaussian(160, Parameter("amp"), sigma=40), - pulse.DriveChannel(0) - ) - backend = SingleTransmonTestBackend(seed=100) + backend = MockIQBackend(MockIQT1Helper(t1=90e-6, iq_cluster_centers=[((-1, 1), (1, 1))])) - exp = Rabi( + exp = T1( physical_qubits=(0,), backend=backend, - schedule=sched, - amplitudes=np.linspace(-0.1, 0.1, 21) + delays=np.linspace(0, 400e-6, 21), ) -We now run the Rabi experiment twice, once with level 1 data and +We now run the T1 experiment twice, once with level 1 data and once with level 2 data. Here, we manually configure two data processors but note that typically you do not need to do this yourself. We begin with single-shot IQ data. @@ -154,7 +142,6 @@ in the code block below. f"Circuit {idx}", points=np.array(exp_data.data(idx)["memory"]).squeeze(), ) - plotter.figure() Now we turn to counts data and see how the diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index 458cfd4502..30168379dd 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -24,7 +24,6 @@ Exploring Modules .. toctree:: :maxdepth: 1 - calibrations data_processor curve_analysis visualization diff --git a/docs/tutorials/visualization.rst b/docs/tutorials/visualization.rst index 0be15a8775..ece05da62b 100644 --- a/docs/tutorials/visualization.rst +++ b/docs/tutorials/visualization.rst @@ -31,7 +31,7 @@ generate figures with the alternative backend. Generating and customizing a figure using a plotter --------------------------------------------------- -First, we display the default figure from a :class:`.Rabi` experiment as a starting point: +First, we display the default figure from a :class:`.T1` experiment as a starting point: .. note:: This tutorial requires the :mod:`qiskit_dynamics`, :external+qiskit_aer:doc:`qiskit-aer `, and @@ -58,46 +58,42 @@ First, we display the default figure from a :class:`.Rabi` experiment as a start import numpy as np - from qiskit import pulse - from qiskit.circuit import Parameter + from qiskit_ibm_runtime.fake_provider import FakeManilaV2 + from qiskit_aer import AerSimulator - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend from qiskit_experiments.data_processing import DataProcessor, nodes - from qiskit_experiments.library import Rabi + from qiskit_experiments.library import T1 - with pulse.build() as sched: - pulse.play( - pulse.Gaussian(160, Parameter("amp"), sigma=40), - pulse.DriveChannel(0) - ) seed = 100 - backend = SingleTransmonTestBackend(seed=seed) + backend = AerSimulator.from_backend(FakeManilaV2()) - rabi = Rabi( - physical_qubits=(0,), - backend=backend, - schedule=sched, - amplitudes=np.linspace(-0.1, 0.1, 21), - ) + delays = np.arange(1.e-6, 300.e-6, 30.e-6) + exp = T1(physical_qubits=(0, ), delays=delays, backend=backend) - rabi_data = rabi.run().block_for_results() - rabi_data.figure(0) + t1_data = exp.run().block_for_results() + t1_data.figure(0) -This is the default figure generated by :class:`.OscillationAnalysis`, the data analysis -class for the Rabi experiment. The fitted cosine is shown as a blue line, with the +This is the default figure generated by :class:`.T1Analysis`, the data analysis +class for the T1 experiment. The fitted exponentail is shown as a blue line, with the individual measurements from the experiment shown as data points with error bars corresponding to their uncertainties. We are also given a small fit report in the caption showing the -``rabi_rate``. +``T1``. The plotter that generated the figure can be accessed through the analysis instance, and customizing the figure can be done by setting the plotter's options. We now modify the color, symbols, and size of our plot, as well as change the axis labels for the amplitude units: +.. jupyter-execute:: + :hide-code: + :hide-output: + + %matplotlib inline + .. jupyter-execute:: # Retrieve the plotter from the analysis instance - plotter = rabi.analysis.plotter + plotter = exp.analysis.plotter # Change the x-axis unit values plotter.set_figure_options( @@ -105,9 +101,9 @@ the color, symbols, and size of our plot, as well as change the axis labels for xval_unit_scale=False # Don't scale the unit with SI prefixes ) - # Change the color and symbol for the cosine + # Change the color and symbol for the exponential plotter.figure_options.series_params.update( - {"cos": {"symbol": "x", "color": "r"}} + {"exp_decay": {"symbol": "x", "color": "r"}} ) # Set figsize directly so we don't overwrite the entire style @@ -126,67 +122,57 @@ To see the residual plot, set ``plot_residuals=True`` in the analysis options: .. jupyter-execute:: # Set to ``True`` analysis option for residual plot - rabi.analysis.set_options(plot_residuals=True) + exp.analysis.set_options(plot_residuals=True) # Run experiment - rabi_data = rabi.run().block_for_results() - rabi_data.figure(0) + exp_data = exp.run().block_for_results() + exp_data.figure(0) This option works for experiments without subplots in their figures. -Here is a more complicated experiment in which we customize the figure of a DRAG +Here is a more complicated experiment in which we customize the figure of a Ramsey XY experiment before it's run, so that we don't need to regenerate the figure like in the previous example. First, we run the experiment without customizing the options to see what the default figure looks like: .. jupyter-execute:: - from qiskit_experiments.library import RoughDrag - from qiskit_experiments.visualization import PlotStyle - from qiskit_experiments.test.mock_iq_helpers import MockIQDragHelper as DragHelper - from qiskit_experiments.test.mock_iq_backend import MockIQBackend - from qiskit.circuit import Parameter - from qiskit import pulse - from qiskit.pulse import DriveChannel, Drag + import numpy as np + from qiskit_experiments.library import RamseyXY + from qiskit_experiments.test.t2hahn_backend import T2HahnBackend - beta = Parameter("beta") - with pulse.build(name="xp") as xp: - pulse.play(pulse.Drag(64, 0.66, 16, beta), pulse.DriveChannel(0)) - drag_experiment_helper = DragHelper(gate_name="Drag(xp)") - backend = MockIQBackend(drag_experiment_helper, rng_seed=seed) + seed = 100 + backend = T2HahnBackend(frequency=1e5, seed=seed) - drag = RoughDrag((0,), xp, backend=backend) + delays = np.linspace(0, 10.e-7, 101) + exp = RamseyXY((0,), backend=backend, delays=delays, osc_freq=2.0e6) - drag_data = drag.run().block_for_results() - drag_data.figure(0) + exp_data = exp.run().block_for_results() + exp_data.figure(0) Now we specify the figure options before running the experiment for a second time: .. jupyter-execute:: - drag = RoughDrag((0,), xp, backend=backend) + exp = RamseyXY((0,), backend=backend, delays=delays, osc_freq=2.0e6) # Set plotter options - plotter = drag.analysis.plotter + plotter = exp.analysis.plotter # Update series parameters plotter.figure_options.series_params.update( { - "nrep=1": { + "X": { "color": (27/255, 158/255, 119/255), "symbol": "^", }, - "nrep=3": { + "Y": { "color": (217/255, 95/255, 2/255), "symbol": "s", }, - "nrep=5": { - "color": (117/255, 112/255, 179/255), - "symbol": "o", - }, } ) @@ -194,15 +180,15 @@ Now we specify the figure options before running the experiment for a second tim plotter.set_figure_options( xval_unit="arb.", xval_unit_scale=False, - figure_title="Rough DRAG Experiment on Qubit 0", + figure_title="Ramsey XY Experiment on Qubit 0", ) # Set style parameters plotter.options.style["symbol_size"] = 10 plotter.options.style["legend_loc"] = "upper center" - drag_data = drag.run().block_for_results() - drag_data.figure(0) + exp_data = exp.run().block_for_results() + exp_data.figure(0) As can be seen in the figure, the different series generated by the experiment were styled differently according to the ``series_params`` attribute of ``figure_options``. @@ -221,10 +207,11 @@ until now in this tutorial: backend = AerSimulator.from_backend(FakePerth()) - t1 = T1(physical_qubits=(0,), - delays=np.linspace(0, 300e-6, 30), - backend=backend - ) + t1 = T1( + physical_qubits=(0,), + delays=np.linspace(0, 300e-6, 30), + backend=backend, + ) plotter = t1.analysis.plotter @@ -267,70 +254,73 @@ analysis class and set the points and centroid series data in the plotter. This is accomplished in the code below, where we also train a discriminator to label the IQ points as one of the three prepared states. :class:`.IQPlotter` supports plotting a discriminator as optional supplementary data, which will show predicted -series over the axis area. +series over the axis area. For a similar working example, see +:class:`.MultiStateDiscriminationAnalysis`. .. jupyter-input:: - with pulse.build(name="xp") as xp: - pulse.play(Drag(duration=160, amp=0.208519, sigma=40, beta=beta), DriveChannel(0)) - - x_plus = xp - drag = RoughDrag(1, x_plus) - expdata = drag.run(backend) - - from qiskit_experiments.framework import BaseAnalysis, Options + from qiskit_experiments.data_processing import BaseDiscriminator + from qiskit_experiments.framework import AnalysisResult, BaseAnalysis, Options from qiskit_experiments.visualization import ( BasePlotter, IQPlotter, MplDrawer, - PlotStyle, ) - class MYIQAnalysis(BaseAnalysis): - @classmethod - def _default_options(cls) -> Options: - options = super()._default_options() - # We create the plotter and create an option for it. - options.plotter = IQPlotter(MplDrawer()) - options.plotter.set_figure_options( - xlabel="In-phase", - ylabel="Quadrature", - figure_title="My IQ Analysis Figure", - series_params={ - "0": {"label": "|0>"}, - "1": {"label": "|1>"}, - "2": {"label": "|2>"}, - }, + + class MyIQAnalysis(BaseAnalysis): + @classmethod + def _default_options(cls) -> Options: + options = super()._default_options() + # We create the plotter and create an option for it. + options.plotter = IQPlotter(MplDrawer()) + options.plotter.set_figure_options( + xlabel="In-phase", + ylabel="Quadrature", + figure_title="My IQ Analysis Figure", + series_params={ + "0": {"label": "|0>"}, + "1": {"label": "|1>"}, + "2": {"label": "|2>"}, + }, + ) + return options + + @property + def plotter(self) -> BasePlotter: + return self.options.plotter + + def _analysis_result(self, datum: dict) -> AnalysisResult: + # Analysis result calculation can be done here + raise NotImplementedError + + def _train_discriminator(self, data: list[dict]) -> BaseDiscriminator: + # Discriminator training can be done here + raise NotImplementedError + + def _run_analysis(self, experiment_data): + data = experiment_data.data() + analysis_results = [] + for datum in data: + # Analysis code + analysis_results.append(self._analysis_result(datum)) + + # Plotting code + series_name = datum["metadata"]["name"] + points = datum["memory"] + centroid = np.mean(points, axis=0) + self.plotter.set_series_data( + series_name, + points=points, + centroid=centroid, ) - return options - - @property - def plotter(self) -> BasePlotter: - return self.options.plotter - - def _run_analysis(self, experiment_data): - data = experiment_data.data() - analysis_results = [] - for datum in data: - # Analysis code - analysis_results.append(self._analysis_result(datum)) - - # Plotting code - series_name = datum["metadata"]["name"] - points = datum["memory"] - centroid = np.mean(points, axis=0) - self.plotter.set_series_data( - series_name, - points=points, - centroid=centroid, - ) - - # Add discriminator to IQPlotter - discriminator = self._train_discriminator(data) - self.plotter.set_supplementary_data(discriminator=discriminator) - - return analysis_results, [self.plotter.figure()] + + # Add discriminator to IQPlotter + discriminator = self._train_discriminator(data) + self.plotter.set_supplementary_data(discriminator=discriminator) + + return analysis_results, [self.plotter.figure()] If we run the above analysis on some appropriate experiment data, as previously described, our class will generate a figure showing IQ points and their centroids. diff --git a/qiskit_experiments/__init__.py b/qiskit_experiments/__init__.py index 32532928b8..9eca7d57c0 100644 --- a/qiskit_experiments/__init__.py +++ b/qiskit_experiments/__init__.py @@ -39,17 +39,13 @@ measurement data. * - :mod:`~qiskit_experiments.curve_analysis` - Utility functions for curve fitting and analysis. - * - :mod:`~qiskit_experiments.calibration_management` - - Classes for managing calibration experiment result data. * - :mod:`~qiskit_experiments.visualization` - Classes for creating figures from experiment results. Certain experiments also have additional utilities contained which can be accessed by importing the following modules. -- :mod:`qiskit_experiments.library.calibration` - :mod:`qiskit_experiments.library.characterization` -- :mod:`qiskit_experiments.library.driven_freq_tuning` - :mod:`qiskit_experiments.library.randomized_benchmarking` - :mod:`qiskit_experiments.library.tomography` """ @@ -60,7 +56,6 @@ from . import framework from . import library from . import curve_analysis -from . import calibration_management from . import data_processing from . import database_service from . import visualization diff --git a/qiskit_experiments/calibration_management/__init__.py b/qiskit_experiments/calibration_management/__init__.py deleted file mode 100644 index d04e8eb064..0000000000 --- a/qiskit_experiments/calibration_management/__init__.py +++ /dev/null @@ -1,155 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -r""" -========================================================================= -Calibration Management (:mod:`qiskit_experiments.calibration_management`) -========================================================================= - -.. currentmodule:: qiskit_experiments.calibration_management - -.. warning:: - The calibrations interface is still in active development. It may have - breaking API changes without deprecation warnings in future releases until - otherwise indicated. - -Calibrating qubit setups is the task of finding the pulse shapes and parameter -values that maximize the fidelity of the resulting quantum operations. This -therefore requires experiments which are analyzed to extract parameter values. -Furthermore, the resulting parameter values and schedules must be managed. The -calibration management module in Qiskit experiments allows users to manage -the resulting schedules and parameter values from obtained when running -calibration experiments from the :mod:`~qiskit_experiments.library`. - -Classes -======= - -.. autosummary:: - :toctree: ../stubs/ - - Calibrations - ParameterValue - FixedFrequencyTransmon - EchoedCrossResonance - BasisGateLibrary - BaseCalibrationExperiment - - -Managing Calibration Data -========================= - -Calibrations are managed by the :class:`.Calibrations` class. This class stores schedules -which are intended to be fully parameterized, including the index of the channels. This -class: - -* supports having different schedules share parameters -* allows default schedules for qubits that can be overridden for specific qubits. - -The following code illustrates how a user can create a parameterized schedule, add -values to the parameters and query a schedule. - -.. code-block:: python - - dur = Parameter("dur") - amp = Parameter("amp") - sigma = Parameter("σ") - - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(dur, amp, sigma), DriveChannel(Parameter("ch0"))) - - cals = Calibrations() - cals.add_schedule(xp) - - # add duration and sigma parameter values for all qubits. - cals.add_parameter_value(160, "dur", schedule="xp") - cals.add_parameter_value(35.5, "σ", schedule="xp") - - # Add an amplitude for qubit 3. - cals.add_parameter_value(0.2+0.05j, "amp", (3, ), "xp") - - # Retrieve an xp pulse with all parameters assigned - cals.get_schedule("xp", (3, )) - - # Retrieve an xp pulse with unassigned amplitude - cals.get_schedule("xp", (3, ), free_params=["amp"]) - -The Calibrations make a couple of assumptions which are discussed below. - -Parametric channel naming convention -************************************ - -Parametrized channel indices must be named according to a predefined pattern to properly -identify the channels and control channels when assigning values to the parametric -channel indices. A channel must have a name that starts with ``ch`` followed by an integer. -For control channels, this integer can be followed by a sequence ``.integer``. -Optionally, the name can end with ``$integer`` to specify the index of a control channel -for the case when a set of qubits share multiple control channels. For example, -valid channel names include ``"ch0"``, ``"ch1"``, ``"ch0.1"``, ``"ch0$"``, ``"ch2$3"``, -and ``"ch1.0.3$2"``. -The ``.`` delimiter is used to specify the different qubits when looking for control -channels. The optional $ delimiter is used to specify which control channel to use -if several control channels work together on the same qubits. For example, if the -control channel configuration is ``{(3,2): [ControlChannel(3), ControlChannel(12)]}`` -then given qubits ``(2, 3)`` the name ``"ch1.0$1"`` will resolve to ``ControlChannel(12)`` while -``"ch1.0$0"`` will resolve to ``ControlChannel(3)``. A channel can only have one parameter. - -Parameter naming restriction -**************************** - -Each parameter must have a unique name within each schedule. For example, it is -acceptable to have a parameter named ``amp`` in the schedule ``xp`` and a different -parameter instance named ``amp`` in the schedule named ``xm``. It is not acceptable -to have two parameters named ``amp`` in the same schedule. The naming restriction -only applies to parameters used in the immediate scope of the schedule. Schedules -called by Call instructions have their own scope for Parameter names. - -The code block below illustrates the creation of a template schedule for a echoed cross- -resonance gate. - -.. code-block:: python - - amp_cr = Parameter("amp") - amp = Parameter("amp") - d0 = DriveChannel(Parameter("ch0")) - c1 = ControlChannel(Parameter("ch0.1")) - sigma = Parameter("σ") - width = Parameter("w") - dur_xp = Parameter("duration") - dur_cr = Parameter("duration") - - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(dur_xp, amp, sigma), d0) - - with pulse.build(name="cr") as cr: - with pulse.align_sequential(): - pulse.play(GaussianSquare(dur_cr, amp_cr, sigma, width), c1) - pulse.call(xp) - pulse.play(GaussianSquare(dur_cr, -amp_cr, sigma, width), c1) - pulse.call(xp) - - cals = Calibrations() - cals.add_schedule(xp) - cals.add_schedule(cr) - -Note that a registered template schedule can be retrieve by doing - -.. code-block:: python - - xp = cals.get_template("xp") - -which would return the default xp schedule block template for all qubits. -""" - -from .calibrations import Calibrations -from .parameter_value import ParameterValue -from .base_calibration_experiment import BaseCalibrationExperiment -from .basis_gate_library import FixedFrequencyTransmon, BasisGateLibrary, EchoedCrossResonance diff --git a/qiskit_experiments/calibration_management/base_calibration_experiment.py b/qiskit_experiments/calibration_management/base_calibration_experiment.py deleted file mode 100644 index b6bf35512b..0000000000 --- a/qiskit_experiments/calibration_management/base_calibration_experiment.py +++ /dev/null @@ -1,346 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Base class for calibration-type experiments.""" - -from abc import ABC, abstractmethod -import functools -import logging -from typing import List, Optional, Sequence, Type, Union -import warnings - -from qiskit import QuantumCircuit -from qiskit.providers.options import Options -from qiskit.pulse import ScheduleBlock -from qiskit.transpiler import StagedPassManager, PassManager, Layout, CouplingMap -from qiskit.transpiler.passes import ( - EnlargeWithAncilla, - FullAncillaAllocation, - ApplyLayout, - SetLayout, -) - -from qiskit_experiments.calibration_management.calibrations import Calibrations -from qiskit_experiments.calibration_management.update_library import BaseUpdater -from qiskit_experiments.framework.base_analysis import BaseAnalysis -from qiskit_experiments.framework.base_experiment import BaseExperiment -from qiskit_experiments.framework.experiment_data import ExperimentData -from qiskit_experiments.exceptions import CalibrationError - -LOG = logging.getLogger(__name__) - - -class BaseCalibrationExperiment(BaseExperiment, ABC): - """A mixin class to create calibration experiments. - - This abstract class extends a characterization experiment by turning it into a - calibration experiment. Such experiments allow schedule management and updating of an - instance of :class:`.Calibrations`. Furthermore, calibration experiments also specify - an auto_update variable which, by default, is set to True. If this variable, - is True then the run method of the experiment will call :meth:`~.ExperimentData.block_for_results` - and update the calibrations instance once the backend has returned the data. - - This mixin class inherits from the :class:`.BaseExperiment` class since calibration - experiments by default call :meth:`~.ExperimentData.block_for_results`. This ensures that the next - calibration experiment cannot proceed before the calibration parameters have been - updated. Developers that wish to create a calibration experiment must subclass this - base class and the characterization experiment. Therefore, developers that use this - mixin class must pay special attention to their class definition. Indeed, the first - class should be this mixin and the second class should be the characterization - experiment since the run method from the mixin must be used. For example, the rough - frequency calibration experiment is defined as - - .. code-block:: python - - RoughFrequencyCal(BaseCalibrationExperiment, QubitSpectroscopy) - - This ensures that the ``run`` method of :class:`.RoughFrequencyCal` will be the - run method of the :class:`.BaseCalibrationExperiment` class. Furthermore, developers - must explicitly call the :meth:`__init__` methods of both parent classes. - - Developers should strive to follow the convention that the first two arguments of - a calibration experiment are the qubit(s) and the :class:`.Calibrations` instance. - - If the experiment uses custom schedules, which is typically the case, then - developers may chose to use the :meth:`get_schedules` method when creating the - circuits for the experiment. If :meth:`get_schedules` is used then the developer - must override at least one of the following methods used by :meth:`get_schedules` - to set the schedules: - - #. :meth:`_get_schedules_from_options` - - #. :meth:`_get_schedules_from_calibrations` - - #. :meth:`_get_schedules_from_defaults` - - These methods are called by :meth:`get_schedules`. - - The :meth:`update_calibrations` method is responsible for updating the values of the parameters - stored in the instance of :class:`.Calibrations`. Here, :class:`BaseCalibrationExperiment` - provides a default update methodology that subclasses can override if a more elaborate behaviour - is needed. At the minimum the developer must set the variable :code:`_updater` which - should have an :code:`update` method and can be chosen from the library - :mod:`qiskit_experiments.calibration_management.update_library`. See also - :class:`qiskit_experiments.calibration_management.update_library.BaseUpdater`. If no updater - is specified the experiment will still run but no update of the calibrations will be performed. - """ - - def __init_subclass__(cls, **kwargs): - """Warn if BaseCalibrationExperiment is not the first parent.""" - for mro_cls in cls.mro(): - if mro_cls is BaseCalibrationExperiment: - break - if issubclass(mro_cls, BaseExperiment) and not issubclass( - mro_cls, BaseCalibrationExperiment - ): - warnings.warn( - "Calibration experiments must inherit from BaseCalibrationExperiment " - f"before a BaseExperiment subclass: {cls}->{mro_cls}." - ) - break - super().__init_subclass__(**kwargs) - - # pylint: disable=super-init-not-called - def __init__( - self, - calibrations: Calibrations, - *args, - schedule_name: Optional[str] = None, - cal_parameter_name: Optional[str] = None, - updater: Optional[Type[BaseUpdater]] = None, - auto_update: bool = True, - **kwargs, - ): - """Setup the calibration experiment object. - - Args: - calibrations: The calibrations instance with which to initialize the experiment. - args: Arguments for the characterization class. - schedule_name: An optional string which specifies the name of the schedule in - the calibrations that will be updated. - cal_parameter_name: An optional string which specifies the name of the parameter in - the calibrations that will be updated. If None is given then no parameter will - be updated. Subclasses may assign default values in their init. - updater: The updater class that updates the Calibrations instance. Different - calibration experiments will use different updaters. - auto_update: If set to True (the default) then the calibrations will automatically be - updated once the experiment has run and :meth:`.block_for_results` will be called. - kwargs: Keyword arguments for the characterization class. - """ - super().__init__(*args, **kwargs) - self._cals = calibrations - self._sched_name = schedule_name - self._param_name = cal_parameter_name - self._updater = updater - self.auto_update = auto_update - - @property - def calibrations(self) -> Calibrations: - """Return the calibrations.""" - return self._cals - - @property - def analysis(self) -> Union[BaseAnalysis, None]: - """Return the analysis instance for the experiment. - - .. note:: - Analysis instance set to calibration experiment is implicitly patched to run - calibration updater to update the parameters in the calibration table. - """ - return self._analysis - - @analysis.setter - def analysis(self, analysis: Union[BaseAnalysis, None]) -> None: - """Set the analysis instance for the experiment""" - if analysis is None: - return - - # Create direct alias to the original run method to avoid infinite recursion. - # .run method is overruled by the wrapped method - # and thus .run method cannot be called within the wrapper function. - analysis_run = getattr(analysis, "run") - - @functools.wraps(analysis_run) - def _wrap_run_analysis(*args, **kwargs): - experiment_data = analysis_run(*args, **kwargs) - if self.auto_update: - experiment_data.add_analysis_callback(self.update_calibrations) - return experiment_data - - # Monkey patch run method. - # This calls update_calibrations immediately after standard analysis. - # This mechanism allows a composite experiment to invoke updater. - # Note that the composite experiment only takes circuits from individual experiment - # and the composite analysis calls analysis.run of each experiment. - # This is only place the updater function can be called from the composite experiment. - analysis.run = _wrap_run_analysis - BaseExperiment.analysis.fset(self, analysis) - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default values for a calibration experiment. - - Experiment Options: - result_index (int): The index of the result from which to update the calibrations. - group (str): The calibration group to which the parameter belongs. This will default - to the value "default". - """ - options = super()._default_experiment_options() - options.update_options(result_index=-1, group="default") - return options - - @classmethod - def _default_transpile_options(cls) -> Options: - """Return empty default transpile options as optimization_level is not used.""" - return Options() - - def set_transpile_options(self, **fields): - r"""Add a warning message. - - .. note:: - If your experiment has overridden `_transpiled_circuits` and needs - transpile options then please also override `set_transpile_options`. - """ - warnings.warn(f"Transpile options are not used in {self.__class__.__name__ }.") - - def update_calibrations(self, experiment_data: ExperimentData): - """Update parameter values in the :class:`.Calibrations` instance. - - The default behaviour is to call the update method of the class variable - :code:`__updater__` with simplistic options. Subclasses can override this - method to update the instance of :class:`.Calibrations` if they require a - more sophisticated behaviour as is the case for the :class:`.Rabi` and - :class:`.FineAmplitude` calibration experiments. - """ - if self._updater is not None: - self._updater.update( - self._cals, - experiment_data, - parameter=self._param_name, - schedule=self._sched_name, - ) - - def _validate_channels(self, schedule: ScheduleBlock, physical_qubits: Sequence[int]): - """Check that the physical qubits are contained in the schedule. - - This is a helper method that experiment developers can call in their implementation - of :meth:`validate_schedules` when checking the schedules. - - Args: - schedule: The schedule for which to check the qubits. - physical_qubits: The qubits that should be included in the schedule. - - Raises: - CalibrationError: If a physical qubit is not contained in the channels schedule. - """ - for qubit in physical_qubits: - if qubit not in set(ch.index for ch in schedule.channels): - raise CalibrationError( - f"Schedule {schedule.name} does not contain a channel " - f"for the physical qubit {qubit}." - ) - - def _validate_parameters(self, schedule: ScheduleBlock, n_expected_parameters: int): - """Check that the schedule has the expected number of parameters. - - This is a helper method that experiment developers can call in their implementation - of :meth:`validate_schedules` when checking the schedules. - - Args: - schedule: The schedule for which to check the qubits. - n_expected_parameters: The number of free parameters the schedule must have. - - Raises: - CalibrationError: If the schedule does not have n_expected_parameters parameters. - """ - if len(schedule.parameters) != n_expected_parameters: - raise CalibrationError( - f"The schedules {schedule.name} for {self.__class__.__name__} must have " - f"{n_expected_parameters} parameters. Found {len(schedule.parameters)}." - ) - - def _metadata(self): - """Add standard calibration metadata.""" - metadata = super()._metadata() - - metadata["cal_group"] = self.experiment_options.group - metadata["cal_param_name"] = self._param_name - metadata["cal_schedule"] = self._sched_name - - # Store measurement level and meas return if they have been - # set for the experiment - for run_opt in ["meas_level", "meas_return"]: - if hasattr(self.run_options, run_opt): - metadata[run_opt] = getattr(self.run_options, run_opt) - return metadata - - def _transpiled_circuits(self) -> List[QuantumCircuit]: - """Override the transpiled circuits method to bring in the inst_map. - - The transpilation should do the strict minimum to make the circuits hardware compatible. - Indeed, calibration experiments are designed with a specific gate sequence in mind. Any - transpiler operation that changes this gate sequence may compromise the validity of the - calibration experiment. Sub-classes may override this method to define their own - transpilation if need be. - - Returns: - A list of transpiled circuits. - """ - transpiled = [] - for circ in self.circuits(): - circ = self._map_to_physical_qubits(circ) - self._attach_calibrations(circ) - - transpiled.append(circ) - - return transpiled - - def _map_to_physical_qubits(self, circuit: QuantumCircuit) -> QuantumCircuit: - """Map program qubits to physical qubits. - - Args: - circuit: The quantum circuit to map to device qubits. - - Returns: - A quantum circuit that has the same number of qubits as the backend and where - the physical qubits of the experiment have been properly mapped. - """ - initial_layout = Layout.from_intlist(list(self.physical_qubits), *circuit.qregs) - - coupling_map = self._backend_data.coupling_map - if coupling_map is not None: - coupling_map = CouplingMap(self._backend_data.coupling_map) - - layout = PassManager( - [ - SetLayout(initial_layout), - FullAncillaAllocation(coupling_map), - EnlargeWithAncilla(), - ApplyLayout(), - ] - ) - - return StagedPassManager(["layout"], layout=layout).run(circuit) - - @abstractmethod - def _attach_calibrations(self, circuit: QuantumCircuit): - """Attach the calibrations to the quantum circuit. - - This method attaches calibrations from the `self._cals` instance to the transpiled - quantum circuits. Given how important this method is it is made abstract to force - potential calibration experiment developers to implement it and think about how - schedules are attached to the circuits. The implementation of this method is delegated - to the sub-classes so that they can map gate instructions to the schedules stored in the - ``Calibrations`` instance. This method is needed for most calibration experiments. However, - some experiments already attach circuits to the logical circuits and do not needed to run - ``_attach_calibrations``. In such experiments a simple ``pass`` statement will suffice. - """ diff --git a/qiskit_experiments/calibration_management/basis_gate_library.py b/qiskit_experiments/calibration_management/basis_gate_library.py deleted file mode 100644 index cbc69d225c..0000000000 --- a/qiskit_experiments/calibration_management/basis_gate_library.py +++ /dev/null @@ -1,494 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -""" -A collections of libraries to setup Calibrations. - -Note that the set of available libraries will be extended in future releases. -""" - -from abc import ABC, abstractmethod -from collections.abc import Mapping -from typing import Any, Dict, List, Optional, Set -from warnings import warn -import numpy as np - -from qiskit.circuit import Parameter -from qiskit import pulse -from qiskit.pulse import ScheduleBlock -from qiskit.utils.deprecation import deprecate_func - -from qiskit_experiments.calibration_management.calibration_key_types import DefaultCalValue -from qiskit_experiments.exceptions import CalibrationError - - -class BasisGateLibrary(ABC, Mapping): - """A base class for libraries of basis gates to make it easier to setup Calibrations.""" - - # Location where default parameter values are stored. These may be updated at construction. - __default_values__ = {} - - # Parameters that do not belong to a schedule, a set of names - __parameters_without_schedule__ = set() - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, support for pulse " - "gate calibrations has been deprecated." - ), - ) - def __init__( - self, - basis_gates: Optional[List[str]] = None, - default_values: Optional[Dict] = None, - **extra_kwargs, - ): - """Setup the library. - - Args: - basis_gates: The basis gates to generate. - default_values: A dictionary to override library default parameter values. - extra_kwargs: Extra key-word arguments of the subclasses that are saved to be able - to reconstruct the library using the :meth:`__init__` method. - - Raises: - CalibrationError: If on of the given basis gates is not supported by the library. - """ - # Update the default values. - self._extra_kwargs = extra_kwargs - self._default_values = self.__default_values__.copy() - if default_values is not None: - self._default_values.update(default_values) - - if basis_gates is None: - basis_gates = list(self.__supported_gates__) - - for gate in basis_gates: - if gate not in self.__supported_gates__: - raise CalibrationError( - f"Gate {gate} is not supported by {self.__class__.__name__}. " - f"Supported gates are: {self.__supported_gates__}." - ) - - self._schedules = self._build_schedules(set(basis_gates)) - - @property - @abstractmethod - def __supported_gates__(self) -> Dict[str, int]: - """Return the supported gates of the library. - - The key is the name of the gate and the value is the number of qubits it applies to. - """ - raise NotImplementedError - - def __getitem__(self, name: str) -> ScheduleBlock: - """Return the schedule.""" - if name not in self._schedules: - raise CalibrationError(f"Gate {name} is not contained in {self.__class__.__name__}.") - - return self._schedules[name] - - def __contains__(self, name: str) -> bool: - """Check if the basis gate is in the library.""" - return name in self._schedules - - def __hash__(self) -> int: - """Return the hash of the library by computing the hash of the schedule strings.""" - data_to_hash = [] - for name, schedule in sorted(self._schedules.items()): - data_to_hash.append((name, str(schedule), self.__supported_gates__[name])) - - return hash(tuple(data_to_hash)) - - def __len__(self): - """The length of the library defined as the number of basis gates.""" - return len(self._schedules) - - def __iter__(self): - """Return an iterator over the basis gate library.""" - return iter(self._schedules) - - def num_qubits(self, name: str) -> int: - """Return the number of qubits that the schedule with the given name acts on.""" - return self.__supported_gates__[name] - - @property - def basis_gates(self) -> List[str]: - """Return the basis gates supported by the library.""" - return list(self._schedules) - - @abstractmethod - def default_values(self) -> List[DefaultCalValue]: - """Return the default values for the parameters. - - Returns - A list of tuples is returned. These tuples are structured so that instances - of :class:`.Calibrations` can call :meth:`.Calibrations.add_parameter_value` - on the tuples. - """ - - @abstractmethod - def _build_schedules(self, basis_gates: Set[str]) -> Dict[str, ScheduleBlock]: - """Build the schedules stored in the library. - - This method is called as the last step in the :meth:`__init__`. Subclasses must implement - :meth:`_build_schedules` to build the schedules of the library based on the inputs given - to the :meth:`__init__` method. - - Args: - basis_gates: The set of basis gates to build. These will be the supported gates or - a subset thereof. - - Returns: - A dictionary where the keys are the names of the schedules/basis gates and the values - are the corresponding schedules. - """ - - def config(self) -> Dict[str, Any]: - """Return the settings used to initialize the library.""" - - kwargs = {"basis_gates": self.basis_gates, "default_values": self._default_values} - kwargs.update(self._extra_kwargs) - - return { - "class": self.__class__.__name__, - "kwargs": kwargs, - "hash": hash(self), - } - - @classmethod - def from_config(cls, config: Dict) -> "BasisGateLibrary": - """Deserialize the library given the input dictionary""" - library = cls(**config["kwargs"]) - - if hash(library) != config["hash"]: - warn( - "Deserialized basis gate library's hash does not match the hash of the serialized " - "library. Typically, the hash changes when the internal structure of the template " - "schedules has been changed." - ) - - return library - - def __json_encode__(self): - """Convert to format that can be JSON serialized.""" - return self.config() - - @classmethod - def __json_decode__(cls, value: Dict[str, Any]) -> "BasisGateLibrary": - """Load from JSON compatible format.""" - return cls.from_config(value) - - -class FixedFrequencyTransmon(BasisGateLibrary): - r"""A library of gates for fixed-frequency superconducting qubit architectures. - - Note that for now this library supports single-qubit gates and will be extended - in the future. - - Provided gates: - - x: :math:`\pi` pulse around the x-axis. - - sx: :math:`\pi/2` pulse around the x-axis. - - y: :math:`\pi` pulse around the y-axis. - - sy: :math:`\pi/2` pulse around the y-axis. - - Pulse parameters: - - duration: Duration of the pulses Default value: 160 samples. - - σ: Standard deviation of the pulses Default value: ``duration / 4``. - - β: DRAG parameter of the pulses Default value: 0. - - amp: Magnitude of the complex amplitude of the pulses. If the parameters are - linked then ``x`` and ``y`` - share the same parameter and ``sx`` and ``sy`` share the same parameter. - Default value: 50% of the maximum output for ``x`` and ``y`` and 25% of the - maximum output for ``sx`` and ``sy``. Note that the user provided default amplitude - in the ``__init__`` method sets the default amplitude of the ``x`` and ``y`` pulses. - The amplitude of the ``sx`` and ``sy`` pulses is half the provided value. - - angle: The phase of the complex amplitude of the pulses. - - Parameters without schedule: - - meas_freq: frequency of the measurement drives. - - drive_freq: frequency of the qubit drives. - - Note that the β and amp parameters may be linked between the x and y as well as between - the sx and sy pulses. All pulses share the same duration and σ parameters. - """ - - __default_values__ = {"duration": 160, "amp": 0.5, "β": 0.0, "angle": 0.0} - - __parameters_without_schedule__ = {"meas_freq", "drive_freq"} - - def __init__( - self, - basis_gates: Optional[List[str]] = None, - default_values: Optional[Dict] = None, - link_parameters: bool = True, - ): - """Setup the schedules. - - Args: - basis_gates: The basis gates to generate. - default_values: Default values for the parameters this dictionary can contain - the following keys: "duration", "amp", "β", and "σ". If "σ" is not provided - this library will take one fourth of the pulse duration as default value. - link_parameters: If set to ``True``, then the amplitude and DRAG parameters of the - :math:`X` and :math:`Y` gates will be linked as well as those of - the :math:`SX` and :math:`SY` gates. - """ - self._link_parameters = link_parameters - - extra_kwargs = {"link_parameters": link_parameters} - - super().__init__(basis_gates, default_values, **extra_kwargs) - - @property - def __supported_gates__(self) -> Dict[str, int]: - """The gates that this library supports.""" - return {"x": 1, "y": 1, "sx": 1, "sy": 1} - - def _build_schedules(self, basis_gates: Set[str]) -> Dict[str, ScheduleBlock]: - """Build the schedule of the class.""" - dur = Parameter("duration") - sigma = Parameter("σ") - - x_amp, x_beta, x_angle = Parameter("amp"), Parameter("β"), Parameter("angle") - - if self._link_parameters: - y_amp, y_beta, y_angle = x_amp, x_beta, x_angle + np.pi / 2 - else: - y_amp, y_beta, y_angle = Parameter("amp"), Parameter("β"), Parameter("angle") - - sx_amp, sx_beta, sx_angle = Parameter("amp"), Parameter("β"), Parameter("angle") - - if self._link_parameters: - sy_amp, sy_beta, sy_angle = sx_amp, sx_beta, sx_angle + np.pi / 2 - else: - sy_amp, sy_beta, sy_angle = Parameter("amp"), Parameter("β"), Parameter("angle") - - # Create the schedules for the gates - sched_x = self._single_qubit_schedule("x", dur, x_amp, sigma, x_beta, x_angle) - sched_y = self._single_qubit_schedule("y", dur, y_amp, sigma, y_beta, y_angle) - sched_sx = self._single_qubit_schedule("sx", dur, sx_amp, sigma, sx_beta, sx_angle) - sched_sy = self._single_qubit_schedule("sy", dur, sy_amp, sigma, sy_beta, sy_angle) - - schedules = {} - for sched in [sched_x, sched_y, sched_sx, sched_sy]: - if sched.name in basis_gates: - schedules[sched.name] = sched - - return schedules - - @staticmethod - def _single_qubit_schedule( - name: str, - dur: Parameter, - amp: Parameter, - sigma: Parameter, - beta: Parameter, - angle: Parameter, - ) -> ScheduleBlock: - """Build a single qubit pulse.""" - - chan = pulse.DriveChannel(Parameter("ch0")) - - with pulse.build(name=name) as sched: - pulse.play(pulse.Drag(duration=dur, amp=amp, sigma=sigma, beta=beta, angle=angle), chan) - - return sched - - def default_values(self) -> List[DefaultCalValue]: - """Return the default values for the parameters. - - Returns - A list of tuples is returned. These tuples are structured so that instances - of :class:`.Calibrations` can call :meth:`.Calibrations.add_parameter_value` - on the tuples. - """ - defaults = [] - for name, schedule in self.items(): - for param in schedule.parameters: - if "ch" not in param.name: - if "y" in name and self._link_parameters: - continue - - if param.name == "σ" and "σ" not in self._default_values: - value = self._default_values["duration"] / 4 - else: - value = self._default_values[param.name] - - if name in {"sx", "sy"} and param.name == "amp": - value /= 2.0 - - if "y" in name and param.name == "angle": - value += np.pi / 2 - - defaults.append(DefaultCalValue(value, param.name, tuple(), name)) - - return defaults - - -class EchoedCrossResonance(BasisGateLibrary): - r"""A library for echoed cross-resonance gates. - - The ``cr45p`` and ``cr45m`` include a pulse on the control qubit and optionally a pulse - on the target qubit. - - Provided gates: - - cr45p: GaussianSquare cross-resonance gate for a :math:`+\pi/4` rotation. - - cr45m: GaussianSquare cross-resonance gate for a :math:`-\pi/4` rotation. - - ecr: Echoed cross-resonance gate defined as ``cr45p - x - cr45m``. - - rzx: RZXGate built from the ecr as ``cr45p - x - cr45m - x``. - - Required gates: - - x: the x gate is defined outside of this library, see :class:`.FixedFrequencyTransmon`. - - Pulse parameters: - - tgt_amp: The amplitude of the pulse applied to the target qubit. Default value: 0. - - σ: The standard deviation of the flanks. Default value: 64 samples. - - amp: The amplitude of the pulses applied to the control qubit. Default value: 50%. - - duration: The duration of the cr45p and cr45m pulses. Default value: 1168 samples. - - risefall: The number of σ's in the flanks of the pulses. Default value: 2. - """ - - __default_values__ = { - "tgt_amp": 0.0, - "tgt_angle": 0.0, - "amp": 0.5, - "angle": 0.0, - "σ": 64, - "risefall": 2, - "duration": 1168, - } - - def __init__( - self, - basis_gates: Optional[List[str]] = None, - default_values: Optional[Dict] = None, - target_pulses: bool = True, - ): - """Setup the library. - - Args: - basis_gates: The basis gates to generate. - default_values: A dictionary to override library default parameter values. - target_pulses: If True (the default) then drives will be added to the target qubit - during the CR tones on the control qubit. - """ - self._target_pulses = target_pulses - super().__init__(basis_gates, default_values) - - @property - def __supported_gates__(self) -> Dict[str, int]: - """The supported gates of the library are two-qubit pulses for the ecr gate.""" - return {"cr45p": 2, "cr45m": 2, "ecr": 2, "rzx": 2} - - def default_values(self) -> List[DefaultCalValue]: - """The default values of the CR library.""" - defaults = [] - for name, schedule in self.items(): - for param in schedule.parameters: - if "ch" not in param.name: - value = self._default_values[param.name] - defaults.append(DefaultCalValue(value, param.name, tuple(), name)) - - return defaults - - def _build_schedules(self, basis_gates: Set[str]) -> Dict[str, ScheduleBlock]: - """Build the schedules of the CR library.""" - - schedules = {} - - tgt_amp = Parameter("tgt_amp") - tgt_angle = Parameter("tgt_angle") - sigma = Parameter("σ") - cr_amp = Parameter("amp") - cr_angle = Parameter("angle") - cr_dur = Parameter("duration") - cr_rf = Parameter("risefall") - t_chan_idx = Parameter("ch1") - u_chan_idx = Parameter("ch0.1") - t_chan = pulse.DriveChannel(t_chan_idx) - u_chan = pulse.ControlChannel(u_chan_idx) - - if "cr45p" in basis_gates: - with pulse.build(name="cr45p") as cr45p: - pulse.play( - pulse.GaussianSquare( - cr_dur, cr_amp, angle=cr_angle, risefall_sigma_ratio=cr_rf, sigma=sigma - ), - u_chan, - ) - - if self._target_pulses: - pulse.play( - pulse.GaussianSquare( - cr_dur, - tgt_amp, - angle=tgt_angle, - risefall_sigma_ratio=cr_rf, - sigma=sigma, - ), - t_chan, - ) - - schedules["cr45p"] = cr45p - - if "cr45m" in basis_gates: - with pulse.build(name="cr45m") as cr45m: - pulse.play( - pulse.GaussianSquare( - cr_dur, - cr_amp, - angle=cr_angle + np.pi, - risefall_sigma_ratio=cr_rf, - sigma=sigma, - ), - u_chan, - ) - - if self._target_pulses: - pulse.play( - pulse.GaussianSquare( - cr_dur, - tgt_amp, - angle=tgt_angle + np.pi, - risefall_sigma_ratio=cr_rf, - sigma=sigma, - ), - t_chan, - ) - - schedules["cr45m"] = cr45m - - # Echoed Cross-Resonance gate - if "ecr" in basis_gates: - with pulse.build(name="ecr") as ecr: - with pulse.align_sequential(): - pulse.reference("cr45p", "q0", "q1") - pulse.reference("x", "q0") - pulse.reference("cr45m", "q0", "q1") - - schedules["ecr"] = ecr - - # RZXGate built from Echoed Cross-Resonance gate - if "rzx" in basis_gates: - with pulse.build(name="rzx") as rzx: - with pulse.align_sequential(): - pulse.reference("cr45p", "q0", "q1") - pulse.reference("x", "q0") - pulse.reference("cr45m", "q0", "q1") - pulse.reference("x", "q0") - - schedules["rzx"] = rzx - - return schedules diff --git a/qiskit_experiments/calibration_management/calibration_key_types.py b/qiskit_experiments/calibration_management/calibration_key_types.py deleted file mode 100644 index 4359e03769..0000000000 --- a/qiskit_experiments/calibration_management/calibration_key_types.py +++ /dev/null @@ -1,48 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Types used by the calibration module.""" - -from typing import NamedTuple, Tuple, Union -from collections import namedtuple - -from qiskit.circuit import ParameterExpression - - -ParameterKey = namedtuple("ParameterKey", ["parameter", "qubits", "schedule"]) -ParameterValueType = Union[ParameterExpression, float, int, complex] - - -class DefaultCalValue(NamedTuple): - """Defines the structure of a default value.""" - - value: Union[float, int, complex] - parameter: str - qubits: Tuple - schedule_name: str - - -class ScheduleKey(NamedTuple): - """Defines the structure of a key to find a schedule.""" - - schedule: str # Name of the schedule - qubits: Tuple # Qubits the schedule acts on - - def __repr__(self): - return f"{self.schedule}::{self.qubits}" - - @classmethod - def from_repr(cls, rep_str: str) -> "ScheduleKey": - """Construct a key form its representation as a string.""" - name, qubits = rep_str.split("::") - qubits = tuple(int(qubit) for qubit in qubits.strip("( )").split(",") if qubit != "") - return ScheduleKey(name, qubits) diff --git a/qiskit_experiments/calibration_management/calibration_utils.py b/qiskit_experiments/calibration_management/calibration_utils.py deleted file mode 100644 index c489268fd5..0000000000 --- a/qiskit_experiments/calibration_management/calibration_utils.py +++ /dev/null @@ -1,170 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019-2021. -# -# 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. - -"""Calibration helper functions""" - -from typing import Optional, Set, Tuple -from functools import lru_cache -import re -import rustworkx as rx - -from qiskit.circuit import ParameterExpression, Parameter -from qiskit.pulse import ScheduleBlock - -from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.calibration_management.calibration_key_types import ScheduleKey - - -# The channel indices need to be parameterized following this regex. -CHANNEL_PATTERN = r"^ch\d+(\.\d+)*(\$\d+){0,1}$" -CHANNEL_PATTERN_REGEX = re.compile(CHANNEL_PATTERN) - - -def update_schedule_dependency(schedule: ScheduleBlock, dag: rx.PyDiGraph, key: ScheduleKey): - """Update a DAG of schedule dependencies. - - Args: - schedule: A ScheduleBlock that potentially has references to other schedules - that are already present in the dag. - dag: A directed acyclic graph that encodes schedule dependencies using references. - key: The schedule key which also contains the qubits. - """ - # First, check if the node is already in the DAG. - try: - # If it already is in the DAG we remove the existing edges and add the new ones later. - parent_idx = dag.nodes().index(key) - for successor in dag.successors(parent_idx): - dag.remove_edge(parent_idx, dag.nodes().index(successor)) - - except ValueError: - # The schedule is not in the DAG: we add a new node. - parent_idx = dag.add_node(key) - - for reference in schedule.references: - ref_key = ScheduleKey(reference[0], key.qubits) - dag.add_edge(parent_idx, _get_node_index(ref_key, dag), None) - - -def used_in_references(keys: Set[ScheduleKey], dag: rx.PyDiGraph) -> Set[str]: - """Find all the schedules in the DAG that reference the given schedules. - - Args: - keys: A list of schedules keys to which references may exist. - dag: The dag that represents the dependencies between schedule references. - - Returns: - A set of schedules that reference the given schedules. - """ - callers = set() - - for key in keys: - callers.update(dag.nodes()[idx] for idx in rx.ancestors(dag, _get_node_index(key, dag))) - - return set(key.schedule for key in callers) - - -def _get_node_index(key: ScheduleKey, dag: rx.PyDiGraph) -> int: - """A helper method to get the node index in the DAG. - - If the given ScheduleKey is not found then we try and get the default schedule with the - same name. I.e. a key for which the qubits are an empty tuple. - - Args: - key: The ScheduleKey for which to find a node in the DAG. - dag: The DAG of schedule dependencies. - - Returns: - The index of the node in the dag corresponding to schedule key or its default. - """ - try: - return dag.nodes().index(key) - except ValueError: - default_key = ScheduleKey(key.schedule, tuple()) - return dag.nodes().index(default_key) - - -def validate_channels(schedule: ScheduleBlock) -> Set[Parameter]: - """Validate and get the parameters in the channels of the schedule. - - Channels implicitly defined in references are ignored. - - Args: - schedule: The schedule for which to get the parameters in the channels. - - Returns: - The set of parameters explicitly defined in the schedule. - - Raises: - CalibrationError: If a channel is parameterized by more than one parameter. - CalibrationError: If the parameterized channel index is not formatted properly. - """ - param_indices = set() - - # Schedules with references do not explicitly have channels. This needs special handling. - if schedule.is_referenced(): - for block in schedule.blocks: - if isinstance(block, ScheduleBlock): - param_indices.update(validate_channels(block)) - - return param_indices - - for ch in schedule.channels: - if isinstance(ch.index, ParameterExpression): - if len(ch.index.parameters) != 1: - raise CalibrationError(f"Channel {ch} can only have one parameter.") - - param_indices.add(ch.index) - if CHANNEL_PATTERN_REGEX.match(ch.index.name) is None: - raise CalibrationError( - f"Parameterized channel must correspond to {CHANNEL_PATTERN}" - ) - - return param_indices - - -@lru_cache() -def reference_info( - reference: Tuple[str, ...], - qubits: Optional[Tuple[int, ...]] = None, -) -> Tuple[str, Tuple[int, ...]]: - """Extract reference information from the reference tuple. - - Args: - reference: The reference of a Reference instruction in a ScheduleBlock. - qubits: Optional argument to reorder the references. - - Returns: - A string corresponding to the name of the referenced schedule and the qubits that - this schedule applies to. - - Raises: - CalibrationError: If ``reference`` is not a tuple. - CalibrationError: If ``reference`` is not a tuple of reference name and the qubits that - that the schedule applies to. - """ - if not isinstance(reference, tuple): - raise CalibrationError(f"A schedule reference must be a tuple. Found {reference}.") - - ref_schedule_name, ref_qubits = reference[0], reference[1:] - - if not isinstance(ref_schedule_name, str) and not isinstance(ref_qubits, tuple): - raise CalibrationError( - f"A schedule reference is a name and qubits tuple. Found {reference}" - ) - - ref_qubits = tuple(int(qubit[1:]) for qubit in ref_qubits) - - # get the qubit indices for which we are getting the schedules - if qubits is not None and len(qubits) >= len(ref_qubits): - ref_qubits = tuple(qubits[idx] for idx in ref_qubits) - - return ref_schedule_name, ref_qubits diff --git a/qiskit_experiments/calibration_management/calibrations.py b/qiskit_experiments/calibration_management/calibrations.py deleted file mode 100644 index af5572570f..0000000000 --- a/qiskit_experiments/calibration_management/calibrations.py +++ /dev/null @@ -1,1746 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Class to store and manage the results of calibration experiments.""" - -import warnings -import os -from collections import defaultdict, Counter -from datetime import datetime, timezone -from typing import Any, Dict, Set, Tuple, Union, List, Optional -import csv -import dataclasses -import json -import rustworkx as rx - -from qiskit.pulse import ( - ScheduleBlock, - DriveChannel, - ControlChannel, - MeasureChannel, - AcquireChannel, - RegisterSlot, - MemorySlot, - InstructionScheduleMap, -) -from qiskit.pulse.channels import PulseChannel -from qiskit.circuit import Parameter, ParameterExpression -from qiskit.providers.backend import Backend -from qiskit.utils.deprecation import deprecate_func, deprecate_arg - -from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.calibration_management.basis_gate_library import BasisGateLibrary -from qiskit_experiments.calibration_management.parameter_value import ParameterValue -from qiskit_experiments.calibration_management.control_channel_map import ControlChannelMap -from qiskit_experiments.calibration_management.calibration_utils import ( - used_in_references, - validate_channels, - reference_info, - update_schedule_dependency, -) -from qiskit_experiments.calibration_management.calibration_key_types import ( - ParameterKey, - ParameterValueType, - ScheduleKey, -) -from qiskit_experiments.framework import BackendData, ExperimentEncoder, ExperimentDecoder - - -class Calibrations: - """ - A class to manage schedules with calibrated parameter values. Schedules are - intended to be fully parameterized, including the index of the channels. See - the module-level documentation for extra details. Note that only instances of - ScheduleBlock are supported. - """ - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, support for pulse " - "gate calibrations has been deprecated." - ), - ) - def __init__( - self, - coupling_map: Optional[List[List[int]]] = None, - control_channel_map: Optional[Dict[Tuple[int, ...], List[ControlChannel]]] = None, - libraries: Optional[List[BasisGateLibrary]] = None, - add_parameter_defaults: bool = True, - backend_name: Optional[str] = None, - backend_version: Optional[str] = None, - ): - """Initialize the calibrations. - - Calibrations can be initialized from a list of basis gate libraries, i.e. a subclass of - :class:`.BasisGateLibrary`. As example consider the following code: - - .. code-block:: python - - cals = Calibrations( - libraries=[ - FixedFrequencyTransmon( - basis_gates=["x", "sx"], - default_values={duration: 320} - ) - ] - ) - - Args: - coupling_map: The device's coupling map. Each sub-list describes connected qubits - For example, the coupling map of a fully pairwise-connected backend with three - qubits is :code:`[[0, 1], [1, 0], [1, 2], [2, 1], [2, 0], [0, 2]]`. - control_channel_map: A configuration dictionary of any control channels. The - keys are tuples of qubits and the values are a list of ControlChannels - that correspond to the qubits in the keys. If a control_channel_map is given - then the qubits must be in the coupling_map. - libraries: A list of library instances from which to get template schedules to register - as well as default parameter values. - add_parameter_defaults: A boolean to indicate weather the default parameter values of - the given libraries should be used to populate the calibrations. By default this - value is True but can be set to false when deserializing a calibrations object. - backend_name: The name of the backend that these calibrations are attached to. - backend_version: The version of the backend that these calibrations are attached to. - """ - self._backend_name = backend_name - self._backend_version = backend_version - - # Mapping between qubits and their control channels. - self._control_channel_map = control_channel_map if control_channel_map else {} - - # Store the reverse mapping between control channels and qubits for ease of look-up. - self._controls_config_r = {} - for qubits, channels in self._control_channel_map.items(): - for channel in channels: - self._controls_config_r[channel] = qubits - - # Dict of the form: (schedule.name, parameter.name, qubits): Parameter - self._parameter_map = {} - - # Reverse mapping of _parameter_map - self._parameter_map_r = defaultdict(set) - - # Default dict of the form: (schedule.name, parameter.name, qubits): [ParameterValue, ...] - self._params = defaultdict(list) - - # Dict of the form: ScheduleKey: ScheduleBlock - self._schedules = {} - - # Dict of the form: ScheduleKey: int (number of qubits in corresponding circuit instruction) - self._schedules_qubits = {} - - # A directed acyclic graph to manage schedule reference dependencies. - # Each node is a schedule key and edges are schedule references. - # The acyclic nature prevents users from registering schedules with cyclic dependencies. - self._schedule_dependency = rx.PyDiGraph(check_cycle=True) - - # A variable to store all parameter hashes encountered and present them as ordered - # indices to the user. - self._hash_to_counter_map = {} - self._parameter_counter = 0 - - self._libraries = libraries - if libraries is not None: - if not isinstance(libraries, list): - libraries = [libraries] - - for lib in libraries: - # Add the basis gates - for gate in lib.basis_gates: - self.add_schedule(lib[gate], num_qubits=lib.num_qubits(gate)) - - # Add the default values - if add_parameter_defaults: - for param_conf in lib.default_values(): - self.add_parameter_value(*param_conf, update_inst_map=False) - - # Add the parameters that do not belong to a schedule. - for param_name in lib.__parameters_without_schedule__: - self._register_parameter(Parameter(param_name), tuple()) - - # This internal parameter is False so that if a schedule is added after the - # init it will be set to True and serialization will raise an error. - self._has_manually_added_schedule = False - - # Instruction schedule map variables and support variables. - self._inst_map = InstructionScheduleMap() - - # Backends with a single qubit may not have a coupling map. - self._coupling_map = coupling_map if coupling_map is not None else [] - - # A dict extension of the coupling map where the key is the number of qubits and - # the values are a list of qubits coupled. - self._operated_qubits = self._get_operated_qubits() - self._check_consistency() - - # Push the schedules to the instruction schedule map. - self.update_inst_map() - - @property - @deprecate_func( - is_property=True, - since="0.6", - package_name="qiskit-experiments", - additional_msg="The drive_freq is moved to FixedFrequencyTransmon basis gate library.", - ) - def drive_freq(self): - """Parameter object for qubit drive frequency.""" - return self._parameter_map.get(("drive_freq", (), None), None) - - @property - @deprecate_func( - is_property=True, - since="0.6", - package_name="qiskit-experiments", - additional_msg="The meas_freq is moved to FixedFrequencyTransmon basis gate library.", - ) - def meas_freq(self): - """Parameter object for qubit measure frequency.""" - return self._parameter_map.get(("meas_freq", (), None), None) - - def _check_consistency(self): - """Check that the attributes defined in self are consistent. - - Raises: - CalibrationError: If there is a control channel map but no coupling map. - CalibrationError: If a qubit in the control channel map is not in the - coupling map. - """ - if not self._coupling_map and self._control_channel_map: - raise CalibrationError("No coupling map but a control channel map was found.") - - if self._coupling_map and self._control_channel_map: - cmap_qubits = set(qubit for pair in self._coupling_map for qubit in pair) - for qubits in self._control_channel_map: - if not set(qubits).issubset(cmap_qubits): - raise CalibrationError( - f"Qubits {qubits} of control_channel_map are not in the coupling map." - ) - - @property - def backend_name(self) -> str: - """Return the name of the backend.""" - return self._backend_name - - @property - def backend_version(self) -> str: - """Return the version of the backend.""" - return self._backend_version - - @property - def schedule_dependency(self) -> rx.PyDiGraph: - """Return the schedule dependencies in the calibrations.""" - return self._schedule_dependency.copy() - - @classmethod - def from_backend( - cls, - backend: Backend, - libraries: Optional[List[BasisGateLibrary]] = None, - add_parameter_defaults: bool = True, - ) -> "Calibrations": - """Create an instance of Calibrations from a backend. - - Args: - backend: A backend instance from which to extract the qubit and readout frequencies - (which will be added as first guesses for the corresponding parameters) as well - as the coupling map. - libraries: A list of libraries from which to get template schedules to register as - well as default parameter values. - add_parameter_defaults: A boolean to indicate whether the default parameter values of - the given library should be used to populate the calibrations. By default, this - value is ``True``. - - Returns: - An instance of Calibrations instantiated from a backend. - """ - backend_data = BackendData(backend) - - control_channel_map = {} - if backend_data.coupling_map is not None: - for qargs in backend_data.coupling_map: - control_channel_map[tuple(qargs)] = backend_data.control_channel(qargs) - - cals = Calibrations( - backend_data.coupling_map, - control_channel_map, - libraries, - add_parameter_defaults, - backend_data.name, - backend_data.version, - ) - - if add_parameter_defaults: - if ("drive_freq", (), None) in cals._parameter_map: - for qubit, freq in enumerate(backend_data.drive_freqs): - cals.add_parameter_value(freq, "drive_freq", qubit, update_inst_map=False) - - if ("meas_freq", (), None) in cals._parameter_map: - for meas, freq in enumerate(backend_data.meas_freqs): - cals.add_parameter_value(freq, "meas_freq", meas, update_inst_map=False) - - # Update the instruction schedule map after adding all parameter values. - cals.update_inst_map() - - return cals - - @property - def libraries(self) -> Optional[List[BasisGateLibrary]]: - """Return the libraries used to initialize the calibrations.""" - return self._libraries - - def _get_operated_qubits(self) -> Dict[int, List[int]]: - """Get a dict describing qubit couplings. - - This is an extension of the coupling map and used as a convenience to help populate - the instruction schedule map. - - Returns: - A dict where the key is the number of qubits coupled and the value is a list of - lists where the sublist shows which qubits are coupled. For example, a three qubit - system with a three qubit gate and three two-qubit gates would be represented as - - .. parsed-literal:: - - { - 1: [[0], [1], [2]], - 2: [[0, 1], [1, 2], [2, 1]], - 3: [[0, 1, 2]] - } - """ - operated_qubits = defaultdict(list) - - # Single qubits - if self._coupling_map: - for qubit in set(qubit for coupled in self._coupling_map for qubit in coupled): - operated_qubits[1].append([qubit]) - else: - # Edge case for single-qubit device. - operated_qubits[1].append([0]) - - # Multi-qubit couplings - for coupling in self._coupling_map: - operated_qubits[len(coupling)].append(coupling) - - return operated_qubits - - @property - def default_inst_map(self) -> InstructionScheduleMap: - """Return the default and up to date instruction schedule map.""" - return self._inst_map - - def get_inst_map( - self, - group: str = "default", - cutoff_date: datetime = None, - ) -> InstructionScheduleMap: - """Get an Instruction schedule map with the calibrated pulses. - - If the group is 'default' and cutoff date is None then the automatically updated - instruction schedule map is returned. However, if these values are different then - a new instruction schedule map is populated based on the values. - - Args: - group: The calibration group from which to draw the parameters. - If not specified this defaults to the 'default' group. - cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters - generated after the cutoff date will be ignored. If the cutoff_date is None then - all parameters are considered. This allows users to discard more recent values that - may be erroneous. - - Returns: - An instruction schedule map with parameters updated up to the desired cutoff date - and from the desired calibration group. - """ - if group == "default" and cutoff_date is None: - return self._inst_map - - inst_map = InstructionScheduleMap() - - self.update_inst_map(group=group, cutoff_date=cutoff_date, inst_map=inst_map) - - return inst_map - - def update_inst_map( - self, - schedules: Optional[Set[str]] = None, - qubits: Optional[Tuple[int, ...]] = None, - group: Optional[str] = "default", - cutoff_date: datetime = None, - inst_map: Optional[InstructionScheduleMap] = None, - ): - """Push all schedules from the Calibrations to the inst map. - - This will create instructions with the same name as the schedules. - - Args: - schedules: The name of the schedules to update. If None is given then - all schedules will be pushed to instructions. - qubits: The qubits for which to update the instruction schedule map. - If qubits is None then all possible schedules defined by the coupling - map will be updated. Note that this argument specifies a particular set of - qubits to update instructions for. For example, if qubits is :code:`(2, 3)` then - only two-qubit instructions that apply to qubits 2 and 3 will be updated. Here, - single-qubit instructions will not be updated. - group: The calibration group from which to draw the parameters. If not specified - this defaults to the 'default' group. - cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters - generated after the cutoff date will be ignored. If the cutoff_date is None then - all parameters are considered. This allows users to discard more recent values that - may be erroneous. - inst_map: The instruction schedule map to update. If None is given then the default - instruction schedule map (i.e. self._inst_map) will be updated. - """ - inst_map = inst_map or self._inst_map - - for key in self._schedules: - sched_name = key.schedule - - if schedules is not None and sched_name not in schedules: - continue - - if qubits: - self._robust_inst_map_add(inst_map, sched_name, qubits, group, cutoff_date) - else: - for qubits_ in self._operated_qubits[self._schedules_qubits[key]]: - self._robust_inst_map_add(inst_map, sched_name, qubits_, group, cutoff_date) - - def _robust_inst_map_add( - self, - inst_map: InstructionScheduleMap, - sched_name: str, - qubits: Union[int, Tuple[int, ...]], - group: str, - cutoff: datetime, - ): - """A helper method for update_inst_map. - - get_schedule may raise an error if not all parameters have values or - default values. In this case we ignore and continue updating inst_map. - Note that ``qubits`` may only be a sub-set of the qubits of the schedule that - we want to update. This may arise in cases such as an ECR gate schedule that calls - an X-gate schedule. When updating the X-gate schedule we need to also update the - corresponding ECR schedules which operate on a larger number of qubits. - - Args: - sched_name: The name of the schedule. - qubits: The qubit to which the schedule applies. Note, these may be only a - subset of the qubits in the schedule. For example, if the name of the - schedule is `"cr"` we may have `qubits` be `(3, )` and this function - will update the CR schedules on all schedules which involve qubit 3. - group: The calibration group. - cutoff: The cutoff date. - """ - for update_qubits in self._get_full_qubits_of_schedule(sched_name, qubits): - try: - schedule = self.get_schedule( - sched_name, update_qubits, group=group, cutoff_date=cutoff - ) - inst_map.add(instruction=sched_name, qubits=update_qubits, schedule=schedule) - except CalibrationError: - # get_schedule may raise an error if not all parameters have values or - # default values. In this case we ignore and continue updating inst_map. - pass - - def _get_full_qubits_of_schedule( - self, schedule_name: str, partial_qubits: Tuple[int, ...] - ) -> List[Tuple[int, ...]]: - """Find all qubits for which there is a schedule ``schedule_name`` on ``partial_qubits``. - - This method uses the map between the schedules and the number of qubits that they - operate on as well as the extension of the coupling map ``_operated_qubits`` to find - which qubits are involved in the schedule named ``schedule_name`` involving the - ``partial_qubits``. - - Args: - schedule_name: The name of the schedule as registered in ``self``. - partial_qubits: A sub-set of qubits on which the schedule applies. - - Returns: - A list of tuples. Each tuple is the set of qubits for which there is a schedule - named ``schedule_name`` and ``partial_qubits`` is a sub-set of said qubits. - """ - for key, circuit_inst_num_qubits in self._schedules_qubits.items(): - if key.schedule == schedule_name: - if len(partial_qubits) == circuit_inst_num_qubits: - return [partial_qubits] - - else: - candidates = self._operated_qubits[circuit_inst_num_qubits] - qubits_for_update = [] - for candidate_qubits in candidates: - if set(partial_qubits).issubset(set(candidate_qubits)): - qubits_for_update.append(tuple(candidate_qubits)) - - return qubits_for_update - - return [] - - def inst_map_add( - self, - instruction_name: str, - qubits: Tuple[int], - schedule_name: Optional[str] = None, - assign_params: Optional[Dict[Union[str, ParameterKey], ParameterValueType]] = None, - ): - """Update a single instruction in the instruction schedule map. - - This method can be used to update a single instruction for the given qubits but - it can also be used by experiments that define custom gates with parameters - such as the :class:`Rabi` experiment. In a Rabi experiment there is a gate named - "Rabi" that scans a pulse with a custom amplitude. Therefore we would do - - .. code-block:: python - - cals.inst_map_add("Rabi", (0, ), "xp", assign_params={"amp": Parameter("amp")}) - - to temporarily add a pulse for the Rabi gate in the instruction schedule map. This - then allows calling :code:`transpile(circ, inst_map=cals.default_inst_map)`. - - Args: - instruction_name: The name of the instruction to add to the instruction schedule map. - qubits: The qubits to which the instruction will apply. - schedule_name: The name of the schedule. If None is given then we assume that the - schedule and the instruction have the same name. - assign_params: An optional dict of parameter mappings to apply. See for instance - :meth:`.get_schedule` of :class:`.Calibrations`. - """ - schedule_name = schedule_name or instruction_name - - inst_map_args = None - if assign_params is not None: - inst_map_args = assign_params.keys() - - self._inst_map.add( - instruction=instruction_name, - qubits=qubits, - schedule=self.get_schedule(schedule_name, qubits, assign_params), - arguments=inst_map_args, - ) - - def add_schedule( - self, - schedule: ScheduleBlock, - qubits: Union[int, Tuple[int, ...]] = None, - num_qubits: Optional[int] = None, - ): - """Add a schedule block and register its parameters. - - Schedules that use Call instructions must register the called schedules separately. - - Args: - schedule: The :class:`ScheduleBlock` to add. - qubits: The qubits for which to add the schedules. If None or an empty tuple is - given then this schedule is the default schedule for all qubits and, in this - case, the number of qubits that this schedule act on must be given. - num_qubits: The number of qubits that this schedule will act on when exported to - a circuit instruction. This argument is optional as long as qubits is either - not None or not an empty tuple (i.e. default schedule). - - Raises: - CalibrationError: If schedule is not an instance of :class:`ScheduleBlock`. - CalibrationError: If a schedule has assigned references. - CalibrationError: If several parameters in the same schedule have the same name. - CalibrationError: If the schedule name starts with the prefix of ScheduleBlock. - CalibrationError: If the schedule calls subroutines that have not been registered. - CalibrationError: If a :class:`Schedule` is Called instead of a :class:`ScheduleBlock`. - CalibrationError: If a schedule with the same name exists and acts on a different - number of qubits. - - """ - self._has_manually_added_schedule = True - - qubits = self._to_tuple(qubits) - - if len(qubits) == 0 and num_qubits is None: - raise CalibrationError("Both qubits and num_qubits cannot simultaneously be None.") - - num_qubits = len(qubits) or num_qubits - - if not isinstance(schedule, ScheduleBlock): - raise CalibrationError(f"{schedule.name} is not a ScheduleBlock.") - - if len(schedule.references) != len(schedule.references.unassigned()): - raise CalibrationError( - f"Cannot add {schedule} with assigned references. {self.__class__.__name__} only " - "accepts schedules without references or schedules with references by name." - ) - - sched_key = ScheduleKey(schedule.name, qubits) - - # Ensure one to one mapping between name and number of qubits. - if sched_key in self._schedules_qubits and self._schedules_qubits[sched_key] != num_qubits: - raise CalibrationError( - f"Cannot add schedule {schedule.name} acting on {num_qubits} qubits." - "self already contains a schedule with the same name acting on " - f"{self._schedules_qubits[sched_key]} qubits. Remove old schedule first." - ) - - # check that channels, if parameterized, have the proper name format. - if schedule.name.startswith(ScheduleBlock.prefix): - raise CalibrationError( - f"{self.__class__.__name__} uses the `name` property of the schedule as part of a " - f"database key. Using the automatically generated name {schedule.name} may have " - f"unintended consequences. Please define a meaningful and unique schedule name." - ) - - param_indices = validate_channels(schedule) - - # Check that subroutines are present. - for reference in schedule.references: - self.get_template(*reference_info(reference, qubits)) - - # Clean the parameter to schedule mapping. This is needed if we overwrite a schedule. - self._clean_parameter_map(schedule.name, qubits) - - # Add the schedule. - self._schedules[sched_key] = schedule - self._schedules_qubits[sched_key] = num_qubits - - # Update the schedule dependency. - update_schedule_dependency(schedule, self._schedule_dependency, sched_key) - - # Register parameters that are not indices. - params_to_register = set() - for param in schedule.parameters: - if param not in param_indices: - params_to_register.add(param) - - if len(params_to_register) != len(set(param.name for param in params_to_register)): - raise CalibrationError(f"Parameter names in {schedule.name} must be unique.") - - for param in params_to_register: - self._register_parameter(param, qubits, schedule) - - def has_template(self, schedule_name: str, qubits: Optional[Tuple[int, ...]] = None) -> bool: - """Test if a template schedule is defined - - Args: - schedule_name: The name of the template schedule. - qubits: The qubits under which the template schedule was registered. - - Returns: - True if a template exists for the schedule name for the given qubits - """ - found = False - try: - self.get_template(schedule_name, qubits) - found = True - except CalibrationError: - pass - - return found - - def get_template( - self, schedule_name: str, qubits: Optional[Tuple[int, ...]] = None - ) -> ScheduleBlock: - """Get a template schedule. - - Allows the user to get a template schedule that was previously registered. - A template schedule will typically be fully parametric, i.e. all pulse - parameters and channel indices are represented by :class:`Parameter`. - - Args: - schedule_name: The name of the template schedule. - qubits: The qubits under which the template schedule was registered. - - Returns: - The registered template schedule. - - Raises: - CalibrationError: If no template schedule for the given schedule name and qubits - was registered. - """ - key = ScheduleKey(schedule_name, self._to_tuple(qubits)) - - if key in self._schedules: - return self._schedules[key] - - if ScheduleKey(schedule_name, ()) in self._schedules: - return self._schedules[ScheduleKey(schedule_name, ())] - - if qubits: - msg = f"Could not find schedule {schedule_name} on qubits {qubits}." - else: - msg = f"Could not find schedule {schedule_name}." - - raise CalibrationError(msg) - - def remove_schedule(self, schedule: ScheduleBlock, qubits: Union[int, Tuple[int, ...]] = None): - """Remove a schedule that was previously registered. - - Allows users to remove a schedule from the calibrations. The history of the parameters - will remain in the calibrations. - - Args: - schedule: The schedule to remove. - qubits: The qubits for which to remove the schedules. If None is given then this - schedule is the default schedule for all qubits. - - Raises: - CalibrationError: If other schedules depend on ``schedule``. - """ - qubits = self._to_tuple(qubits) - sched_key = ScheduleKey(schedule.name, qubits) - - # Remove the schedule from the schedule dependency DAG. Raise if others depend on it. - sched_idx = self._schedule_dependency.nodes().index(sched_key) - prev_nodes = self._schedule_dependency.predecessors(sched_idx) - if len(prev_nodes) > 0: - raise CalibrationError( - f"Cannot remove schedule {schedule.name} as {prev_nodes} depend on it." - ) - - self._schedule_dependency.remove_node(sched_idx) - - if sched_key in self._schedules: - del self._schedules[sched_key] - del self._schedules_qubits[sched_key] - - # Clean the parameter to schedule mapping. - self._clean_parameter_map(schedule.name, qubits) - - def _clean_parameter_map(self, schedule_name: str, qubits: Tuple[int, ...]): - """Clean the parameter to schedule mapping for the given schedule, parameter and qubits. - - Args: - schedule_name: The name of the schedule. - qubits: The qubits to which this schedule applies. - - """ - keys_to_remove = [] # of the form (schedule.name, parameter.name, qubits) - for key in self._parameter_map: - if key.schedule == schedule_name and key.qubits == qubits: - keys_to_remove.append(key) - - for key in keys_to_remove: - del self._parameter_map[key] - - # Key set is a set of tuples (schedule.name, parameter.name, qubits) - for param, key_set in self._parameter_map_r.items(): - if key in key_set: - key_set.remove(key) - - # Remove entries that do not point to at least one (schedule.name, parameter.name, qubits) - keys_to_delete = [] - for param, key_set in self._parameter_map_r.items(): - if not key_set: - keys_to_delete.append(param) - - for key in keys_to_delete: - del self._parameter_map_r[key] - - def _register_parameter( - self, - parameter: Parameter, - qubits: Tuple[int, ...], - schedule: ScheduleBlock = None, - ): - """Registers a parameter for the given schedule. - - This method allows self to determine the parameter instance that corresponds to the given - schedule name, parameter name and qubits. - - Args: - parameter: The parameter to register. - qubits: The qubits for which to register the parameter. - schedule: The schedule to which this parameter belongs. The schedule can - be None which allows the calibration to accommodate, e.g. qubit frequencies. - """ - if parameter not in self._hash_to_counter_map: - self._hash_to_counter_map[parameter] = self._parameter_counter - self._parameter_counter += 1 - - sched_name = schedule.name if schedule else None - key = ParameterKey(parameter.name, qubits, sched_name) - self._parameter_map[key] = parameter - self._parameter_map_r[parameter].add(key) - - @property - def parameters(self) -> Dict[Parameter, Set[ParameterKey]]: - """Return a mapping between parameters and parameter keys. - - Returns a dictionary mapping parameters managed by the calibrations to the schedules and - qubits and parameter names using the parameters. The values of the dict are sets containing - the parameter keys. Parameters that are not attached to a schedule will have None in place - of a schedule name. - """ - return self._parameter_map_r - - def calibration_parameter( - self, - parameter_name: str, - qubits: Union[int, Tuple[int, ...]] = None, - schedule_name: str = None, - ) -> Parameter: - """Return a parameter given its keys. - - Returns a Parameter object given the triplet parameter_name, qubits and schedule_name - which uniquely determine the context of a parameter. - - Args: - parameter_name: Name of the parameter to get. - qubits: The qubits to which this parameter belongs. If qubits is None then - the default scope is assumed and the key will be an empty tuple. - schedule_name: The name of the schedule to which this parameter belongs. A - parameter may not belong to a schedule in which case None is accepted. - - Returns: - calibration parameter: The parameter that corresponds to the given arguments. - - Raises: - CalibrationError: If the desired parameter is not found. - """ - qubits = self._to_tuple(qubits) - - # 1) Check for qubit specific parameters. - if ParameterKey(parameter_name, qubits, schedule_name) in self._parameter_map: - return self._parameter_map[ParameterKey(parameter_name, qubits, schedule_name)] - - # 2) Check for default parameters. - elif ParameterKey(parameter_name, (), schedule_name) in self._parameter_map: - return self._parameter_map[ParameterKey(parameter_name, (), schedule_name)] - else: - raise CalibrationError( - f"No parameter for {parameter_name} and schedule {schedule_name} " - f"and qubits {qubits}. No default value exists." - ) - - def add_parameter_value( - self, - value: Union[int, float, complex, ParameterValue], - param: Union[Parameter, str], - qubits: Union[int, Tuple[int, ...]] = None, - schedule: Union[ScheduleBlock, str] = None, - update_inst_map: bool = True, - ): - """Add a parameter value to the stored parameters. - - This parameter value may be applied to several channels, for instance, all - DRAG pulses may have the same standard deviation. - - Args: - value: The value of the parameter to add. If an int, float, or complex is given - then the timestamp of the parameter value will automatically be generated - and set to the current local time of the user. - param: The parameter or its name for which to add the measured value. - qubits: The qubits to which this parameter applies. - schedule: The schedule or its name for which to add the measured parameter value. - update_inst_map: Update the instruction schedule map if True (the default). - - Raises: - CalibrationError: If the schedule name is given but no schedule with that name - exists. - """ - qubits = self._to_tuple(qubits) - - if isinstance(value, (int, float, complex)): - value = ParameterValue(value, datetime.now(timezone.utc).astimezone()) - - param_name = param.name if isinstance(param, Parameter) else param - sched_name = schedule.name if isinstance(schedule, ScheduleBlock) else schedule - - registered_schedules = set(key.schedule for key in self._schedules) - - if sched_name and sched_name not in registered_schedules: - raise CalibrationError(f"Schedule named {sched_name} was never registered.") - - self._params[ParameterKey(param_name, qubits, sched_name)].append(value) - - # When updating the inst_map we need to - # a) Update all the schedules that use the parameter to be updated - # b) Update all schedules that reference the updated schedules under a) - if update_inst_map and schedule is not None: - param_obj = self.calibration_parameter(param_name, qubits, sched_name) - - # Take care of a) i.e. update all schedules that use the parameter. - schedules = set(key.schedule for key in self._parameter_map_r[param_obj]) - keys = set(ScheduleKey(sched, qubits) for sched in schedules) - - # Take care of b) i.e. find all schedules that refer to the schedules that - # make use of the updated parameter. - schedules.update(used_in_references(keys, self._schedule_dependency)) - self.update_inst_map(schedules, qubits=qubits) - - def _get_channel_index(self, qubits: Tuple[int, ...], chan: PulseChannel) -> int: - """Get the index of the parameterized channel. - - The return index is determined from the given qubits and the name of the parameter - in the channel index. The name of this parameter for control channels must be written - as chqubit_index1.qubit_index2... followed by an optional $index. For example, the - following parameter names are valid: 'ch1', 'ch1.0', 'ch30.12', and 'ch1.0$1'. - - Args: - qubits: The qubits for which we want to obtain the channel index. - chan: The channel with a parameterized name. - - Returns: - index: The index of the channel. For example, if qubits=(10, 32) and - chan is a control channel with parameterized index name 'ch1.0' - the method returns the control channel corresponding to - qubits (qubits[1], qubits[0]) which is here the control channel of - qubits (32, 10). - - Raises: - CalibrationError: - - If the number of qubits is incorrect. - - If the number of inferred ControlChannels is not correct. - - If ch is not a DriveChannel, MeasureChannel, or ControlChannel. - """ - if isinstance(chan.index, Parameter): - if isinstance( - chan, (DriveChannel, MeasureChannel, AcquireChannel, RegisterSlot, MemorySlot) - ): - index = int(chan.index.name[2:].split("$")[0]) - - if len(qubits) <= index: - raise CalibrationError(f"Not enough qubits given for channel {chan}.") - - return qubits[index] - - # Control channels name example ch1.0$1 - if isinstance(chan, ControlChannel): - channel_index_parts = chan.index.name[2:].split("$") - qubit_channels = channel_index_parts[0] - - indices = [int(sub_channel) for sub_channel in qubit_channels.split(".")] - ch_qubits = tuple(qubits[index] for index in indices) - chs_ = self._control_channel_map.get(ch_qubits, []) - - control_index = 0 - if len(channel_index_parts) == 2: - control_index = int(channel_index_parts[1]) - - if len(chs_) <= control_index: - raise CalibrationError( - f"Control channel index {control_index} not found for qubits {qubits}." - ) - - return chs_[control_index].index - - raise CalibrationError( - f"{chan} must be a sub-type of {PulseChannel} or an {AcquireChannel}, " - f"{RegisterSlot}, or a {MemorySlot}." - ) - - return chan.index - - def get_parameter_value( - self, - param: Union[Parameter, str], - qubits: Union[int, Tuple[int, ...]], - schedule: Union[ScheduleBlock, str, None] = None, - valid_only: bool = True, - group: str = "default", - cutoff_date: datetime = None, - ) -> Union[int, float, complex]: - """Retrieves the value of a parameter. - - Parameters may be linked. :meth:`get_parameter_value` does the following steps: - - 1. Retrieve the parameter object corresponding to (param, qubits, schedule). - 2. The values of this parameter may be stored under another schedule since - schedules can share parameters. To deal with this, a list of candidate keys - is created internally based on the current configuration. - 3. Look for candidate parameter values under the candidate keys. - 4. Filter the candidate parameter values according to their date (up until the - cutoff_date), validity and calibration group. - 5. Return the most recent parameter. - - Args: - param: The parameter or the name of the parameter for which to get the parameter value. - qubits: The qubits for which to get the value of the parameter. - schedule: The schedule or its name for which to get the parameter value. - valid_only: Use only parameters marked as valid. - group: The calibration group from which to draw the parameters. - If not specified this defaults to the 'default' group. - cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters - generated after the cutoff date will be ignored. If the cutoff_date is None then - all parameters are considered. This allows users to discard more recent values that - may be erroneous. - - Returns: - value: The value of the parameter. - - Raises: - CalibrationError: If there is no parameter value for the given parameter name and - pulse channel. - """ - qubits = self._to_tuple(qubits) - - # 1) Identify the parameter object. - param_name = param.name if isinstance(param, Parameter) else param - sched_name = schedule.name if isinstance(schedule, ScheduleBlock) else schedule - - param = self.calibration_parameter(param_name, qubits, sched_name) - - # 2) Get a list of candidate keys restricted to the qubits of interest. - candidate_keys = [] - for key in self._parameter_map_r[param]: - candidate_keys.append(ParameterKey(key.parameter, qubits, key.schedule)) - - # 3) Loop though the candidate keys to candidate values - candidates = [] - for key in candidate_keys: - if key in self._params: - candidates += self._params[key] - - # If no candidate parameter values were found look for default parameters - # i.e. parameters that do not specify a qubit. - if len(candidates) == 0: - for key in candidate_keys: - if ParameterKey(key.parameter, (), key.schedule) in self._params: - candidates += self._params[ParameterKey(key.parameter, (), key.schedule)] - - # 4) Filter candidate parameter values. - if valid_only: - candidates = [val for val in candidates if val.valid] - - candidates = [val for val in candidates if val.group == group] - - if cutoff_date: - cutoff_date = cutoff_date.astimezone() - candidates = [val for val in candidates if val.date_time <= cutoff_date] - - if len(candidates) == 0: - msg = f"No candidate parameter values for {param_name} in calibration group {group} " - - if qubits: - msg += f"on qubits {qubits} " - - if sched_name: - msg += f"in schedule {sched_name} " - - if cutoff_date: - msg += f"with cutoff date: {cutoff_date}" - - raise CalibrationError(msg) - - # 5) Return the most recent parameter. - return max(enumerate(candidates), key=lambda x: (x[1].date_time, x[0]))[1].value - - def _standardize_assign_params( - self, - assign_params: Dict, - qubits: Tuple[int, ...], - schedule_name: str, - ) -> Dict[ParameterKey, ParameterValueType]: - """Standardize the format of manually specified parameter assignments. - - Users specify parameter assignment dictionaries as tuples. This function (a) converts - these tuples to ``ParameterKey`` instances and (b) looks up parameter links between - schedules and adds them to the standardized returned parameter assignment dictionary. - - Args: - assign_params: The dictionary that specifies how to assign parameters. - qubits: The qubits for which to get a schedule. - schedule_name: The name of the schedule in which the parameters can be found. - - Returns: - A dict with :class:`.ParameterKey`s as keys and :class:`.ParameterValueType`s as values. - """ - linked_assign_params = {} - - if assign_params: - # Add parameter links for automatic linking. - for param, value in assign_params.items(): - if isinstance(param, str): - key = ParameterKey(param, qubits, schedule_name) - else: - key = ParameterKey(*param) - linked_assign_params[key] = value - param = self.calibration_parameter(*key) - for key2 in self._parameter_map_r[param]: # Loop over linked keys pointing to param - # Link only over the same qubits. - if key2.qubits == (): - key2 = ParameterKey(key2.parameter, key.qubits, key2.schedule) - - if key2 not in linked_assign_params: - linked_assign_params[key2] = value - - return linked_assign_params - - def get_schedule( - self, - name: str, - qubits: Union[int, Tuple[int, ...]], - assign_params: Dict[Union[str, ParameterKey], ParameterValueType] = None, - group: Optional[str] = "default", - cutoff_date: datetime = None, - ) -> ScheduleBlock: - """Get the template schedule with parameters assigned to values. - - All the parameters in the template schedule block will be assigned to the values managed - by the calibrations unless they are specified in assign_params. In this case the value in - assign_params will override the value stored by the calibrations. A parameter value in - assign_params may also be a :class:`ParameterExpression`. - - .. code-block:: python - - # Get an xp schedule with a parametric amplitude - sched = cals.get_schedule("xp", 3, assign_params={"amp": Parameter("amp")}) - - # Get an echoed-cross-resonance schedule between qubits (0, 2) where the xp echo gates - # are referenced schedules but leave their amplitudes as parameters. - assign_dict = {("amp", (0,), "xp"): Parameter("my_amp")} - sched = cals.get_schedule("cr", (0, 2), assign_params=assign_dict) - - Args: - name: The name of the schedule to get. - qubits: The qubits for which to get the schedule. - assign_params: The parameters to assign manually. Each parameter is specified by a - ParameterKey which is a named tuple of the form (parameter name, qubits, - schedule name). Each entry in assign_params can also be a string corresponding - to the name of the parameter. In this case, the schedule name and qubits of the - corresponding ParameterKey will be the name and qubits given as arguments to - get_schedule. - group: The calibration group from which to draw the parameters. If not specified - this defaults to the 'default' group. - cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters - generated after the cutoff date will be ignored. If the cutoff_date is None then - all parameters are considered. This allows users to discard more recent values that - may be erroneous. - - Returns: - schedule: A copy of the template schedule with all parameters assigned. - - Raises: - CalibrationError: If the name of the schedule is not known. - CalibrationError: If a parameter could not be found. - """ - qubits = self._to_tuple(qubits) - - assign_params = self._standardize_assign_params(assign_params, qubits, name) - - schedule = self.get_template(name, qubits) - - # assign any references using a depth first-search. - referenced_schedules = {} - for ref in schedule.references.unassigned(): - ref_name, ref_qubits = reference_info(ref, qubits) - ref_assign = {} - for key, val in assign_params.items(): - if key.schedule == ref_name and (key.qubits == tuple() or key.qubits == ref_qubits): - ref_assign[key] = val - - referenced_schedules[ref] = self.get_schedule( - ref_name, - ref_qubits, - assign_params=ref_assign, - ) - schedule = schedule.assign_references(referenced_schedules, inplace=False) - - # Retrieve the channel indices based on the qubits and bind them. - binding_dict = {} - for ch in schedule.channels: - if ch.is_parameterized(): - binding_dict[ch.index] = self._get_channel_index(qubits, ch) - - # Binding the channel indices makes it easier to deal with parameters later on - schedule = schedule.assign_parameters(binding_dict, inplace=False) - - # Now assign the other parameters - assigned_schedule = self._assign(schedule, qubits, assign_params, group, cutoff_date) - - free_params = set() - for param_value in assign_params.values(): - if isinstance(param_value, ParameterExpression): - free_params.add(param_value) - - if len(assigned_schedule.parameters) != len(free_params): - raise CalibrationError( - f"The number of free parameters {len(assigned_schedule.parameters)} in " - f"the assigned schedule differs from the requested number of free " - f"parameters {len(free_params)}." - ) - - return assigned_schedule - - def _assign( - self, - schedule: ScheduleBlock, - qubits: Tuple[int, ...], - assign_params: Dict[Union[str, ParameterKey], ParameterValueType], - group: Optional[str] = "default", - cutoff_date: datetime = None, - ) -> ScheduleBlock: - """Assign parameters in a schedule. - - This function builds the binding dictionary and ensures that manually specified parameters - have priority over those managed by the calibrations class. - - Args: - schedule: The schedule with assigned channel indices for which we wish to - assign values to non-channel parameters. - qubits: The qubits for which to get the schedule. - assign_params: The parameters to manually assign. See get_schedules for details. - group: The calibration group of the parameters. - cutoff_date: Retrieve the most recent parameter up until the cutoff date. Parameters - generated after the cutoff date will be ignored. If the cutoff_date is None then - all parameters are considered. This allows users to discard more recent values that - may be erroneous. - - Returns: - ret_schedule: The schedule with assigned parameters. - - Raises: - CalibrationError: If a channel has not been assigned. - CalibrationError: If there is an ambiguous parameter assignment. - CalibrationError: If there are inconsistencies between a called schedule and the - template schedule registered under the name of the called schedule. - """ - # 1) Restrict the given qubits to those in the given schedule. - qubit_set = set() - for chan in schedule.channels: - if isinstance(chan.index, ParameterExpression): - raise CalibrationError( - f"Parametric channels must be assigned. {chan} is parametric." - ) - - if isinstance(chan, (DriveChannel, MeasureChannel)): - qubit_set.add(chan.index) - - if isinstance(chan, ControlChannel): - for qubit in self._controls_config_r[chan]: - qubit_set.add(qubit) - - qubits_ = tuple(qubit for qubit in qubits if qubit in qubit_set) - - # 2) Build the parameter binding dictionary. - # 2a) Handle parameter assignments that come outside of Calibrations - binding_dict = {} - assignment_table = {} - for key, value in assign_params.items(): - key_orig = key - if key.qubits == (): - key = ParameterKey(key.parameter, qubits_, key.schedule) - if key in assign_params: - # if (param, (1,), sched) and (param, (), sched) are both - # in assign_params, skip the default value instead of - # possibly triggering an error about conflicting - # parameters. - continue - elif key.qubits != qubits_: - continue - param = self.calibration_parameter(*key) - if param in schedule.parameters: - assign_okay = ( - param not in binding_dict - or key.schedule == schedule.name - and assignment_table[param].schedule != schedule.name - ) - if assign_okay: - binding_dict[param] = value - assignment_table[param] = key_orig - elif ( - key.schedule == schedule.name - or assignment_table[param].schedule != schedule.name - ) and binding_dict[param] != value: - raise CalibrationError( - "Ambiguous assignment: assign_params keys " - f"{key_orig} and {assignment_table[param]} " - "resolve to the same parameter." - ) - - # 2b) Handle automatic parameter assignments managed by calibrations. - if schedule.name in set(key.schedule for key in self._parameter_map): # skip references - for param in schedule.parameters: - key = ParameterKey(param.name, qubits_, schedule.name) - # Get the parameter object. Since we are dealing with a schedule the name of - # the schedule is always defined. However, the parameter may be a default - # parameter for all qubits, i.e. qubits may be an empty tuple. - param = self.calibration_parameter(*key) - - if param not in binding_dict: - binding_dict[param] = self.get_parameter_value( - key.parameter, - key.qubits, - key.schedule, - group=group, - cutoff_date=cutoff_date, - ) - - return schedule.assign_parameters(binding_dict, inplace=False) - - def schedules(self) -> List[Dict[str, Any]]: - """Return the managed schedules in a list of dictionaries. - - Returns: - data: A list of dictionaries with all the schedules in it. The key-value pairs are - - * ``qubits``: the qubits to which this schedule applies. This may be an empty - tuple () if the schedule is the default for all qubits. - * ``schedule``: The schedule. - * ``parameters``: The parameters in the schedule exposed for convenience. - - This list of dictionaries can easily be converted to a data frame. - """ - data = [] - for key, sched in self._schedules.items(): - data.append({"qubits": key.qubits, "schedule": sched, "parameters": sched.parameters}) - - return data - - def parameters_table( - self, - parameters: List[str] = None, - qubit_list: List[Tuple[int, ...]] = None, - schedules: List[Union[ScheduleBlock, str]] = None, - most_recent_only: bool = True, - group: Optional[str] = None, - ) -> Dict[str, Union[List[Dict], List[str]]]: - """A convenience function to help users visualize the values of their parameter. - - Args: - parameters: The parameter names that should be included in the returned - table. If None is given then all names are included. - qubit_list: The qubits that should be included in the returned table. - If None is given then all channels are returned. - schedules: The schedules to which to restrict the output. - most_recent_only: return only the most recent parameter values. - group: If the group is given then only the parameters from this group are returned. - - Returns: - A dictionary with the keys "data" and "columns" that can easily - be converted to a data frame. The "data" are a list of dictionaries - each holding a parameter value. The "columns" are the keys in the "data" - dictionaries and are returned in the preferred display order. - """ - if qubit_list: - qubit_list = [self._to_tuple(qubits) for qubits in qubit_list] - - # Convert inputs to lists of strings - if schedules is not None: - schedules = {sdl.name if isinstance(sdl, ScheduleBlock) else sdl for sdl in schedules} - - # Look for exact matches. Default values will be ignored. - keys = set() - for key in self._params.keys(): - if parameters and key.parameter not in parameters: - continue - if schedules and key.schedule not in schedules: - continue - if qubit_list and key.qubits not in qubit_list: - continue - - keys.add(key) - - data = [] - if most_recent_only: - most_recent = {k: max(self._params[k], key=lambda x: x.date_time) for k in keys} - - for key, value in most_recent.items(): - self._append_to_list(data, value, key, group) - - else: - for key in keys: - for value in self._params[key]: - self._append_to_list(data, value, key, group) - - columns = [ - "parameter", - "qubits", - "schedule", - "value", - "group", - "valid", - "date_time", - "exp_id", - ] - return {"data": data, "columns": columns} - - @staticmethod - def _append_to_list( - data: List[Dict], value: ParameterValue, key: ParameterKey, group: Optional[str] = None - ): - """Helper function to add a value to the data.""" - if group and value.group != group: - return - - value_dict = dataclasses.asdict(value) - value_dict["qubits"] = key.qubits - value_dict["parameter"] = key.parameter - value_dict["schedule"] = key.schedule - value_dict["date_time"] = value_dict["date_time"].strftime("%Y-%m-%d %H:%M:%S.%f%z") - data.append(value_dict) - - @deprecate_arg( - name="file_type", - since="0.6", - additional_msg="Full calibration saving is now supported in json format. csv is deprecated.", - package_name="qiskit-experiments", - predicate=lambda file_type: file_type == "csv", - ) - def save( - self, - file_type: str = "json", - folder: str = None, - overwrite: bool = False, - file_prefix: str = "", - most_recent_only: bool = False, - ): - """Save the parameterized schedules and parameter value. - - .. note:: - - Full round-trip serialization of a :class:`.Calibrations` instance - is only supported in JSON format. - This may be extended to other file formats in future version. - - Args: - file_type: The type of file to which to save. By default, this is a json. - Other file types may be supported in the future. - folder: The folder in which to save the calibrations. - overwrite: If the files already exist then they will not be overwritten - unless overwrite is set to True. - file_prefix: A prefix to add to the name of the files such as a date tag or a - UUID. - most_recent_only: Save only the most recent value. This is set to False by - default so that when saving to csv all values will be saved. - - Raises: - CalibrationError: If the files exist and overwrite is not set to True. - """ - cwd = os.getcwd() - if folder: - os.chdir(folder) - - if file_type == "json": - from .save_utils import calibrations_to_dict - - file_path = file_prefix + ".json" - if os.path.isfile(file_path) and not overwrite: - raise CalibrationError(f"{file_path} already exists. Set overwrite to True.") - - canonical_data = calibrations_to_dict(self, most_recent_only=most_recent_only) - with open(file_path, "w", encoding="utf-8") as file: - json.dump(canonical_data, file, cls=ExperimentEncoder) - - elif file_type == "csv": - warnings.warn("Schedules are only saved in text format. They cannot be re-loaded.") - - parameter_config_file = file_prefix + "parameter_config.csv" - parameter_value_file = file_prefix + "parameter_values.csv" - schedule_file = file_prefix + "schedules.csv" - - if os.path.isfile(parameter_config_file) and not overwrite: - raise CalibrationError( - f"{parameter_config_file} already exists. Set overwrite to True." - ) - - if os.path.isfile(parameter_value_file) and not overwrite: - raise CalibrationError( - f"{parameter_value_file} already exists. Set overwrite to True." - ) - - if os.path.isfile(schedule_file) and not overwrite: - raise CalibrationError(f"{schedule_file} already exists. Set overwrite to True.") - - # Write the parameter configuration. - header_keys = ["parameter.name", "parameter unique id", "schedule", "qubits"] - body = [] - - for parameter, keys in self.parameters.items(): - for key in keys: - body.append( - { - "parameter.name": parameter.name, - "parameter unique id": self._hash_to_counter_map[parameter], - "schedule": key.schedule, - "qubits": key.qubits, - } - ) - - with open(parameter_config_file, "w", newline="", encoding="utf-8") as output_file: - dict_writer = csv.DictWriter(output_file, header_keys) - dict_writer.writeheader() - dict_writer.writerows(body) - - # Write the values of the parameters. - values = self.parameters_table(most_recent_only=most_recent_only)["data"] - if len(values) > 0: - header_keys = values[0].keys() - - with open(parameter_value_file, "w", newline="", encoding="utf-8") as output_file: - dict_writer = csv.DictWriter(output_file, header_keys) - dict_writer.writeheader() - dict_writer.writerows(values) - - # Serialize the schedules. For now we just print them. - header_keys, schedules = self.schedule_information() - - with open(schedule_file, "w", newline="", encoding="utf-8") as output_file: - dict_writer = csv.DictWriter(output_file, header_keys) - dict_writer.writeheader() - dict_writer.writerows(schedules) - - else: - raise CalibrationError(f"Saving to .{file_type} is not yet supported.") - - os.chdir(cwd) - - @deprecate_func( - since="0.6", - additional_msg=( - "Saving calibration in csv format is deprecate " - "as well as functions that support this functionality." - ), - package_name="qiskit-experiments", - ) - def schedule_information(self) -> Tuple[List[str], List[Dict]]: - """Get the information on the schedules stored in the calibrations. - - This function serializes the schedule by simply printing them. - - Returns: - A tuple, the first element is the header row while the second is a dictionary - of the schedules in the calibrations where the key is an element of the header - and the values are the name of the schedule, the qubits to which it applies, - a string of the schedule. - """ - # Serialize the schedules. For now we just print them. - schedules = [] - for key, sched in self._schedules.items(): - schedules.append({"name": key.schedule, "qubits": key.qubits, "schedule": str(sched)}) - - return ["name", "qubits", "schedule"], schedules - - @deprecate_func( - since="0.6", - additional_msg="Loading and saving calibrations in CSV format is deprecated.", - package_name="qiskit-experiments", - ) - def load_parameter_values(self, file_name: str = "parameter_values.csv"): - """ - Load parameter values from a given file into self._params. - - Args: - file_name: The name of the file that stores the parameters. Will default to - parameter_values.csv. - """ - with open(file_name, encoding="utf-8") as fp: - reader = csv.DictReader(fp, delimiter=",", quotechar='"') - - for row in reader: - self._add_parameter_value_from_conf(**row) - - self.update_inst_map() - - def _add_parameter_value_from_conf( - self, - value: Union[str, int, float, complex], - date_time: str, - valid: Union[str, bool], - exp_id: str, - group: str, - schedule: Union[str, None], - parameter: str, - qubits: Union[str, int, Tuple[int, ...]], - ): - """Add a parameter value from a parameter configuration. - - The intended usage is :code:`add_parameter_from_conf(**param_conf)`. Entries such - as ``value`` or ``date_time`` are converted to the proper type. - - Args: - value: The value of the parameter. - date_time: The datetime string. - valid: Whether or not the parameter is valid. - exp_id: The id of the experiment that created the parameter value. - group: The calibration group to which the parameter belongs. - schedule: The schedule to which the parameter belongs. The empty string - "" is converted to None. - parameter: The name of the parameter. - qubits: The qubits on which the parameter acts. - """ - # TODO remove this after load_parameter_values method is removed. - - param_val = ParameterValue(value, date_time, valid, exp_id, group) - - if schedule == "": - schedule_name = None - else: - schedule_name = schedule - - key = ParameterKey(parameter, self._to_tuple(qubits), schedule_name) - self.add_parameter_value(param_val, *key, update_inst_map=False) - - @classmethod - @deprecate_arg( - name="files", - new_alias="file_path", - since="0.6", - package_name="qiskit-experiments", - ) - def load(cls, file_path: str) -> "Calibrations": - """ - Retrieves the parameterized schedules and pulse parameters from the - given location. - - Args: - file_path: Path to file location. - - Returns: - Calibration instance restored from the file. - """ - from .save_utils import calibrations_from_dict - - with open(file_path, "r", encoding="utf-8") as file: - # Do we really need branching for data types? - # Parsing data format and dispatching the loader seems an overkill, - # but save method intend to support multiple formats. - cal_data = json.load(file, cls=ExperimentDecoder) - - return calibrations_from_dict(cal_data) - - @staticmethod - def _to_tuple(qubits: Union[str, int, Tuple[int, ...]]) -> Tuple[int, ...]: - """Ensure that qubits is a tuple of ints. - - Args: - qubits: An int, a tuple of ints, or a string representing a tuple of ints. - - Returns: - qubits: A tuple of ints. - - Raises: - CalibrationError: If the given input does not conform to an int or - tuple of ints. - """ - if qubits is None: - return tuple() - - if isinstance(qubits, str): - try: - return tuple(int(qubit) for qubit in qubits.strip("( )").split(",") if qubit != "") - except ValueError: - pass - - if isinstance(qubits, int): - return (qubits,) - - if isinstance(qubits, list): - return tuple(qubits) - - if isinstance(qubits, tuple): - if all(isinstance(n, int) for n in qubits): - return qubits - - raise CalibrationError( - f"{qubits} must be int, tuple of ints, or str that can be parsed" - f"to a tuple if ints. Received {qubits}." - ) - - def __eq__(self, other: "Calibrations") -> bool: - """Test equality between two calibrations. - - Two calibration instances are considered equal if - - The backends have the same name. - - The backends have the same version. - - The calibrations contain the same schedules. - - The stored parameters have the same values. - """ - if self.backend_name != other.backend_name: - return False - - if self._backend_version != other.backend_version: - return False - - # Compare the contents of schedules, schedules are compared by their string - # representation because they contain parameters. - for key, schedule in self._schedules.items(): - if repr(schedule) != repr(other._schedules.get(key, None)): - return False - - # Check the keys. - if self._schedules.keys() != other._schedules.keys(): - return False - - def _counting(table): - return Counter(map(lambda d: tuple(d.items()), table["data"])) - - # Use counting sort algorithm to compare unordered sequences - # https://en.wikipedia.org/wiki/Counting_sort - return _counting(self.parameters_table()) == _counting(other.parameters_table()) - - @deprecate_func( - since="0.6", - additional_msg=( - "Configuration data for Calibrations instance is deprecate. " - "Please use ExperimentEncoder and ExperimentDecoder to " - "serialize and deserialize this instance with JSON format." - ), - package_name="qiskit-experiments", - ) - def config(self) -> Dict[str, Any]: - """Return the settings used to initialize the calibrations. - - Returns: - The config dictionary of the calibrations instance. - - Raises: - CalibrationError: If schedules were added outside of the :code:`__init__` - method. This will remain so until schedules can be serialized. - """ - if self._has_manually_added_schedule: - raise CalibrationError( - f"Config dictionaries for {self.__class__.__name__} are currently " - "not supported if schedules were added manually." - ) - - kwargs = { - "coupling_map": self._coupling_map, - "control_channel_map": ControlChannelMap(self._control_channel_map), - "libraries": self.libraries, - "add_parameter_defaults": False, # the parameters will be added outside of the init - "backend_name": self._backend_name, - "backend_version": self._backend_version, - } - - return { - "class": self.__class__.__name__, - "kwargs": kwargs, - "parameters": self.parameters_table()["data"], - } - - @classmethod - @deprecate_func( - since="0.6", - additional_msg="This method will be removed and no alternative will be provided.", - package_name="qiskit-experiments", - ) - def from_config(cls, config: Dict) -> "Calibrations": - """Restore Calibration from config data. - - Args: - config: Configuration data. - - Returns: - Calibration instance restored from configuration data. - """ - from .save_utils import calibrations_from_dict - - return calibrations_from_dict(config) - - def __json_encode__(self): - """Convert to format that can be JSON serialized.""" - from .save_utils import calibrations_to_dict - - return calibrations_to_dict(self, most_recent_only=False) - - @classmethod - def __json_decode__(cls, value: Dict[str, Any]) -> "Calibrations": - """Load from JSON compatible format.""" - from .save_utils import calibrations_from_dict - - return calibrations_from_dict(value) diff --git a/qiskit_experiments/calibration_management/control_channel_map.py b/qiskit_experiments/calibration_management/control_channel_map.py deleted file mode 100644 index d5ba3281ac..0000000000 --- a/qiskit_experiments/calibration_management/control_channel_map.py +++ /dev/null @@ -1,62 +0,0 @@ -# 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. - -"""A qubit to control channel map.""" - -from typing import Any, Dict, List, Tuple - -from qiskit.pulse import ControlChannel - - -class ControlChannelMap: - """A class to help serialize control channel maps.""" - - def __init__(self, control_channel_map: Dict[Tuple[int, ...], List[ControlChannel]]): - """Setup the control channel map. - - Args: - control_channel_map: A configuration dictionary of any control channels. The - keys are tuples of qubits and the values are a list of ControlChannels - that correspond to the qubits in the keys. - """ - self._map = control_channel_map or {} - - @property - def chan_map(self): - """Return the qubits to control channel map.""" - return self._map - - def config(self) -> Dict[str, Any]: - """Return the settings used to initialize the mapping.""" - return { - "class": self.__class__.__name__, - "map": [{"key": k, "value": [chan.index for chan in v]} for k, v in self._map.items()], - } - - @classmethod - def from_config(cls, config: Dict) -> "ControlChannelMap": - """Deserialize the control channel map given the input dictionary""" - - ch_map = config["map"] - - return cls( - {tuple(item["key"]): [ControlChannel(idx) for idx in item["value"]] for item in ch_map} - ) - - def __json_encode__(self): - """Convert to format that can be JSON serialized.""" - return self.config() - - @classmethod - def __json_decode__(cls, value: Dict[str, Any]) -> "ControlChannelMap": - """Load from JSON compatible format.""" - return cls.from_config(value) diff --git a/qiskit_experiments/calibration_management/parameter_value.py b/qiskit_experiments/calibration_management/parameter_value.py deleted file mode 100644 index 8044ebaeb9..0000000000 --- a/qiskit_experiments/calibration_management/parameter_value.py +++ /dev/null @@ -1,130 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Data class for parameter values.""" - -from dataclasses import dataclass -from datetime import datetime -from typing import Union -import warnings - -from qiskit_experiments.exceptions import CalibrationError - - -@dataclass -class ParameterValue: - """A data class to store parameter values.""" - - # Value assumed by the parameter - value: Union[int, float, complex] = None - - # Data time when the value of the parameter was generated - date_time: datetime = datetime.fromtimestamp(0) - - # A bool indicating if the parameter is valid - valid: bool = True - - # The experiment from which the value of this parameter was generated. - exp_id: str = None - - # The group of calibrations to which this parameter belongs - group: str = "default" - - def __post_init__(self): - """ - Ensure that the variables in self have the proper types. This allows - us to give strings to self.__init__ as input which is useful when loading - serialized parameter values. - """ - if isinstance(self.valid, str): - if self.valid == "True": - self.valid = True - else: - self.valid = False - - if isinstance(self.value, str): - self.value = self._validated_value(self.value) - - if isinstance(self.date_time, str): - base_fmt = "%Y-%m-%d %H:%M:%S.%f" - zone_fmts = ["%z", "", "Z"] - for time_zone in zone_fmts: - date_format = base_fmt + time_zone - try: - self.date_time = datetime.strptime(self.date_time, date_format) - break - except ValueError: - pass - else: - formats = list(base_fmt + zone for zone in zone_fmts) - raise CalibrationError( - f"Cannot parse {self.date_time} in either of {formats} formats." - ) - - self.date_time = self.date_time.astimezone() - - if isinstance(self.value, complex): - warnings.warn( - "Support of complex parameters is now pending deprecation, following the" - "same transition in Qiskit's Pulse module." - "The main use of complex parameters was the complex amplitude in SymbolicPulse" - "instances. This use could be removed by converting the pulses to the" - "ScalableSymbolicPulse class which uses two floats (amp,angle) for the" - "complex amplitude.", - PendingDeprecationWarning, - ) - - if not isinstance(self.value, (int, float, complex)): - raise CalibrationError(f"Values {self.value} must be int, float or complex.") - - if not isinstance(self.date_time, datetime): - raise CalibrationError(f"Datetime {self.date_time} must be a datetime.") - - if not isinstance(self.valid, bool): - raise CalibrationError(f"Valid {self.valid} is not a boolean.") - - if self.exp_id and not isinstance(self.exp_id, str): - raise CalibrationError(f"Experiment id {self.exp_id} is not a string.") - - if not isinstance(self.group, str): - raise CalibrationError(f"Group {self.group} is not a string.") - - @staticmethod - def _validated_value(value: str) -> Union[int, float, complex]: - """ - Convert the string representation of value to the correct type. - - Args: - value: The string to convert to either an int, float, or complex. - - Returns: - value converted to either int, float, or complex. - - Raises: - CalibrationError: If the conversion fails. - """ - try: - return int(value) - except ValueError: - pass - - try: - return float(value) - except ValueError: - pass - - try: - return complex(value) - except ValueError as val_err: - raise CalibrationError( - f"Could not convert {value} to int, float, or complex." - ) from val_err diff --git a/qiskit_experiments/calibration_management/save_utils.py b/qiskit_experiments/calibration_management/save_utils.py deleted file mode 100644 index 7be2054f3b..0000000000 --- a/qiskit_experiments/calibration_management/save_utils.py +++ /dev/null @@ -1,294 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""A helper module to save calibration data to local storage. - -.. warning:: - - This module is expected to be internal and is not intended as a stable user-facing API. - -.. note:: - - Because a locally saved :class:`.Calibrations` instance may not conform to the - data model of the latest Qiskit Experiments implementation, the calibration loader - must be aware of the data model version. - CalibrationModel classes representing the data model must have - the version suffix, e.g., `CalibrationModelV1` and the `schema_version` field. - This helps the loader to raise user-friendly error rather than being crashed by - incompatible data, and possibly to dispatch the loader function based on the version number. - - When a developer refactors the :class:`.Calibrations` class to a new data model, - the developer must also define a corresponding CalibrationModel class with new version number. - Existing CalibrationModel classes should be preserved for backward compatibility. - - -.. note:: - - We don't guarantee the portability of stored data across different Qiskit Experiments - versions. We allow the calibration loader to raise an error for non-supported - data models. - -""" - -from dataclasses import dataclass, field, asdict -from datetime import datetime -from typing import List, Dict, Any - -from qiskit import QuantumCircuit -from qiskit.circuit import Instruction -from qiskit.pulse import ScheduleBlock - -from .calibrations import Calibrations -from .control_channel_map import ControlChannelMap -from .parameter_value import ParameterValue - - -@dataclass(frozen=True) -class ParameterModelV1: - """A data schema of a single calibrated parameter. - - .. note:: - This is intentionally agnostic to the data structure of - Qiskit Experiments Calibrations for portability. - - """ - - param_name: str - """Name of the parameter.""" - - qubits: List[int] - """List of associated qubits.""" - - schedule: str = "" - """Associated schedule name.""" - - value: float = 0.0 - """Parameter value.""" - - datetime: datetime = None - """A date time at which this value is obtained.""" - - valid: bool = True - """If this parameter is valid.""" - - exp_id: str = "" - """Associated experiment ID which is used to obtain this value.""" - - group: str = "" - """Name of calibration group in which this calibration parameter belongs to.""" - - @classmethod - def apply_schema(cls, data: Dict[str, Any]): - """Consume dictionary and returns canonical data model.""" - return ParameterModelV1(**data) - - -@dataclass -class CalibrationModelV1: - """A data schema to represent instances of Calibrations. - - .. note:: - This is intentionally agnostic to the data structure of - Qiskit Experiments Calibrations for portability. - - """ - - backend_name: str - """Name of the backend.""" - - backend_version: str - """Version of the backend.""" - - device_coupling_graph: List[List[int]] - """Qubit coupling graph of the device.""" - - control_channel_map: ControlChannelMap - """Mapping of ControlChannel to qubit index.""" - - schedules: List[ScheduleBlock] = field(default_factory=list) - """Template schedules. It must contain the metadata for qubits and num_qubits.""" - - parameters: List[ParameterModelV1] = field(default_factory=list) - """List of calibrated pulse parameters.""" - - schedule_free_parameters: QuantumCircuit = field(default_factory=lambda: QuantumCircuit(1)) - """Placeholder circuit for parameters not associated with a schedule - - The circuit contains placeholder instructions which have the Parameter - objects attached and operate on the qubits that the parameter is associated - with in the calibrations. - """ - - schema_version: str = "1.0" - """Version of this data model. This must be static.""" - - @classmethod - def apply_schema(cls, data: Dict[str, Any]): - """Consume dictionary and returns canonical data model.""" - in_data = {} - for key, value in data.items(): - if key == "parameters": - value = list(map(ParameterModelV1.apply_schema, value)) - in_data[key] = value - return CalibrationModelV1(**in_data) - - -def calibrations_to_dict( - cals: Calibrations, - most_recent_only: bool = True, -) -> Dict[str, Any]: - """A helper function to convert calibration data into dictionary. - - Args: - cals: A calibration instance to save. - most_recent_only: Set True to save calibration parameters with most recent time stamps. - - Returns: - Canonical calibration data in dictionary format. - """ - schedules = getattr(cals, "_schedules") - num_qubits = getattr(cals, "_schedules_qubits") - parameters = getattr(cals, "_params") - if most_recent_only: - # Get values with most recent time stamps. - parameters = {k: [max(parameters[k], key=lambda x: x.date_time)] for k in parameters} - - data_entries = [] - for param_key, param_values in parameters.items(): - for param_value in param_values: - entry = ParameterModelV1( - param_name=param_key.parameter, - qubits=param_key.qubits, - schedule=param_key.schedule, - value=param_value.value, - datetime=param_value.date_time, - valid=param_value.valid, - exp_id=param_value.exp_id, - group=param_value.group, - ) - data_entries.append(entry) - - sched_entries = [] - for sched_key, sched_obj in schedules.items(): - if "qubits" not in sched_obj.metadata or "num_qubits" not in sched_obj.metadata: - qubit_metadata = { - "qubits": sched_key.qubits, - "num_qubits": num_qubits[sched_key], - } - sched_obj.metadata.update(qubit_metadata) - sched_entries.append(sched_obj) - - max_qubit = max( - (max(k.qubits or (0,)) for k in cals._parameter_map if k.schedule is None), - default=0, - ) - schedule_free_parameters = QuantumCircuit(max_qubit + 1) - for sched_key, param in cals._parameter_map.items(): - if sched_key.schedule is None: - schedule_free_parameters.append( - Instruction("parameter_container", len(sched_key.qubits), 0, [param]), - sched_key.qubits, - ) - - model = CalibrationModelV1( - backend_name=cals.backend_name, - backend_version=cals.backend_version, - device_coupling_graph=getattr(cals, "_coupling_map"), - control_channel_map=ControlChannelMap(getattr(cals, "_control_channel_map")), - schedules=sched_entries, - parameters=data_entries, - schedule_free_parameters=schedule_free_parameters, - ) - - return asdict(model) - - -def calibrations_from_dict( - cal_data: Dict[str, Any], -) -> Calibrations: - """A helper function to build calibration instance from canonical dictionary. - - Args: - cal_data: Calibration data dictionary which is formatted according to the - predefined data schema provided by Qiskit Experiments. - This formatting is implicitly performed when the calibration data is - dumped into dictionary with the :func:`calibrations_to_dict` function. - - Returns: - Calibration instance. - - Raises: - ValueError: When input data model version is not supported. - KeyError: When input data model doesn't conform to data schema. - """ - # Apply schema for data field validation - try: - version = cal_data["schema_version"] - if version == "1.0": - model = CalibrationModelV1.apply_schema(cal_data) - else: - raise ValueError( - f"Loading calibration data with schema version {version} is no longer supported. " - "Use the same version of Qiskit Experiments at the time of saving." - ) - except (KeyError, TypeError) as ex: - raise KeyError( - "Loaded data doesn't match with the defined data schema. " - "Check if this object is dumped from the Calibrations instance." - ) from ex - - # This can dispatch loading mechanism depending on schema version - cals = Calibrations( - coupling_map=model.device_coupling_graph, - control_channel_map=model.control_channel_map.chan_map, - backend_name=model.backend_name, - backend_version=model.backend_version, - ) - - # Add schedules - for sched in model.schedules: - qubits = sched.metadata.pop("qubits", tuple()) - num_qubits = sched.metadata.pop("num_qubits", None) - cals.add_schedule( - schedule=sched, - qubits=qubits if qubits and len(qubits) != 0 else None, - num_qubits=num_qubits, - ) - - # Add parameters - for param in model.parameters: - param_value = ParameterValue( - value=param.value, - date_time=param.datetime, - valid=param.valid, - exp_id=param.exp_id, - group=param.group, - ) - cals.add_parameter_value( - value=param_value, - param=param.param_name, - qubits=tuple(param.qubits), - schedule=param.schedule, - update_inst_map=False, - ) - - for instruction in model.schedule_free_parameters.data: - # For some reason, pylint thinks the items in data are tuples instead - # of CircuitInstruction. Remove the following line if it ever stops - # thinking that: - # pylint: disable=no-member - for param in instruction.operation.params: - cals._register_parameter(param, instruction.qubits) - - cals.update_inst_map() - - return cals diff --git a/qiskit_experiments/calibration_management/update_library.py b/qiskit_experiments/calibration_management/update_library.py deleted file mode 100644 index ff6009432c..0000000000 --- a/qiskit_experiments/calibration_management/update_library.py +++ /dev/null @@ -1,181 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""A library of experiment calibrations.""" - -from abc import ABC -from datetime import datetime, timezone -from typing import Optional, Union - -from qiskit.circuit import Parameter -from qiskit.pulse import ScheduleBlock - -from qiskit_experiments.framework.experiment_data import ExperimentData -from qiskit_experiments.calibration_management.calibrations import Calibrations -from qiskit_experiments.calibration_management.parameter_value import ParameterValue -from qiskit_experiments.calibration_management.calibration_key_types import ParameterValueType -from qiskit_experiments.exceptions import CalibrationError - - -class BaseUpdater(ABC): - """A base class to update calibrations.""" - - __fit_parameter__ = None - - def __init__(self): - """Updaters are not meant to be instantiated. - - Instead of instantiating updaters use them by calling the :meth:`update` class method. - For example, the :class:`.Frequency` updater is called in the following way - - .. code-block:: python - - Frequency.update(calibrations, spectroscopy_data) - - Here, calibrations is an instance of :class:`.Calibrations` and spectroscopy_data - is the result of a :class:`.QubitSpectroscopy` experiment. - """ - raise CalibrationError( - "Calibration updaters are not meant to be instantiated. The intended usage" - "is Updater.update(calibrations, exp_data, ...)." - ) - - @staticmethod - def _time_stamp(exp_data: ExperimentData) -> datetime: - """Helper method to extract the datetime.""" - all_times = exp_data.completion_times.values() - if all_times: - return max(all_times).astimezone() - return datetime.now(timezone.utc).astimezone() - - @classmethod - def add_parameter_value( - cls, - cal: Calibrations, - exp_data: ExperimentData, - value: ParameterValueType, - param: Union[Parameter, str], - schedule: Union[ScheduleBlock, str] = None, - group: str = "default", - ): - """Update the calibrations with the given value. - - Args: - cal: The Calibrations instance to update. - exp_data: The ExperimentData instance that contains the result and the experiment data. - value: The value extracted by the subclasses in the :meth:`update` method. - param: The name of the parameter, or the parameter instance, which will receive an - updated value. - schedule: The ScheduleBlock instance or the name of the instance to which the parameter - is attached. - group: The calibrations group to update. - """ - - qubits = exp_data.metadata["physical_qubits"] - - param_value = ParameterValue( - value=value, - date_time=cls._time_stamp(exp_data), - group=group, - exp_id=exp_data.experiment_id, - ) - - cal.add_parameter_value(param_value, param, qubits, schedule) - - @classmethod - def update( - cls, - calibrations: Calibrations, - exp_data: ExperimentData, - parameter: str, - schedule: Optional[Union[ScheduleBlock, str]], - result_index: Optional[int] = -1, - group: str = "default", - fit_parameter: Optional[str] = None, - ): - """Update the calibrations based on the data. - - Args: - calibrations: The calibrations to update. - exp_data: The experiment data from which to update. - parameter: The name of the parameter in the calibrations to update. - schedule: The ScheduleBlock instance or the name of the instance to which the parameter - is attached. - result_index: The result index to use which defaults to -1. - group: The calibrations group to update. Defaults to "default." - fit_parameter: The name of the fit parameter in the analysis result. This will default - to the class variable :code:`__fit_parameter__` if not given. - - Raises: - CalibrationError: If the analysis result does not contain a frequency variable. - """ - fit_parameter = fit_parameter or cls.__fit_parameter__ - value = BaseUpdater.get_value(exp_data, fit_parameter, result_index) - - cls.add_parameter_value( - calibrations, exp_data, value, parameter, schedule=schedule, group=group - ) - - @staticmethod - def get_value(exp_data: ExperimentData, param_name: str, index: Optional[int] = -1) -> float: - """A helper method to extract values from experiment data instances.""" - # Because this is called within analysis callbacks the block=False kwarg - # must be passed to analysis results so we don't block indefinitely - candidates = exp_data.analysis_results(param_name, block=False) - if isinstance(candidates, list): - return candidates[index].value.nominal_value - else: - return candidates.value.nominal_value - - -class Frequency(BaseUpdater): - """Update frequencies.""" - - __fit_parameter__ = "f01" - - # pylint: disable=arguments-differ,unused-argument - @classmethod - def update( - cls, - calibrations: Calibrations, - exp_data: ExperimentData, - result_index: Optional[int] = None, - parameter: str = "drive_freq", - group: str = "default", - fit_parameter: Optional[str] = None, - **options, - ): - """Update a qubit frequency from, e.g., QubitSpectroscopy - - The value of the amplitude must be derived from the fit so the base method cannot be used. - - Args: - calibrations: The calibrations to update. - exp_data: The experiment data from which to update. - result_index: The result index to use which defaults to -1. - parameter: The name of the parameter to update. If None is given this will default - to `drive_freq`. - group: The calibrations group to update. Defaults to "default." - options: Trailing options. - fit_parameter: The name of the fit parameter in the analysis result. This will default - to the class variable :code:`__fit_parameter__` if not given. - - """ - super().update( - calibrations=calibrations, - exp_data=exp_data, - parameter=parameter, - schedule=None, - result_index=result_index, - group=group, - fit_parameter=fit_parameter, - ) diff --git a/qiskit_experiments/curve_analysis/__init__.py b/qiskit_experiments/curve_analysis/__init__.py index f55b6ece56..03c8a57119 100644 --- a/qiskit_experiments/curve_analysis/__init__.py +++ b/qiskit_experiments/curve_analysis/__init__.py @@ -55,7 +55,6 @@ DecayAnalysis DampedOscillationAnalysis OscillationAnalysis - ResonanceAnalysis GaussianAnalysis ErrorAmplificationAnalysis @@ -126,7 +125,6 @@ DecayAnalysis, DampedOscillationAnalysis, OscillationAnalysis, - ResonanceAnalysis, GaussianAnalysis, ErrorAmplificationAnalysis, BlochTrajectoryAnalysis, diff --git a/qiskit_experiments/curve_analysis/standard_analysis/__init__.py b/qiskit_experiments/curve_analysis/standard_analysis/__init__.py index 4808f200d9..ec56c3e37b 100644 --- a/qiskit_experiments/curve_analysis/standard_analysis/__init__.py +++ b/qiskit_experiments/curve_analysis/standard_analysis/__init__.py @@ -13,7 +13,6 @@ """Standard curve analysis library.""" from .oscillation import OscillationAnalysis, DampedOscillationAnalysis -from .resonance import ResonanceAnalysis from .gaussian import GaussianAnalysis from .error_amplification_analysis import ErrorAmplificationAnalysis from .decay import DecayAnalysis diff --git a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py b/qiskit_experiments/curve_analysis/standard_analysis/resonance.py deleted file mode 100644 index 156f92d1d2..0000000000 --- a/qiskit_experiments/curve_analysis/standard_analysis/resonance.py +++ /dev/null @@ -1,170 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Resonance analysis class.""" - -from typing import List, Union, Optional - -import lmfit -import numpy as np - -from qiskit.utils.deprecation import deprecate_func - -import qiskit_experiments.curve_analysis as curve -from qiskit_experiments.framework import Options - - -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. - - """ - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments and related classses " - "involving pulse gate calibrations like this one have been deprecated." - ), - ) - def __init__( - self, - name: Optional[str] = None, - ): - super().__init__( - models=[ - lmfit.models.ExpressionModel( - expr="a * abs(kappa) / sqrt(kappa**2 + 4 * (x - freq)**2) + b", - name="lorentzian", - ) - ], - name=name, - ) - - @classmethod - def _default_options(cls) -> Options: - options = super()._default_options() - options.plotter.set_figure_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.ScatterTable, - ) -> Union[curve.FitOptions, List[curve.FitOptions]]: - """Create algorithmic initial fit guess from analysis options and curve data. - - Args: - user_opt: Fit options filled with user provided guess and bounds. - curve_data: Formatted data collection to fit. - - Returns: - List of fit options that are passed to the fitter function. - """ - 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.CurveFitResult) -> Union[str, None]: - """Algorithmic criteria for whether the fit is good or bad. - - A good fit has: - - a reduced chi-squared less than 3 and greater than zero, - - 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.ufloat_params["a"] - fit_b = fit_data.ufloat_params["b"] - fit_freq = fit_data.ufloat_params["freq"] - fit_kappa = fit_data.ufloat_params["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, - 0 < fit_data.reduced_chisq < 3, - curve.utils.is_error_not_significant(fit_kappa), - snr > 2, - ] - - if all(criteria): - return "good" - - return "bad" diff --git a/qiskit_experiments/exceptions.py b/qiskit_experiments/exceptions.py index 4e05feac18..02a200997e 100644 --- a/qiskit_experiments/exceptions.py +++ b/qiskit_experiments/exceptions.py @@ -17,7 +17,3 @@ class AnalysisError(QiskitError): """Class for errors raised by experiment analysis.""" - - -class CalibrationError(QiskitError): - """Errors raised by the calibration module.""" diff --git a/qiskit_experiments/framework/backend_data.py b/qiskit_experiments/framework/backend_data.py index 5558676d06..fab0ac9775 100644 --- a/qiskit_experiments/framework/backend_data.py +++ b/qiskit_experiments/framework/backend_data.py @@ -18,7 +18,6 @@ class unifies data access for various data fields. import warnings from qiskit.providers.models import PulseBackendConfiguration # pylint: disable=no-name-in-module from qiskit.providers import BackendV1, BackendV2 -from qiskit.utils.deprecation import deprecate_func class BackendData: @@ -54,94 +53,6 @@ def name(self): return self._backend.name return str(self._backend) - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def control_channel(self, qubits): - """Returns the backend control channel for the given qubits""" - try: - if self._v1: - return self._backend.configuration().control(qubits) - elif self._v2: - try: - return self._backend.control_channel(qubits) - except NotImplementedError: - return self._pulse_conf.control(qubits) - except AttributeError: - return [] - return [] - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def drive_channel(self, qubit): - """Returns the backend drive channel for the given qubit""" - try: - if self._v1: - return self._backend.configuration().drive(qubit) - elif self._v2: - try: - return self._backend.drive_channel(qubit) - except NotImplementedError: - return self._pulse_conf.drive(qubit) - except AttributeError: - return None - return None - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def measure_channel(self, qubit): - """Returns the backend measure channel for the given qubit""" - try: - if self._v1: - return self._backend.configuration().measure(qubit) - elif self._v2: - try: - return self._backend.measure_channel(qubit) - except NotImplementedError: - return self._pulse_conf.measure(qubit) - except AttributeError: - return None - return None - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def acquire_channel(self, qubit): - """Returns the backend acquire channel for the given qubit""" - try: - if self._v1: - return self._backend.configuration().acquire(qubit) - elif self._v2: - try: - return self._backend.acquire_channel(qubit) - except NotImplementedError: - return self._pulse_conf.acquire(qubit) - except AttributeError: - return None - return None - @property def granularity(self): """Returns the backend's time constraint granularity""" @@ -154,69 +65,6 @@ def granularity(self): return 1 return 1 - @property - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - is_property=True, - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def min_length(self): - """Returns the backend's time constraint minimum duration""" - try: - if self._v1: - return self._backend.configuration().timing_constraints.get("min_length", 0) - elif self._v2: - return self._backend.target.min_length - except AttributeError: - return 0 - return 0 - - @property - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - is_property=True, - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def pulse_alignment(self): - """Returns the backend's time constraint pulse alignment""" - try: - if self._v1: - return self._backend.configuration().timing_constraints.get("pulse_alignment", 1) - elif self._v2: - return self._backend.target.pulse_alignment - except AttributeError: - return 1 - return 1 - - @property - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - is_property=True, - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def acquire_alignment(self): - """Returns the backend's time constraint acquire alignment""" - try: - if self._v1: - return self._backend.configuration().timing_constraints.get("acquire_alignment", 1) - elif self._v2: - return self._backend.target.acquire_alignment - except AttributeError: - return 1 - return 1 - @property def dt(self): """Returns the backend's input time resolution""" @@ -271,49 +119,6 @@ def provider(self): return None return None - @property - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - is_property=True, - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def drive_freqs(self): - """Returns the backend's qubit drive frequencies""" - if self._v1: - return getattr(self._backend.defaults(), "qubit_freq_est", []) - elif self._v2: - if self._backend.target.qubit_properties is None: - return [] - return [property.frequency for property in self._backend.target.qubit_properties] - return [] - - @property - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - is_property=True, - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def meas_freqs(self): - """Returns the backend's measurement stimulus frequencies. - - .. note:: - - The qiskit base classes do not provide this information as a - standard backend property, but it is available from some providers - in the data returned by the ``Backend.defaults()`` method. - """ - if not hasattr(self._backend, "defaults"): - return [] - return getattr(self._backend.defaults(), "meas_freq_est", []) - @property def num_qubits(self): """Returns the backend's number of qubits""" diff --git a/qiskit_experiments/framework/backend_timing.py b/qiskit_experiments/framework/backend_timing.py index 0eea6fce67..8a10a237dc 100644 --- a/qiskit_experiments/framework/backend_timing.py +++ b/qiskit_experiments/framework/backend_timing.py @@ -11,51 +11,33 @@ # that they have been altered from the originals. """Backend timing helper functions""" -from math import gcd +import warnings from typing import Optional, Union from qiskit import QiskitError from qiskit.providers.backend import Backend -from qiskit.utils.deprecation import deprecate_func from qiskit_experiments.framework import BackendData -def lcm(int1: int, int2: int) -> int: - """Least common multiple - - ``math.lcm`` was added in Python 3.9. This function should be replaced with - ``from math import lcm`` after dropping support for Python 3.8. - - .. note:: - - ``math.lcm`` supports an arbitrary number of arguments, but this - version supports exactly two. - """ - return int1 * int2 // gcd(int1, int2) - - class BackendTiming: """Helper for calculating pulse and delay times for an experiment The methods and properties provided by this class help with calculating - delay and pulse timing that depends on the timing constraints of the - backend. + delay timing that depends on the timing constraints of the backend. When designing qubit characterization experiments, it is often necessary to - deal with precise timing of pulses and delays. The fact that physical - backends (i.e. not simulators) only support sampling time at intervals of - ``dt`` complicates this process as times must be rounded. Besides the - sampling time, there can be additional constraints like a minimum pulse - length or a pulse granularity, which specifies the allowed increments of a - pulse length in samples (i.e., for a granularity of 16, pulse lengths of 64 + deal with precise timing of delays. The fact that physical backends (i.e. + not simulators) only support sampling time at intervals of ``dt`` + complicates this process as times must be rounded. Besides the + sampling time, there can be additional constraints like a + granularity, which specifies the allowed increments of a + delay length in samples (i.e., for a granularity of 16, delay lengths of 64 and 80 samples are valid but not any number in between). Here are some specific problems that can occur when dealing with timing constraints for pulses and delays: - - An invalid pulse length or pulse start time could result in an error from - the backend. - An invalid delay length could be rounded by the backend, and this rounding could lead to error in analysis that assumes the unrounded value. @@ -63,51 +45,6 @@ class BackendTiming: scheduling pass of a circuit during transpilation, which is a computationally expensive process. Scheduling the circuit with valid timing to start out can avoid this rescheduling. - - While there are separate alignment requirements for drive - (``pulse_alignment``) and for measurement (``acquire_alignment``) - channels, the nature of pulse and circuit instruction alignment can - couple the timing of different instructions, resulting in improperly - aligned instructions. For example, consider this circuit: - - .. code-block:: python - - from qiskit import QuantumCircuit - qc = QuantumCircuit(1, 1) - qc.x(0) - qc.delay(delay, 0) - qc.x(0) - qc.delay(delay2, 0) - qc.measure(0, 0) - - Because the circuit instructions are all pushed together sequentially in - time without extra delays, whether or not the ``measure`` instruction - occurs at a valid time depends on the details of the circuit. In - particular, since the ``x`` gates typically have durations that are - multiples of ``acquire_alignment`` (because ``granularity`` usually is), - the ``measure`` start will occur at a time consistent with - ``acquire_alignment`` when ``delay + delay2`` is a multiple of - ``acquire_alignment``. Note that in the case of IBM Quantum backends, - when ``acquire_alignment`` is not satisfied, there is no error reported - by Qiskit or by the backend. Instead the measurement pulse is misaligned - relative to the start of the signal acquisition, resulting in an - incorrect phase and often an incorrect state discrimination. - - To help avoid these problems, :class:`.BackendTiming` provides methods for - calculating pulse and delay durations. These methods work with samples and - seconds as appropriate. If these methods are used for all durations in a - circuit, the alignment constraints should always be satisfied. - - .. note:: - - For delay duration, the least common multiple of ``pulse_alignment`` - and ``acquire_alignment`` is used as the granularity. Thus, in the - example above about the coupling between ``pulse_alignment`` and - ``acquire_alignment`` , ``delay`` and ``delay2`` would each be rounded - to a multiple of ``acquire_alignment`` and so the sum would always be a - multiple of each alignment value as well. This approach modifies some - valid circuits (like each delay being half of ``acquire_alignment``) - but has the benefit of always being valid without detailed analysis of - the full circuit. As an example use-case for :class:`.BackendTiming`, consider a T1 experiment where delay times are specified in seconds in a @@ -139,46 +76,6 @@ def circuits(self): } circuits.append(circ) - - As another example, consider a time Rabi experiment where the width of a - pulse in a schedule is stretched: - - .. code-block:: python - - from qiskit import pulse - from qiskit.circuit import Gate, Parameter - - - def circuits(self): - chan = pulse.DriveChannel(0) - dur = Parameter("duration") - - with pulse.build() as sched: - pulse.play(pulse.Gaussian(duration=dur, amp=1, sigma=dur / 4), chan) - - gate = Gate("Rabi", num_qubits=1, params=[dur]) - - template_circ = QuantumCircuit(1, 1) - template_circ.append(gate, [0]) - template_circ.measure(0, 0) - template_circ.add_calibration(gate, (0,), sched) - - # Pass backend to BackendTiming - timing = BackendTiming(self.backend) - - circs = [] - # durations is a list of pulse durations in seconds - for duration in self.experiment_options.durations: - # Calculate valid sample number closest to this duration - circ = template_circ.assign_parameters( - {dur: timing.round_pulse(time=duration)}, - inplace=False, - ) - # Track corresponding duration for the pulse in seconds - circ.metadata = { - "xval": timing.pulse_time(time=duration), - "unit": "s", - } """ def __init__( @@ -202,26 +99,26 @@ def __init__( Args: backend: the backend to provide timing help for. - acquire_alignment: Optional. Constraint for the acquisition instruction alignment - in units of dt. Default to the backend value. + acquire_alignment: Optional. Deprecated and unused. granularity: Optional. Constraint for the pulse samples granularity in units of dt. Defaults to the backend value. - min_length: Optional. Constraint for the minimum pulse samples - in units of dt. Defaults to the backend value. - pulse_alignment: Optional. Constraint for the pulse play instruction alignment - in units of dt. Default to the backend value. + min_length: Optional. Deprecated and unused. + pulse_alignment: Optional. Deprecated and unused. dt: Optional. Time interval of pulse samples. Default to the backend value. """ backend_data = BackendData(backend) # Pull all the timing data from the backend - self._acquire_alignment = acquire_alignment or backend_data.acquire_alignment self._granularity = granularity or backend_data.granularity - self._min_length = min_length or backend_data.min_length - self._pulse_alignment = pulse_alignment or backend_data.pulse_alignment #: The backend's ``dt`` value, copied to :class:`.BackendTiming` for convenience self.dt = dt or backend_data.dt + if min_length is not None or acquire_alignment is not None or pulse_alignment is not None: + warnings.warn( + "Arguments acquire_alignment, min_length, and pulse_alignment " + "are no longer used by BackendTiming." + ) + @property def delay_unit(self) -> str: """The delay unit for the current backend @@ -278,75 +175,10 @@ def round_delay( if samples is None: samples = time / self.dt - granularity = lcm(self._pulse_alignment, self._acquire_alignment) - - samples_out = int(round(samples / granularity) * granularity) + samples_out = int(round(samples / self._granularity) * self._granularity) return samples_out - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def round_pulse( - self, *, time: Optional[float] = None, samples: Optional[Union[int, float]] = None - ) -> int: - """The number of samples giving the valid pulse duration closest to the input - - The multiple of the pulse granularity giving the time closest to the - input (either ``time`` or ``samples``) is used. The returned value is - always at least the backend's ``min_length``. - - Args: - time: Nominal pulse duration in seconds - samples: Nominal pulse duration in samples - - Returns: - The number of samples corresponding to the input - - Raises: - QiskitError: If either both ``time`` and ``samples`` are passed or - neither is passed. - QiskitError: The backend does not include a dt value. - QiskitError: If the algorithm used to calculate the pulse length - produces a length that is not commensurate with the pulse or - acquire alignment values. This should not happen unless the - alignment constraints provided by the backend do not fit the - assumptions that the algorithm makes. - """ - if time is None and samples is None: - raise QiskitError("Either time or samples must be a numerical value.") - if time is not None and samples is not None: - raise QiskitError("Only one of time and samples can be a numerical value.") - - if self.dt is None: - raise QiskitError("Backend has no dt value.") - - if samples is None: - samples = time / self.dt - - samples = int(round(samples / self._granularity)) * self._granularity - samples = max(samples, self._min_length) - - pulse_alignment = self._pulse_alignment - acquire_alignment = self._acquire_alignment - - if samples % pulse_alignment != 0: - raise QiskitError( - "Pulse duration calculation does not match pulse alignment constraints!" - ) - - if samples % acquire_alignment != 0: - raise QiskitError( - "Pulse duration calculation does not match acquire alignment constraints!" - ) - - return samples - def delay_time( self, *, time: Optional[float] = None, samples: Optional[Union[int, float]] = None ) -> float: @@ -371,38 +203,3 @@ def delay_time( return time return self.dt * self.round_delay(time=time, samples=samples) - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, utility functions involving " - "pulse like this one have been deprecated." - ), - ) - def pulse_time( - self, *, time: Optional[float] = None, samples: Optional[Union[int, float]] = None - ) -> float: - """The closest valid pulse duration to the input in seconds - - This method uses :meth:`.BackendTiming.round_pulse` and then - converts back into seconds. - - Args: - time: Nominal pulse duration in seconds - samples: Nominal pulse duration in samples - - Returns: - The realizable pulse time in seconds - - Raises: - QiskitError: If either both ``time`` and ``samples`` are passed or - neither is passed. - QiskitError: The backend does not include a dt value. - QiskitError: If the algorithm used to calculate the pulse length - produces a length that is not commensurate with the pulse or - acquire alignment values. This should not happen unless the - alignment constraints provided by the backend do not fit the - assumptions that the algorithm makes. - """ - return self.dt * self.round_pulse(time=time, samples=samples) diff --git a/qiskit_experiments/framework/composite/parallel_experiment.py b/qiskit_experiments/framework/composite/parallel_experiment.py index b3bbaa9877..54f6f7978c 100644 --- a/qiskit_experiments/framework/composite/parallel_experiment.py +++ b/qiskit_experiments/framework/composite/parallel_experiment.py @@ -182,9 +182,4 @@ def _combined_circuits(self, device_layout: bool) -> List[QuantumCircuit]: circuit.metadata["composite_qubits"].append(qubits) circuit.metadata["composite_clbits"].append(clbits) - # Add the calibrations - for gate, cals in sub_circ.calibrations.items(): - for key, sched in cals.items(): - circuit.add_calibration(gate, qubits=key[0], schedule=sched, params=key[1]) - return joint_circuits diff --git a/qiskit_experiments/framework/json.py b/qiskit_experiments/framework/json.py index 5aec9641de..c8ec264687 100644 --- a/qiskit_experiments/framework/json.py +++ b/qiskit_experiments/framework/json.py @@ -34,7 +34,6 @@ import uncertainties from qiskit import qpy from qiskit.circuit import ParameterExpression, QuantumCircuit, Instruction -from qiskit.pulse import ScheduleBlock from qiskit_experiments.version import __version__ @@ -508,11 +507,6 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-renamed data=obj, serializer=lambda buff, data: qpy.dump(data, buff) ) return {"__type__": "QuantumCircuit", "__value__": value} - if isinstance(obj, ScheduleBlock): - value = _serialize_and_encode( - data=obj, serializer=lambda buff, data: qpy.dump(data, buff) - ) - return {"__type__": "ScheduleBlock", "__value__": value} if isinstance(obj, ParameterExpression): value = _serialize_and_encode( data=obj, @@ -569,8 +563,6 @@ def object_hook(self, obj): return circuit.data[0].operation if obj_type == "QuantumCircuit": return _decode_and_deserialize(obj_val, qpy.load, name=obj_type)[0] - if obj_type == "ScheduleBlock": - return _decode_and_deserialize(obj_val, qpy.load, name=obj_type)[0] if obj_type == "ParameterExpression": return _decode_and_deserialize( obj_val, qpy._read_parameter_expression, name=obj_type diff --git a/qiskit_experiments/library/__init__.py b/qiskit_experiments/library/__init__.py index 2e47b281e8..524009608c 100644 --- a/qiskit_experiments/library/__init__.py +++ b/qiskit_experiments/library/__init__.py @@ -50,7 +50,6 @@ ========================================== Experiments for characterization of properties of individual qubits. -Some experiments also have a calibration experiment version. .. autosummary:: :toctree: ../stubs/ @@ -60,26 +59,17 @@ ~characterization.T2Hahn ~characterization.T2Ramsey ~characterization.Tphi - ~characterization.QubitSpectroscopy - ~characterization.EFSpectroscopy ~characterization.HalfAngle ~characterization.FineAmplitude ~characterization.FineXAmplitude ~characterization.FineSXAmplitude - ~characterization.Rabi - ~characterization.EFRabi ~characterization.RamseyXY ~characterization.FineFrequency ~characterization.ReadoutAngle - ~characterization.ResonatorSpectroscopy - ~characterization.RoughDrag ~characterization.FineDrag ~characterization.FineXDrag ~characterization.FineSXDrag ~characterization.MultiStateDiscrimination - ~driven_freq_tuning.StarkRamseyXY - ~driven_freq_tuning.StarkRamseyXYAmpScan - ~driven_freq_tuning.StarkP1Spectroscopy .. _characterization two qubits: @@ -92,8 +82,6 @@ :toctree: ../stubs/ :template: autosummary/experiment.rst - ~characterization.CrossResonanceHamiltonian - ~characterization.EchoedCrossResonanceHamiltonian ~characterization.ZZRamsey ~characterization.FineZXAmplitude @@ -111,70 +99,15 @@ ~characterization.LocalReadoutError ~characterization.CorrelatedReadoutError -.. _calibration: - -Calibration Experiments -======================= - -Experiments for pulse level calibration of quantum gates. These experiments -are usually run with a -:class:`~qiskit_experiments.calibration_management.Calibrations` -class instance to manage parameters and pulse schedules. -See :doc:`/tutorials/calibrations` for examples. - -.. autosummary:: - :toctree: ../stubs/ - :template: autosummary/experiment.rst - - ~calibration.RoughFrequencyCal - ~calibration.RoughEFFrequencyCal - ~calibration.FrequencyCal - ~calibration.FineFrequencyCal - ~calibration.RoughDragCal - ~calibration.FineXDragCal - ~calibration.FineSXDragCal - ~calibration.FineDragCal - ~calibration.FineAmplitudeCal - ~calibration.FineXAmplitudeCal - ~calibration.FineSXAmplitudeCal - ~calibration.HalfAngleCal - ~calibration.RoughAmplitudeCal - ~calibration.RoughXSXAmplitudeCal - ~calibration.EFRoughXSXAmplitudeCal - """ -from .calibration import ( - RoughDragCal, - FineDragCal, - FineXDragCal, - FineSXDragCal, - RoughAmplitudeCal, - RoughXSXAmplitudeCal, - EFRoughXSXAmplitudeCal, - FineAmplitudeCal, - FineXAmplitudeCal, - FineSXAmplitudeCal, - RoughFrequencyCal, - RoughEFFrequencyCal, - FrequencyCal, - FineFrequencyCal, - HalfAngleCal, -) from .characterization import ( T1, T2Hahn, T2Ramsey, Tphi, - QubitSpectroscopy, - EFSpectroscopy, - CrossResonanceHamiltonian, - EchoedCrossResonanceHamiltonian, - RoughDrag, FineDrag, FineXDrag, FineSXDrag, - Rabi, - EFRabi, HalfAngle, FineAmplitude, FineXAmplitude, @@ -183,7 +116,6 @@ class instance to manage parameters and pulse schedules. RamseyXY, FineFrequency, ReadoutAngle, - ResonatorSpectroscopy, LocalReadoutError, CorrelatedReadoutError, ZZRamsey, @@ -198,14 +130,8 @@ class instance to manage parameters and pulse schedules. MitigatedProcessTomography, ) from .quantum_volume import QuantumVolume -from .driven_freq_tuning import ( - StarkRamseyXY, - StarkRamseyXYAmpScan, - StarkP1Spectroscopy, -) # Experiment Sub-modules -from . import calibration from . import characterization from . import randomized_benchmarking from . import tomography diff --git a/qiskit_experiments/library/calibration/__init__.py b/qiskit_experiments/library/calibration/__init__.py deleted file mode 100644 index 8b24f93075..0000000000 --- a/qiskit_experiments/library/calibration/__init__.py +++ /dev/null @@ -1,65 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -""" -======================================================================= -Calibration Experiments (:mod:`qiskit_experiments.library.calibration`) -======================================================================= - -.. currentmodule:: qiskit_experiments.library.calibration - -Calibrating qubit setups is the task of finding the pulse shapes and parameter -values that maximizes the fidelity of the resulting quantum operations. This -therefore requires experiments which are analyzed to extract parameter values. -Furthermore, the resulting parameter values and schedules must be managed. The -calibration module in Qiskit experiments allows users to run calibration -experiments and manage the resulting schedules and parameter values. - -The following experiments are designed to calibrate parameter values. Some experiments such -as :class:`.QubitSpectroscopy` can both be seen as characterization and calibrations -experiments. Such experiments can be found in the -:mod:`~qiskit_experiments.library.characterization` -module. - -.. autosummary:: - :toctree: ../stubs/ - :template: autosummary/experiment.rst - - RoughFrequencyCal - FrequencyCal - FineFrequencyCal - RoughDragCal - FineDragCal - FineXDragCal - FineSXDragCal - FineAmplitudeCal - FineXAmplitudeCal - FineSXAmplitudeCal - HalfAngleCal - RoughAmplitudeCal - RoughXSXAmplitudeCal - EFRoughXSXAmplitudeCal - -Calibrations management -======================= - -See :mod:`.calibration_management`. -""" - -from .rough_frequency import RoughFrequencyCal, RoughEFFrequencyCal -from .rough_drag_cal import RoughDragCal -from .rough_amplitude_cal import RoughAmplitudeCal, RoughXSXAmplitudeCal, EFRoughXSXAmplitudeCal -from .fine_amplitude import FineAmplitudeCal, FineXAmplitudeCal, FineSXAmplitudeCal -from .fine_drag_cal import FineDragCal, FineXDragCal, FineSXDragCal -from .frequency_cal import FrequencyCal -from .fine_frequency_cal import FineFrequencyCal -from .half_angle_cal import HalfAngleCal diff --git a/qiskit_experiments/library/calibration/fine_amplitude.py b/qiskit_experiments/library/calibration/fine_amplitude.py deleted file mode 100644 index 3055d6fedb..0000000000 --- a/qiskit_experiments/library/calibration/fine_amplitude.py +++ /dev/null @@ -1,360 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Fine amplitude calibration experiment.""" - -from typing import Dict, Optional, Sequence -import numpy as np - -from qiskit.circuit import Gate, QuantumCircuit -from qiskit.providers.backend import Backend - -from qiskit_experiments.calibration_management import ( - BaseCalibrationExperiment, - Calibrations, -) -from qiskit_experiments.library.characterization import FineAmplitude -from qiskit_experiments.framework import ExperimentData, Options -from qiskit_experiments.calibration_management.update_library import BaseUpdater - - -class FineAmplitudeCal(BaseCalibrationExperiment, FineAmplitude): - r"""A calibration version of the :class:`.FineAmplitude` experiment. - - # section: overview - - :class:`FineAmplitudeCal` is a subclass of :class:`.FineAmplitude`. In the calibration - experiment the circuits that are run have a custom gate with the pulse schedule attached - to it through the calibrations. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=101) - - .. jupyter-execute:: - - import numpy as np - from qiskit.circuit.library import SXGate - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library import FineAmplitudeCal - - library = FixedFrequencyTransmon(default_values={"duration": 320, "amp": 0.030}) - cals = Calibrations.from_backend(backend=backend, libraries=[library]) - exp_cal = FineAmplitudeCal(physical_qubits=(0,), - calibrations=cals, - schedule_name="sx", - backend=backend, - cal_parameter_name="amp", - auto_update=True, - gate=SXGate(), - measurement_qubits=(0,)) - # This option is necessary! - exp_cal.analysis.set_options(fixed_parameters={"angle_per_gate" : np.pi / 2, - "phase_offset" : np.pi}) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - schedule_name: str, - backend: Optional[Backend] = None, - cal_parameter_name: Optional[str] = "amp", - auto_update: bool = True, - gate: Optional[Gate] = None, - measurement_qubits: Sequence[int] = None, - ): - """See class :class:`FineAmplitude` for details. - - Args: - physical_qubits: Sequence containing the qubit(s) for which to run - the fine amplitude calibration. This can be a pair of qubits - which correspond to control and target qubit. - calibrations: The calibrations instance with the schedules. - schedule_name: The name of the schedule to calibrate. - backend: Optional, the backend to run the experiment on. - cal_parameter_name: The name of the parameter in the schedule to update. - auto_update: Whether or not to automatically update the calibrations. By - default this variable is set to True. - gate: The gate to repeat in the quantum circuit. If this argument - is None (the default), then the gate is built from the schedule name. - measurement_qubits: The qubits in the given physical qubits that need to - be measured. - """ - gate = gate or Gate(name=schedule_name, num_qubits=len(physical_qubits), params=[]) - - super().__init__( - calibrations, - physical_qubits, - gate, - schedule_name=schedule_name, - backend=backend, - measurement_qubits=measurement_qubits, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - - @classmethod - def _default_experiment_options(cls): - """Default values for the fine amplitude calibration experiment. - - Experiment Options: - target_angle (float): The target angle of the pulse. - """ - options = super()._default_experiment_options() - options.target_angle = np.pi - return options - - def _metadata(self) -> Dict[str, any]: - """Add metadata to the experiment data making it more self contained. - - The following keys are added to each experiment's metadata: - cal_param_value: The value of the pulse amplitude. This value together with - the fit result will be used to find the new value of the pulse amplitude. - cal_param_name: The name of the parameter in the calibrations. - cal_schedule: The name of the schedule in the calibrations. - target_angle: The target angle of the gate. - cal_group: The calibration group to which the parameter belongs. - """ - metadata = super()._metadata() - metadata["target_angle"] = self.experiment_options.target_angle - metadata["cal_param_value"] = self._cals.get_parameter_value( - self._param_name, - self.physical_qubits, - self._sched_name, - group=self.experiment_options.group, - ) - - return metadata - - def _attach_calibrations(self, circuit: QuantumCircuit): - """Adds the calibrations to the transpiled circuits.""" - for gate in ["x", "sx"]: - schedule = self._cals.get_schedule(gate, self.physical_qubits) - circuit.add_calibration(gate, self.physical_qubits, schedule) - - def update_calibrations(self, experiment_data: ExperimentData): - r"""Update the amplitude of the pulse in the calibrations. - - The update rule of this experiment is - - .. math:: - - A \to A \frac{\theta_\text{target}}{\theta_\text{target} + {\rm d}\theta} - - Where :math:`A` is the amplitude of the pulse before the update. - - Args: - experiment_data: The experiment data from which to extract the measured over/under - rotation used to adjust the amplitude. - """ - - result_index = self.experiment_options.result_index - group = experiment_data.metadata["cal_group"] - target_angle = experiment_data.metadata["target_angle"] - prev_amp = experiment_data.metadata["cal_param_value"] - - # Protect against cases where the complex amplitude was converted to a list. - if isinstance(prev_amp, list) and len(prev_amp) == 2: - prev_amp = prev_amp[0] + 1.0j * prev_amp[1] - - d_theta = BaseUpdater.get_value(experiment_data, "d_theta", result_index) - - BaseUpdater.add_parameter_value( - self._cals, - experiment_data, - prev_amp * target_angle / (target_angle + d_theta), - self._param_name, - self._sched_name, - group, - ) - - -class FineXAmplitudeCal(FineAmplitudeCal): - """A calibration experiment to calibrate the amplitude of the X schedule. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=111) - - .. jupyter-execute:: - - import numpy as np - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library import FineXAmplitudeCal - - library = FixedFrequencyTransmon(default_values={"duration": 320, "amp": 0.030}) - cals = Calibrations.from_backend(backend, libraries=[library]) - - exp_cal = FineXAmplitudeCal((0,), - cals, - schedule_name="x", - backend=backend, - cal_parameter_name="amp", - auto_update=True, - ) - - exp_data = exp_cal.run().block_for_results() - display(exp_data.figure(0)) - exp_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - schedule_name: str, - backend: Optional[Backend] = None, - cal_parameter_name: Optional[str] = "amp", - auto_update: bool = True, - ): - super().__init__( - physical_qubits, - calibrations, - schedule_name, - backend=backend, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - self.analysis.set_options( - fixed_parameters={ - "angle_per_gate": np.pi, - "phase_offset": np.pi / 2, - } - ) - - @classmethod - def _default_transpile_options(cls): - """Default transpile options. - - Transpile Options: - basis_gates (list(str)): A list of basis gates needed for this experiment. - The schedules for these basis gates will be provided by the instruction - schedule map from the calibrations. - """ - options = super()._default_transpile_options() - options.basis_gates = ["x", "sx"] - - return options - - def _pre_circuit(self, num_clbits: int) -> QuantumCircuit: - """The preparation circuit is an sx gate to move to the equator of the Bloch sphere.""" - circuit = QuantumCircuit(self.num_qubits, num_clbits) - circuit.sx(0) - return circuit - - -class FineSXAmplitudeCal(FineAmplitudeCal): - """A calibration experiment to calibrate the amplitude of the SX schedule. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=105) - - .. jupyter-execute:: - - import numpy as np - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library import FineSXAmplitudeCal - - library = FixedFrequencyTransmon(default_values={"duration": 320, "amp": 0.015}) - cals = Calibrations.from_backend(backend, libraries=[library]) - - exp_cal = FineSXAmplitudeCal((0,), - cals, - schedule_name="sx", - backend=backend, - cal_parameter_name="amp", - auto_update=True, - ) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - schedule_name: str, - backend: Optional[Backend] = None, - cal_parameter_name: Optional[str] = "amp", - auto_update: bool = True, - ): - super().__init__( - physical_qubits, - calibrations, - schedule_name, - backend=backend, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - self.analysis.set_options( - fixed_parameters={ - "angle_per_gate": np.pi / 2, - "phase_offset": np.pi, - } - ) - - @classmethod - def _default_experiment_options(cls) -> Options: - r"""Default values for the fine amplitude experiment. - - Experiment Options: - add_sx (bool): This option is False by default when calibrating gates with a target - angle per gate of :math:`\pi/2` as this increases the sensitivity of the - experiment. - add_xp_circuit (bool): This option is False by default when calibrating gates with - a target angle per gate of :math:`\pi/2`. - repetitions (List[int]): By default the repetitions take on odd numbers for - :math:`\pi/2` target angles as this ideally prepares states on the equator of - the Bloch sphere. Note that the repetitions include two repetitions which - plays the same role as including a circuit with an X gate. - target_angle (float): The target angle per gate. - """ - options = super()._default_experiment_options() - options.add_cal_circuits = False - options.repetitions = [0, 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 21, 23, 25] - options.target_angle = np.pi / 2 - return options diff --git a/qiskit_experiments/library/calibration/fine_drag_cal.py b/qiskit_experiments/library/calibration/fine_drag_cal.py deleted file mode 100644 index 883a69fd66..0000000000 --- a/qiskit_experiments/library/calibration/fine_drag_cal.py +++ /dev/null @@ -1,316 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Fine drag calibration experiment.""" - -from typing import Dict, Optional, Sequence -import numpy as np - -from qiskit.circuit import Gate, QuantumCircuit -from qiskit.providers.backend import Backend -from qiskit.pulse import Play - -from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.framework import ExperimentData, Options -from qiskit_experiments.calibration_management import ( - BaseCalibrationExperiment, - Calibrations, -) -from qiskit_experiments.calibration_management.update_library import BaseUpdater -from qiskit_experiments.library.characterization.fine_drag import FineDrag - - -class FineDragCal(BaseCalibrationExperiment, FineDrag): - """A calibration version of the fine DRAG experiment. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - #backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=108) - - .. jupyter-execute:: - - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library import FineDragCal - - library = FixedFrequencyTransmon(default_values={"duration": 320, "amp": 0.030, "beta": 0.0}) - cals = Calibrations.from_backend(backend, libraries=[library]) - - exp_cal = FineDragCal((0,), - calibrations=cals, - backend=backend, - schedule_name="sx", - cal_parameter_name="β", - auto_update=True, - ) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - schedule_name: str, - backend: Optional[Backend] = None, - cal_parameter_name: Optional[str] = "β", - auto_update: bool = True, - ): - r"""See class :class:`.FineDrag` for details. - - Note that this class implicitly assumes that the target angle of the gate - is :math:`\pi` as seen from the default experiment options. - - Args: - physical_qubits: Sequence containing the qubit for which to run the - fine drag calibration. - calibrations: The calibrations instance with the schedules. - schedule_name: The name of the schedule to calibrate. - backend: Optional, the backend to run the experiment on. - cal_parameter_name: The name of the parameter in the schedule to update. - auto_update: Whether or not to automatically update the calibrations. By - default this variable is set to True. - """ - super().__init__( - calibrations, - physical_qubits, - Gate(name=schedule_name, num_qubits=1, params=[]), - schedule_name=schedule_name, - backend=backend, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default experiment options. - - Experiment Options: - target_angle (float): The target rotation angle of the gate being calibrated. - This value is needed for the update rule. - """ - options = super()._default_experiment_options() - options.update_options(target_angle=np.pi) - return options - - def _metadata(self) -> Dict[str, any]: - """Add metadata to the experiment data making it more self contained. - - The following keys are added to each experiment's metadata: - cal_param_value: The value of the drag parameter. This value together with - the fit result will be used to find the new value of the drag parameter. - cal_param_name: The name of the parameter in the calibrations. - cal_schedule: The name of the schedule in the calibrations. - target_angle: The target angle of the gate. - cal_group: The calibration group to which the parameter belongs. - """ - metadata = super()._metadata() - metadata["target_angle"] = self.experiment_options.target_angle - metadata["cal_param_value"] = self._cals.get_parameter_value( - self._param_name, - self.physical_qubits, - self._sched_name, - group=self.experiment_options.group, - ) - - return metadata - - def _attach_calibrations(self, circuit: QuantumCircuit): - """Attach the calibrations to the circuit.""" - schedule = self._cals.get_schedule(self._sched_name, self.physical_qubits) - circuit.add_calibration(self._sched_name, self.physical_qubits, schedule) - # FineDrag always uses sx so attach it if it is not sched_name - if self._sched_name != "sx": - schedule = self._cals.get_schedule("sx", self.physical_qubits) - circuit.add_calibration("sx", self.physical_qubits, schedule) - - def update_calibrations(self, experiment_data: ExperimentData): - """Update the drag parameter of the pulse in the calibrations.""" - - result_index = self.experiment_options.result_index - group = experiment_data.metadata["cal_group"] - target_angle = experiment_data.metadata["target_angle"] - qubits = experiment_data.metadata["physical_qubits"] - - schedule = self._cals.get_schedule(self._sched_name, qubits) - - # Obtain sigma as it is needed for the fine DRAG update rule. - sigmas = [] - for block in schedule.blocks: - if isinstance(block, Play) and hasattr(block.pulse, "sigma"): - sigmas.append(getattr(block.pulse, "sigma")) - - if len(set(sigmas)) != 1: - raise CalibrationError( - "Cannot run fine Drag calibration on a schedule with multiple values of sigma." - ) - - if len(sigmas) == 0: - raise CalibrationError(f"Could not infer sigma from {schedule}.") - - d_theta = BaseUpdater.get_value(experiment_data, "d_theta", result_index) - - # See the documentation in fine_drag.py for the derivation of this rule. - d_beta = -np.sqrt(np.pi) * d_theta * sigmas[0] / target_angle**2 - old_beta = experiment_data.metadata["cal_param_value"] - new_beta = old_beta + d_beta - - BaseUpdater.add_parameter_value( - self._cals, experiment_data, new_beta, self._param_name, schedule, group - ) - - -class FineXDragCal(FineDragCal): - """Fine DRAG calibration of X gate. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - #backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=118) - - .. jupyter-execute:: - - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library import FineXDragCal - - library = FixedFrequencyTransmon(default_values={"duration": 320, "amp": 0.030, "beta": 0.0}) - cals = Calibrations.from_backend(backend, libraries=[library]) - - exp_cal = FineXDragCal((0,), - calibrations=cals, - backend=backend, - cal_parameter_name="β", - auto_update=True, - ) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - backend: Optional[Backend] = None, - cal_parameter_name: Optional[str] = "β", - auto_update: bool = True, - ): - r"""see class :class:`.FineDrag` for details. - - Args: - physical_qubits: Sequence containing the qubit for which to run the - fine drag calibration. - calibrations: The calibrations instance with the schedules. - backend: Optional, the backend to run the experiment on. - cal_parameter_name: The name of the parameter in the schedule to update. - auto_update: Whether or not to automatically update the calibrations. By - default this variable is set to True. - """ - super().__init__( - physical_qubits, - calibrations, - schedule_name="x", - backend=backend, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - - -class FineSXDragCal(FineDragCal): - """Fine DRAG calibration of X gate. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - #backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=118) - - .. jupyter-execute:: - - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library import FineSXDragCal - - library = FixedFrequencyTransmon(default_values={"duration": 320, "amp": 0.030, "beta": 0.0}) - cals = Calibrations.from_backend(backend=backend, libraries=[library]) - - exp_cal = FineSXDragCal((0,), - calibrations=cals, - backend=backend, - cal_parameter_name="β", - auto_update=True, - ) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - backend: Optional[Backend] = None, - cal_parameter_name: Optional[str] = "β", - auto_update: bool = True, - ): - r"""see class :class:`.FineDrag` for details. - - Args: - physical_qubits: Sequence containing the qubit for which to run the - fine drag calibration. - calibrations: The calibrations instance with the schedules. - backend: Optional, the backend to run the experiment on. - cal_parameter_name: The name of the parameter in the schedule to update. - auto_update: Whether or not to automatically update the calibrations. By - default this variable is set to True. - """ - super().__init__( - physical_qubits, - calibrations, - schedule_name="sx", - backend=backend, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default experiment options.""" - options = super()._default_experiment_options() - options.target_angle = np.pi / 2 - return options diff --git a/qiskit_experiments/library/calibration/fine_frequency_cal.py b/qiskit_experiments/library/calibration/fine_frequency_cal.py deleted file mode 100644 index ed9652af86..0000000000 --- a/qiskit_experiments/library/calibration/fine_frequency_cal.py +++ /dev/null @@ -1,173 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Fine frequency calibration experiment.""" - -from typing import Dict, List, Optional, Sequence -import numpy as np - -from qiskit.providers.backend import Backend -from qiskit.circuit import QuantumCircuit - -from qiskit_experiments.framework import ExperimentData -from qiskit_experiments.calibration_management.update_library import BaseUpdater -from qiskit_experiments.calibration_management import ( - BaseCalibrationExperiment, - Calibrations, -) -from qiskit_experiments.library.characterization.fine_frequency import FineFrequency - - -class FineFrequencyCal(BaseCalibrationExperiment, FineFrequency): - """A calibration version of the fine frequency experiment. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - # backend - from qiskit_ibm_runtime.fake_provider import FakePerth - from qiskit_aer import AerSimulator - - backend = AerSimulator.from_backend(FakePerth()) - - .. jupyter-execute:: - - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library.calibration.fine_frequency_cal import FineFrequencyCal - - cals = Calibrations.from_backend(backend=backend, libraries=[FixedFrequencyTransmon()]) - exp_cal = FineFrequencyCal((0,), cals, backend=backend, auto_update=False, gate_name="sx") - - cal_data=exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - backend: Optional[Backend] = None, - cal_parameter_name: Optional[str] = "drive_freq", - delay_duration: Optional[int] = None, - repetitions: List[int] = None, - auto_update: bool = True, - gate_name: str = "sx", - ): - r"""See class :class:`.FineFrequency` for details. - - Note that this class implicitly assumes that the target angle of the gate - is :math:`\pi/2` as seen from the default analysis options. This experiment - can be seen as a calibration of a finite duration ``rz(pi/2)`` gate with any - error attributed to a frequency offset in the qubit. - - Args: - physical_qubits: Sequence containing the qubit for which to run the - fine frequency calibration. - calibrations: The calibrations instance with the schedules. - backend: Optional, the backend to run the experiment on. - cal_parameter_name: The name of the parameter to update in the calibrations. - This defaults to `drive_freq`. - delay_duration: The duration of the delay at :math:`n=1`. If this value is - not given then the duration of the gate named ``gate_name`` in the - calibrations will be used. - auto_update: Whether to automatically update the calibrations or not. By - default, this variable is set to True. - gate_name: This argument is only needed if ``delay_duration`` is None. This - should be the name of a valid schedule in the calibrations. - """ - if delay_duration is None: - delay_duration = calibrations.get_schedule(gate_name, physical_qubits[0]).duration - - super().__init__( - calibrations, - physical_qubits, - delay_duration=delay_duration, - schedule_name=None, - repetitions=repetitions, - backend=backend, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - - if self.backend is not None: - self.set_experiment_options(dt=self._backend_data.dt) - - @classmethod - def _default_experiment_options(cls): - """default values for the fine frequency calibration experiment. - - Experiment Options: - dt (float): The duration of the time unit ``dt`` of the delay and schedules in seconds. - """ - options = super()._default_experiment_options() - options.dt = None - return options - - def _metadata(self) -> Dict[str, any]: - """Add metadata to the experiment data making it more self contained. - - The following keys are added to the experiment's metadata: - cal_param_value: The value of the drive frequency parameter. This value together with - the fit result will be used to find the new value of the drive frequency parameter. - cal_param_name: The name of the parameter in the calibrations. - cal_group: The calibration group to which the parameter belongs. - delay_duration: The duration of the first delay. - dt: The number of ``dt`` units of the delay. - """ - metadata = super()._metadata() - metadata["delay_duration"] = self.experiment_options.delay_duration - metadata["dt"] = self.experiment_options.dt - metadata["cal_param_value"] = self._cals.get_parameter_value( - self._param_name, - self.physical_qubits, - group=self.experiment_options.group, - ) - - return metadata - - def _attach_calibrations(self, circuit: QuantumCircuit): - """Adds the calibrations to the transpiled circuits.""" - schedule = self._cals.get_schedule("sx", self.physical_qubits) - circuit.add_calibration("sx", self.physical_qubits, schedule) - - def update_calibrations(self, experiment_data: ExperimentData): - r"""Update the qubit frequency based on the measured angle deviation. - - The frequency of the qubit is updated according to - - .. math:: - - f \to f - \frac{{\rm d}\theta}{2\pi\tau{\rm d}t} - - Here, :math:`{\rm d}\theta` is the measured angle error from the fit. The duration of - the single qubit-gate is :math:`\tau` in samples and :math:`{\rm d}t` is the duration - of a sample. This is also the duration of the time unit ``dt`` of the delay. - """ - result_index = self.experiment_options.result_index - group = experiment_data.metadata["cal_group"] - prev_freq = experiment_data.metadata["cal_param_value"] - tau = experiment_data.metadata["delay_duration"] - dt = experiment_data.metadata["dt"] - - d_theta = BaseUpdater.get_value(experiment_data, "d_theta", result_index) - new_freq = prev_freq + d_theta / (2 * np.pi * tau * dt) - - BaseUpdater.add_parameter_value( - self._cals, experiment_data, new_freq, self._param_name, self._sched_name, group - ) diff --git a/qiskit_experiments/library/calibration/frequency_cal.py b/qiskit_experiments/library/calibration/frequency_cal.py deleted file mode 100644 index 8f8bd48848..0000000000 --- a/qiskit_experiments/library/calibration/frequency_cal.py +++ /dev/null @@ -1,128 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Ramsey XY frequency calibration experiment.""" - -from typing import Dict, List, Optional, Sequence - -from qiskit.circuit import QuantumCircuit -from qiskit.providers.backend import Backend - -from qiskit_experiments.framework import ExperimentData -from qiskit_experiments.library.characterization.ramsey_xy import RamseyXY -from qiskit_experiments.calibration_management.calibrations import Calibrations -from qiskit_experiments.calibration_management.update_library import BaseUpdater -from qiskit_experiments.calibration_management.base_calibration_experiment import ( - BaseCalibrationExperiment, -) - - -class FrequencyCal(BaseCalibrationExperiment, RamseyXY): - """A qubit frequency calibration experiment based on the Ramsey XY experiment. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - # backend - from qiskit_ibm_runtime.fake_provider import FakePerth - from qiskit_aer import AerSimulator - - backend = AerSimulator.from_backend(FakePerth()) - - .. jupyter-execute:: - - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library.calibration.frequency_cal import FrequencyCal - - cals = Calibrations.from_backend(backend=backend, libraries=[FixedFrequencyTransmon()]) - exp_cal = FrequencyCal((0,), cals, backend=backend, auto_update=False) - - cal_data=exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - backend: Optional[Backend] = None, - cal_parameter_name: Optional[str] = "drive_freq", - delays: Optional[List] = None, - osc_freq: float = 2e6, - auto_update: bool = True, - ): - """ - Args: - physical_qubits: Sequence containing the qubit on which to run the - frequency calibration. - calibrations: The calibrations instance with the schedules. - backend: Optional, the backend to run the experiment on. - cal_parameter_name: The name of the parameter to update in the calibrations. - This defaults to `drive_freq`. - delays: The list of delays that will be scanned in the experiment, in seconds. - osc_freq: A frequency shift in Hz that will be applied by means of - a virtual Z rotation to increase the frequency of the measured oscillation. - auto_update: If set to True, which is the default, then the experiment will - automatically update the frequency in the calibrations. - """ - super().__init__( - calibrations, - physical_qubits, - backend=backend, - delays=delays, - osc_freq=osc_freq, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - - def _metadata(self) -> Dict[str, any]: - """Add the oscillation frequency of the experiment to the metadata.""" - metadata = super()._metadata() - metadata["osc_freq"] = self.experiment_options.osc_freq - metadata["cal_param_value"] = self._cals.get_parameter_value( - self._param_name, - self.physical_qubits, - group=self.experiment_options.group, - ) - - return metadata - - def _attach_calibrations(self, circuit: QuantumCircuit): - """Adds the calibrations to the transpiled circuits.""" - schedule = self._cals.get_schedule("sx", self.physical_qubits) - circuit.add_calibration("sx", self.physical_qubits, schedule) - - def update_calibrations(self, experiment_data: ExperimentData): - """Update the frequency using the reported frequency less the imparted oscillation.""" - - result_index = self.experiment_options.result_index - osc_freq = experiment_data.metadata["osc_freq"] - group = experiment_data.metadata["cal_group"] - old_freq = experiment_data.metadata["cal_param_value"] - - fit_freq = BaseUpdater.get_value(experiment_data, "freq", result_index) - new_freq = old_freq + fit_freq - osc_freq - - BaseUpdater.add_parameter_value( - self._cals, - experiment_data, - new_freq, - self._param_name, - group=group, - ) diff --git a/qiskit_experiments/library/calibration/half_angle_cal.py b/qiskit_experiments/library/calibration/half_angle_cal.py deleted file mode 100644 index b298cb552e..0000000000 --- a/qiskit_experiments/library/calibration/half_angle_cal.py +++ /dev/null @@ -1,203 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Half angle calibration.""" - -from typing import Dict, Optional, Sequence - -from qiskit.circuit import QuantumCircuit -from qiskit.providers.backend import Backend - -from qiskit_experiments.framework import ExperimentData -from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.calibration_management import ( - BaseCalibrationExperiment, - Calibrations, -) -from qiskit_experiments.library.characterization import HalfAngle -from qiskit_experiments.calibration_management.update_library import BaseUpdater - - -class HalfAngleCal(BaseCalibrationExperiment, HalfAngle): - """Calibration version of the :class:`.HalfAngle` experiment. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - warnings.filterwarnings("ignore", - message=".*entire Qiskit Pulse package is being deprecated.*", - category=DeprecationWarning, - ) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=199) - - .. jupyter-execute:: - - from qiskit import pulse - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library.calibration.half_angle_cal import HalfAngleCal - - library = FixedFrequencyTransmon(default_values={"duration": 640}) - cals = Calibrations.from_backend(backend=backend, libraries=[library]) - exp_cal = HalfAngleCal((0,), cals, backend=backend) - - inst_map = backend.defaults().instruction_schedule_map - with pulse.build(backend=backend, name="y") as sched_build: - pulse.play(pulse.Drag(duration=160, - sigma=40, - beta=5, - amp=0.05821399464431249, - angle=0.0,), pulse.DriveChannel(0),) - inst_map.add("y", (0,), sched_build) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - backend: Optional[Backend] = None, - schedule_name: str = "sx", - cal_parameter_name: Optional[str] = "angle", - auto_update: bool = True, - ): - """The experiment to update angle of half-pi rotation gates. - - Args: - physical_qubits: Sequence containing the qubit for which to run the - half-angle calibration. - calibrations: The calibrations instance with the schedules. - backend: Optional, the backend to run the experiment on. - schedule_name: The name of the schedule to calibrate which defaults to sx. - cal_parameter_name: The name of the parameter in the schedule to update. This will - default to 'angle' in accordance with the naming convention of the - :class:`~qiskit.pulse.ScalableSymbolicPulse` class. - auto_update: Whether or not to automatically update the calibrations. By - default this variable is set to True. - - Raises: - CalibrationError: if cal_parameter_name is set to ``amp``, to reflect the - transition from calibrating complex amplitude to calibrating the phase. - CalibrationError: if the default cal_parameter_name is used, and it is not - a valid parameter of the calibrated schedule. - """ - if cal_parameter_name == "amp": - raise CalibrationError( - "The Half-Angle calibration experiment was changed from calibrating" - " the pulse's complex amplitude, to calibrating the angle parameter " - "in the real (amp,angle) representation. Setting cal_parameter_name to " - "'amp' thus indicates that you are probably using the experiment in " - "an inconsistent way. If your pulse does in fact use a complex amplitude," - "you need to convert it to (amp,angle) representation, preferably using" - "the ScalableSymbolicPulse class. Note that all library pulses now use " - "this representation." - ) - # If the default cal_parameter_name is used, validate that it is in fact a parameter - if cal_parameter_name == "angle": - try: - calibrations.calibration_parameter("angle", schedule_name=schedule_name) - except CalibrationError as err: - raise CalibrationError( - "The Half-Angle calibration experiment was changed from calibrating" - " the pulse's complex amplitude, to calibrating the angle parameter " - "in the real (amp,angle) representation. The default cal_parameter_name " - "was thus changed to angle, which is not a valid parameter of the " - "calibrated schedule. It is likely that you are trying to calibrate " - "a schedule which is defined by a complex amplitude. To use the " - "Half-Angle experiment you need to convert the pulses in the schedule " - "to (amp,angle) representation (preferably, using the " - "ScalableSymbolicPulse class), and have a parameter associated with " - "the angle. Note that all library pulses now use this representation." - ) from err - - super().__init__( - calibrations, - physical_qubits, - backend=backend, - schedule_name=schedule_name, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - - def _metadata(self) -> Dict[str, any]: - """Add metadata to the experiment data making it more self contained. - - The following keys are added to the experiment's metadata: - cal_param_value: The value of the pulse amplitude. This value together with - the fit result will be used to find the new value of the pulse amplitude. - cal_param_name: The name of the parameter in the calibrations. - cal_schedule: The name of the schedule in the calibrations. - cal_group: The calibration group to which the parameter belongs. - """ - metadata = super()._metadata() - metadata["cal_param_value"] = self._cals.get_parameter_value( - self._param_name, - self._physical_qubits, - self._sched_name, - group=self.experiment_options.group, - ) - - return metadata - - def _attach_calibrations(self, circuit: QuantumCircuit): - """Adds the calibrations to the transpiled circuits.""" - for gate in ["y", "sx"]: - schedule = self._cals.get_schedule(gate, self.physical_qubits) - circuit.add_calibration(gate, self.physical_qubits, schedule) - - def update_calibrations(self, experiment_data: ExperimentData): - r"""Update the value of the parameter in the calibrations. - - The parameter that is updated is the phase of the sx pulse. This phase is contained - in the complex amplitude of the pulse. The update rule for the half angle calibration is - therefore: - - .. math:: - - A \to A \cdot e^{-i{\rm d}\theta_\text{hac}/2} - - where :math:`A` is the complex amplitude of the sx pulse which has an angle which might be - different from the angle of the x pulse due to the non-linearity in the mixer's skew. The - angle :math:`{\rm d}\theta_\text{hac}` is the angle deviation measured through the error - amplifying pulse sequence. - - Args: - experiment_data: The experiment data from which to extract the measured over/under - rotation used to adjust the amplitude. - """ - - result_index = self.experiment_options.result_index - group = experiment_data.metadata["cal_group"] - prev_angle = experiment_data.metadata["cal_param_value"] - - d_theta = BaseUpdater.get_value(experiment_data, "d_hac", result_index) - new_angle = prev_angle - (d_theta / 2) - - BaseUpdater.add_parameter_value( - self._cals, - experiment_data, - new_angle, - self._param_name, - self._sched_name, - group, - ) diff --git a/qiskit_experiments/library/calibration/rough_amplitude_cal.py b/qiskit_experiments/library/calibration/rough_amplitude_cal.py deleted file mode 100644 index 65251c912a..0000000000 --- a/qiskit_experiments/library/calibration/rough_amplitude_cal.py +++ /dev/null @@ -1,427 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Rough amplitude calibration using Rabi.""" - -from collections import namedtuple -from typing import Dict, Iterable, Optional, Sequence -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.providers.backend import Backend - -from qiskit_experiments.framework import ExperimentData -from qiskit_experiments.calibration_management import BaseCalibrationExperiment, Calibrations -from qiskit_experiments.library.characterization import Rabi -from qiskit_experiments.calibration_management.update_library import BaseUpdater - -AnglesSchedules = namedtuple( - "AnglesSchedules", ["target_angle", "parameter", "schedule", "previous_value"] -) - - -class RoughAmplitudeCal(BaseCalibrationExperiment, Rabi): - """A calibration version of the Rabi experiment. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=106) - - .. jupyter-execute:: - - import numpy as np - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library.calibration import RoughAmplitudeCal - - library = FixedFrequencyTransmon() - cals = Calibrations.from_backend(backend=backend, libraries=[library]) - - exp_cal = RoughAmplitudeCal(physical_qubits=(0,), - calibrations=cals, - schedule_name="x", - amplitudes=np.linspace(-0.1, 0.1, 51), - cal_parameter_name="amp", - target_angle=np.pi, - auto_update=True, - group="default", - backend=backend) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - schedule_name: str = "x", - amplitudes: Iterable[float] = None, - cal_parameter_name: Optional[str] = "amp", - target_angle: float = np.pi, - auto_update: bool = True, - group: str = "default", - backend: Optional[Backend] = None, - ): - r"""see class :class:`Rabi` for details. - - Args: - physical_qubits: Sequence containing the qubit for which to run the - rough amplitude calibration. - calibrations: The calibrations instance with the schedules. - schedule_name: The name of the schedule to calibrate. Defaults to "x". - amplitudes: A list of amplitudes to scan. If None is given 51 amplitudes ranging - from -0.95 to 0.95 will be scanned. - cal_parameter_name: The name of the parameter in the schedule to update. - target_angle: The target angle of the gate to calibrate this will default to a - :math:`\pi`-pulse. - auto_update: Whether or not to automatically update the calibrations. By - default this variable is set to True. - group: The group of calibration parameters to use. The default value is "default". - backend: Optional, the backend to run the experiment on. - """ - qubit = physical_qubits[0] - schedule = calibrations.get_schedule( - schedule_name, qubit, assign_params={cal_parameter_name: Parameter("amp")}, group=group - ) - - self._validate_channels(schedule, [qubit]) - self._validate_parameters(schedule, 1) - - super().__init__( - calibrations, - physical_qubits, - schedule=schedule, - amplitudes=amplitudes, - backend=backend, - schedule_name=schedule_name, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - - # Set the pulses to update. - prev_amp = calibrations.get_parameter_value(cal_parameter_name, qubit, schedule_name) - self.experiment_options.group = group - self.experiment_options.angles_schedules = [ - AnglesSchedules( - target_angle=target_angle, - parameter=cal_parameter_name, - schedule=schedule_name, - previous_value=prev_amp, - ) - ] - - @classmethod - def _default_experiment_options(cls): - """Default values for the rough amplitude calibration experiment. - - Experiment Options: - result_index (int): The index of the result from which to update the calibrations. - angles_schedules (list(float, str, str, float)): A list of parameter update information. - Each entry of the list is a tuple with four entries: the target angle of the - rotation, the name of the amplitude parameter to update, the name of the schedule - containing the amplitude parameter to update, and the previous value of the - amplitude parameter to update. This allows one experiment to update several - schedules, see for example :class:`RoughXSXAmplitudeCal`. - group (str): The calibration group to which the parameter belongs. This will default - to the value "default". - """ - options = super()._default_experiment_options() - - options.angles_schedules = [ - AnglesSchedules(target_angle=np.pi, parameter="amp", schedule="x", previous_value=None) - ] - - return options - - def _metadata(self) -> Dict[str, any]: - """Add metadata to the experiment data making it more self contained. - - The following keys are added to each circuit's metadata: - angles_schedules: A list of parameter update information. Each entry of the list - is a tuple with four entries: the target angle of the rotation, the name of the - amplitude parameter to update, the name of the schedule containing the amplitude - parameter to update, and the previous value of the amplitude parameter to update. - cal_group: The calibration group to which the amplitude parameters belong. - """ - metadata = super()._metadata() - param_values = [] - for angle, param_name, schedule_name, _ in self.experiment_options.angles_schedules: - param_val = self._cals.get_parameter_value( - param_name, - self._physical_qubits, - schedule_name, - group=self.experiment_options.group, - ) - - param_values.append( - AnglesSchedules( - target_angle=angle, - parameter=param_name, - schedule=schedule_name, - previous_value=param_val, - ) - ) - - metadata["angles_schedules"] = param_values - - return metadata - - def _attach_calibrations(self, circuit: QuantumCircuit): - """Rabi already has the schedules attached in the program circuits.""" - pass - - def update_calibrations(self, experiment_data: ExperimentData): - r"""Update the amplitude of one or several schedules. - - The update rule extracts the rate of the oscillation from the fit to the cosine function. - Recall that the amplitude is the x-axis in the analysis of the :class:`Rabi` experiment. - The value of the amplitude is thus the desired rotation-angle divided by the rate of - the oscillation: - - .. math:: - - A_i \to \frac{\theta_i}{\omega} - - where :math:`\theta_i` is the desired rotation angle (e.g. :math:`\pi` and :math:`\pi/2` - for "x" and "sx" gates, respectively) and :math:`\omega` is the rate of the oscillation. - - Args: - experiment_data: The experiment data from which to extract the measured Rabi oscillation - used to set the pulse amplitude. - """ - - result_index = self.experiment_options.result_index - group = experiment_data.metadata["cal_group"] - - rate = 2 * np.pi * BaseUpdater.get_value(experiment_data, self.__outcome__, result_index) - - for angle, param, schedule, prev_amp in experiment_data.metadata["angles_schedules"]: - - # This implementation conserves the type, while working for both real and complex prev_amp - value = np.round(angle / rate, decimals=8) * prev_amp / np.abs(prev_amp) - - BaseUpdater.add_parameter_value( - self._cals, experiment_data, value, param, schedule, group - ) - - -class RoughXSXAmplitudeCal(RoughAmplitudeCal): - """A rough amplitude calibration of x and sx gates. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=180) - - .. jupyter-execute:: - - import numpy as np - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library.calibration import RoughXSXAmplitudeCal - - library = FixedFrequencyTransmon() - cals = Calibrations.from_backend(backend, libraries=[library]) - exp_cal = RoughXSXAmplitudeCal((0,), - cals, - backend=backend, - amplitudes=np.linspace(-0.1, 0.1, 51) - ) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - amplitudes: Iterable[float] = None, - backend: Optional[Backend] = None, - ): - """A rough amplitude calibration that updates both the sx and x pulses.""" - super().__init__( - physical_qubits, - calibrations, - schedule_name="x", - amplitudes=amplitudes, - backend=backend, - cal_parameter_name="amp", - target_angle=np.pi, - ) - - self.experiment_options.angles_schedules = [ - AnglesSchedules(target_angle=np.pi, parameter="amp", schedule="x", previous_value=None), - AnglesSchedules( - target_angle=np.pi / 2, parameter="amp", schedule="sx", previous_value=None - ), - ] - - -class EFRoughXSXAmplitudeCal(RoughAmplitudeCal): - r"""A rough amplitude calibration of :math:`X` and :math:`SX` gates on the - :math:`|1\rangle` <-> :math:`|2\rangle` transition. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - warnings.filterwarnings("ignore", - message=".*entire Qiskit Pulse package is being deprecated.*", - category=DeprecationWarning, - ) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=180) - - .. jupyter-execute:: - - import numpy as np - import qiskit.pulse as pulse - from qiskit.circuit import Parameter - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library.calibration import EFRoughXSXAmplitudeCal - - library = FixedFrequencyTransmon() - cals = Calibrations.from_backend( - backend=backend, - libraries=[library] - ) - - amp = Parameter("amp") - - with pulse.build(name="x12") as build_x12: - with pulse.align_left(): - pulse.shift_frequency(-.25e9, pulse.DriveChannel(0)) - pulse.play(pulse.Drag(160, - amp, - 40, - 0., - 0., - ), pulse.DriveChannel(0)) - - with pulse.build(name="sx12") as build_sx12: - with pulse.align_left(): - pulse.shift_frequency(-.25e9, pulse.DriveChannel(0)) - pulse.play(pulse.Drag(160, - amp / 2, - 40, - 0., - 0., - ), pulse.DriveChannel(0)) - - cals.add_schedule(build_x12, qubits=(0,), num_qubits=1) - cals.add_schedule(build_sx12, qubits=(0,), num_qubits=1) - for sched in ["x", "x12"]: - cals.add_parameter_value(0.5, "amp", (0,), schedule=sched) - - for sched in ["sx", "sx12"]: - cals.add_parameter_value(0.25, "amp", (0,), schedule=sched) - - exp_cal = EFRoughXSXAmplitudeCal(physical_qubits=(0,), - calibrations=cals, - amplitudes=np.linspace(-0.1, 0.1, 51), - backend=backend, - ef_pulse_label="12",) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - __outcome__ = "rabi_rate_12" - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - amplitudes: Iterable[float] = None, - backend: Optional[Backend] = None, - ef_pulse_label: str = "12", - ): - r"""A rough amplitude calibration that updates both the sx and x pulses on the - :math:`|1\rangle` <-> :math:`|2\rangle` transition. - - Args: - physical_qubits: Sequence containing the index of the qubit - (technically a qutrit) to run on. - calibrations: The calibrations instance that stores the pulse schedules. - amplitudes: The amplitudes to scan. - backend: Optional, the backend to run the experiment on. - ef_pulse_label: A label that is post-pended to "x" and "sx" to obtain the name - of the pulses that drive a :math:`\pi` and :math:`\pi/2` rotation on - the :math:`|1\rangle` <-> :math:`|2\rangle` transition. - """ - super().__init__( - physical_qubits, - calibrations, - schedule_name="x" + ef_pulse_label, - amplitudes=amplitudes, - backend=backend, - cal_parameter_name="amp", - target_angle=np.pi, - ) - - self.experiment_options.angles_schedules = [ - AnglesSchedules( - target_angle=np.pi, - parameter="amp", - schedule="x" + ef_pulse_label, - previous_value=None, - ), - AnglesSchedules( - target_angle=np.pi / 2, - parameter="amp", - schedule="sx" + ef_pulse_label, - previous_value=None, - ), - ] - - def _pre_circuit(self) -> QuantumCircuit: - """A circuit with operations to perform before the Rabi.""" - circ = QuantumCircuit(1) - circ.x(0) - return circ - - def _attach_calibrations(self, circuit: QuantumCircuit): - """Attach an x calibration if it is defined.""" - # Attach the x calibration as well if it is in self._cals. We allow for - # it not to be present in case a user wants to rely on the default x - # calibration and only calibrate the pulses between levels 1 and 2. - if self._cals.has_template("x", self.physical_qubits): - schedule = self._cals.get_schedule("x", self.physical_qubits) - circuit.add_calibration("x", self.physical_qubits, schedule) diff --git a/qiskit_experiments/library/calibration/rough_drag_cal.py b/qiskit_experiments/library/calibration/rough_drag_cal.py deleted file mode 100644 index 4628472223..0000000000 --- a/qiskit_experiments/library/calibration/rough_drag_cal.py +++ /dev/null @@ -1,146 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Rough drag calibration experiment.""" - -from typing import Dict, Iterable, Optional, Sequence - -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.providers.backend import Backend - -from qiskit_experiments.framework import ExperimentData -from qiskit_experiments.calibration_management import ( - BaseCalibrationExperiment, - Calibrations, -) -from qiskit_experiments.calibration_management.update_library import BaseUpdater -from qiskit_experiments.library.characterization.drag import RoughDrag - - -class RoughDragCal(BaseCalibrationExperiment, RoughDrag): - """A calibration version of the :class:`.RoughDrag` experiment. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - #backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=161) - - .. jupyter-execute:: - - import numpy as np - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library import RoughDragCal - - library = FixedFrequencyTransmon(default_values={"duration": 320, "amp": 0.03}) - cals = Calibrations.from_backend(backend=backend, libraries=[library]) - exp_cal = RoughDragCal((0,), cals, backend=backend, betas=np.linspace(-20, 20, 25)) - exp_cal.set_experiment_options(reps=[3, 5, 7]) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - - # section: manual - :ref:`DRAG Calibration` - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - backend: Optional[Backend] = None, - schedule_name: str = "x", - betas: Iterable[float] = None, - cal_parameter_name: Optional[str] = "β", - auto_update: bool = True, - group: str = "default", - ): - r"""see class :class:`.RoughDrag` for details. - - Args: - physical_qubits: Sequence containing the qubit for which to run the - rough DRAG calibration. - calibrations: The calibrations instance with the schedules. - backend: Optional, the backend to run the experiment on. - schedule_name: The name of the schedule to calibrate. Defaults to "x". - betas: A list of DRAG parameter values to scan. If None is given 51 betas ranging - from -5 to 5 will be scanned. - cal_parameter_name: The name of the parameter in the schedule to update. - Defaults to "β". - auto_update: Whether or not to automatically update the calibrations. By - default this variable is set to True. - group: The group of calibration parameters to use. The default value is "default". - """ - qubit = physical_qubits[0] - schedule = calibrations.get_schedule( - schedule_name, qubit, assign_params={cal_parameter_name: Parameter("β")}, group=group - ) - - self._validate_channels(schedule, [qubit]) - self._validate_parameters(schedule, 1) - - super().__init__( - calibrations, - physical_qubits, - schedule=schedule, - betas=betas, - backend=backend, - schedule_name=schedule_name, - cal_parameter_name=cal_parameter_name, - auto_update=auto_update, - ) - - def _metadata(self) -> Dict[str, any]: - """Add metadata to the experiment data making it more self contained. - - The following keys are added to each experiment's metadata: - cal_param_value: The value of the previous calibrated beta. - cal_param_name: The name of the parameter in the calibrations. - cal_schedule: The name of the schedule in the calibrations. - cal_group: The calibration group to which the parameter belongs. - """ - metadata = super()._metadata() - metadata["cal_param_value"] = self._cals.get_parameter_value( - self._param_name, self.physical_qubits, self._sched_name, self.experiment_options.group - ) - return metadata - - def _attach_calibrations(self, circuit: QuantumCircuit): - """RoughDrag already has the schedules attached in the program circuits.""" - pass - - def update_calibrations(self, experiment_data: ExperimentData): - """Update the beta using the value directly reported from the fit. - - See :class:`.DragCalAnalysis` for details on the fit. - """ - - new_beta = BaseUpdater.get_value( - experiment_data, "beta", self.experiment_options.result_index - ) - - BaseUpdater.add_parameter_value( - self._cals, - experiment_data, - new_beta, - self._param_name, - self._sched_name, - self.experiment_options.group, - ) diff --git a/qiskit_experiments/library/calibration/rough_frequency.py b/qiskit_experiments/library/calibration/rough_frequency.py deleted file mode 100644 index da675df742..0000000000 --- a/qiskit_experiments/library/calibration/rough_frequency.py +++ /dev/null @@ -1,160 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Calibration version of spectroscopy experiments.""" - -from typing import Iterable, Optional, Sequence - -from qiskit.circuit import QuantumCircuit -from qiskit.providers.backend import Backend - -from qiskit_experiments.library.characterization.qubit_spectroscopy import QubitSpectroscopy -from qiskit_experiments.library.characterization.ef_spectroscopy import EFSpectroscopy -from qiskit_experiments.calibration_management.update_library import Frequency -from qiskit_experiments.calibration_management.calibrations import Calibrations -from qiskit_experiments.calibration_management.base_calibration_experiment import ( - BaseCalibrationExperiment, -) - - -class RoughFrequencyCal(BaseCalibrationExperiment, QubitSpectroscopy): - """A calibration experiment that runs :class:`.QubitSpectroscopy` to calibrate the qubit - transition frequency. - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=100) - - .. jupyter-execute:: - - import numpy as np - from qiskit_experiments.calibration_management.calibrations import Calibrations - from qiskit_experiments.calibration_management.basis_gate_library \ - import FixedFrequencyTransmon - from qiskit_experiments.library.calibration.rough_frequency import RoughFrequencyCal - - qubit=0 - library = FixedFrequencyTransmon() - cals = Calibrations.from_backend(backend=backend, libraries=[library]) - - freq_est = backend.defaults().qubit_freq_est[qubit] - frequencies = np.linspace(freq_est-15e6, freq_est+15e6, 51) - exp_cal = RoughFrequencyCal((qubit,), cals, frequencies, backend=backend) - exp_cal.set_experiment_options(amp=0.005) - - cal_data = exp_cal.run().block_for_results() - display(cal_data.figure(0)) - cal_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - frequencies: Iterable[float], - backend: Optional[Backend] = None, - auto_update: bool = True, - absolute: bool = True, - cal_parameter_name: Optional[str] = "drive_freq", - ): - """See :class:`.QubitSpectroscopy` for detailed documentation. - - Args: - physical_qubits: List with the qubit on which to run spectroscopy. - calibrations: If calibrations is given then running the experiment may update the values - of the frequencies stored in calibrations. - frequencies: The frequencies to scan in the experiment, in Hz. - backend: Optional, the backend to run the experiment on. - auto_update: If set to True, which is the default, then the experiment will - automatically update the frequency in the calibrations. - absolute: Boolean to specify if the frequencies are absolute or relative to the - qubit frequency in the backend. - cal_parameter_name: The name of the parameter to update in the calibrations. - This defaults to `drive_freq`. - - Raises: - QiskitError: If there are less than three frequency shifts. - - """ - super().__init__( - calibrations, - physical_qubits, - frequencies, - backend=backend, - absolute=absolute, - updater=Frequency, - auto_update=auto_update, - cal_parameter_name=cal_parameter_name, - ) - - def _attach_calibrations(self, circuit: QuantumCircuit): - """QubitSpectroscopy already has the schedules attached in the program circuits.""" - pass - - -class RoughEFFrequencyCal(BaseCalibrationExperiment, EFSpectroscopy): - r"""A calibration experiment that runs :class:`.QubitSpectroscopy` for the - :math:`|1\rangle` <-> :math:`|2\rangle` transition. - - """ - - __updater__ = Frequency - - # pylint: disable=super-init-not-called - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - frequencies: Iterable[float], - backend: Optional[Backend] = None, - auto_update: bool = True, - absolute: bool = True, - ): - """See :class:`.QubitSpectroscopy` for detailed documentation. - - Args: - physical_qubits: List containing the qubit on which to run spectroscopy. - calibrations: If calibrations is given then running the experiment may update the values - of the frequencies stored in calibrations. - frequencies: The frequencies to scan in the experiment, in Hz. - backend: Optional, the backend to run the experiment on. - auto_update: If set to True, which is the default, then the experiment will - automatically update the frequency in the calibrations. - absolute: Boolean to specify if the frequencies are absolute or relative to the - qubit frequency in the backend. - - Raises: - QiskitError: If there are less than three frequency shifts. - - """ - super().__init__( - calibrations, - physical_qubits, - frequencies, - backend, - absolute, - cal_parameter_name="f12", - updater=Frequency, - auto_update=auto_update, - ) - - def _attach_calibrations(self, circuit: QuantumCircuit): - """Adds the calibrations to the transpiled circuits.""" - schedule = self._cals.get_schedule("x", self.physical_qubits) - circuit.add_calibration("x", self.physical_qubits, schedule) diff --git a/qiskit_experiments/library/characterization/__init__.py b/qiskit_experiments/library/characterization/__init__.py index daa29bb4a4..0e3b1b7978 100644 --- a/qiskit_experiments/library/characterization/__init__.py +++ b/qiskit_experiments/library/characterization/__init__.py @@ -27,11 +27,6 @@ T2Ramsey T2Hahn Tphi - QubitSpectroscopy - CrossResonanceHamiltonian - EchoedCrossResonanceHamiltonian - Rabi - EFRabi HalfAngle FineAmplitude FineXAmplitude @@ -39,14 +34,12 @@ FineZXAmplitude RamseyXY FineFrequency - RoughDrag ReadoutAngle FineDrag FineXDrag FineSXDrag LocalReadoutError CorrelatedReadoutError - ResonatorSpectroscopy MultiStateDiscrimination ZZRamsey @@ -63,12 +56,9 @@ T2RamseyAnalysis T2HahnAnalysis TphiAnalysis - CrossResonanceHamiltonianAnalysis - DragCalAnalysis FineAmplitudeAnalysis RamseyXYAnalysis ReadoutAngleAnalysis - ResonatorSpectroscopyAnalysis LocalReadoutErrorAnalysis CorrelatedReadoutErrorAnalysis ZZRamseyAnalysis @@ -77,7 +67,6 @@ """ from .analysis import ( - DragCalAnalysis, FineAmplitudeAnalysis, RamseyXYAnalysis, T2RamseyAnalysis, @@ -85,9 +74,7 @@ T1KerneledAnalysis, T2HahnAnalysis, TphiAnalysis, - CrossResonanceHamiltonianAnalysis, ReadoutAngleAnalysis, - ResonatorSpectroscopyAnalysis, LocalReadoutErrorAnalysis, CorrelatedReadoutErrorAnalysis, ZZRamseyAnalysis, @@ -95,22 +82,16 @@ ) from .t1 import T1 -from .qubit_spectroscopy import QubitSpectroscopy -from .ef_spectroscopy import EFSpectroscopy from .t2ramsey import T2Ramsey from .t2hahn import T2Hahn from .tphi import Tphi -from .cr_hamiltonian import CrossResonanceHamiltonian, EchoedCrossResonanceHamiltonian -from .rabi import Rabi, EFRabi from .half_angle import HalfAngle from .fine_amplitude import FineAmplitude, FineXAmplitude, FineSXAmplitude, FineZXAmplitude from .ramsey_xy import RamseyXY from .fine_frequency import FineFrequency -from .drag import RoughDrag from .readout_angle import ReadoutAngle from .fine_drag import FineDrag, FineXDrag, FineSXDrag from .local_readout_error import LocalReadoutError from .correlated_readout_error import CorrelatedReadoutError -from .resonator_spectroscopy import ResonatorSpectroscopy from .zz_ramsey import ZZRamsey from .multi_state_discrimination import MultiStateDiscrimination diff --git a/qiskit_experiments/library/characterization/analysis/__init__.py b/qiskit_experiments/library/characterization/analysis/__init__.py index f249dcd9be..585d57302c 100644 --- a/qiskit_experiments/library/characterization/analysis/__init__.py +++ b/qiskit_experiments/library/characterization/analysis/__init__.py @@ -12,17 +12,14 @@ """Analysis Classes""" -from .drag_analysis import DragCalAnalysis from .fine_amplitude_analysis import FineAmplitudeAnalysis from .ramsey_xy_analysis import RamseyXYAnalysis from .t2ramsey_analysis import T2RamseyAnalysis from .t2hahn_analysis import T2HahnAnalysis from .t1_analysis import T1Analysis, T1KerneledAnalysis from .tphi_analysis import TphiAnalysis -from .cr_hamiltonian_analysis import CrossResonanceHamiltonianAnalysis from .readout_angle_analysis import ReadoutAngleAnalysis 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 from .multi_state_discrimination_analysis import MultiStateDiscriminationAnalysis diff --git a/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py b/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py deleted file mode 100644 index 2d1a41f624..0000000000 --- a/qiskit_experiments/library/characterization/analysis/cr_hamiltonian_analysis.py +++ /dev/null @@ -1,161 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Cross resonance Hamiltonian tomography experiment analysis.""" - -from typing import List, Dict -import numpy as np - -from qiskit.utils.deprecation import deprecate_func - -import qiskit_experiments.curve_analysis as curve -from qiskit_experiments.framework import AnalysisResultData -from qiskit_experiments.visualization import PlotStyle - - -class CrossResonanceHamiltonianAnalysis(curve.CompositeCurveAnalysis): - r"""A class to analyze cross resonance Hamiltonian tomography experiment. - - # section: fit_model - - This analysis performs :class:`.BlochTrajectoryAnalysis` on the target qubit - with the control qubit states in :math:`\in \{ |0\rangle, |1\rangle \}`. - - Based on the fit result, cross resonance Hamiltonian coefficients can be determined by - - .. math:: - - ZX &= \frac{p_{x, |0\rangle} - p_{x, |1\rangle}}{2}, \\ - ZY &= \frac{p_{y, |0\rangle} - p_{y, |1\rangle}}{2}, \\ - ZZ &= \frac{p_{z, |0\rangle} - p_{z, |1\rangle}}{2}, \\ - IX &= \frac{p_{x, |0\rangle} + p_{x, |1\rangle}}{2}, \\ - IY &= \frac{p_{y, |0\rangle} + p_{y, |1\rangle}}{2}, \\ - IZ &= \frac{p_{z, |0\rangle} + p_{z, |1\rangle}}{2}, - - where :math:`p_{\beta, |j\rangle}` is a fit parameter of :class:`.BlochTrajectoryAnalysis` - for the projection axis :math:`\beta` with the control qubit state :math:`|j\rangle`. - - """ - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments and related classses " - "involving pulse gate calibrations like this one have been deprecated." - ), - ) - def __init__(self): - analyses = [] - for control_state in (0, 1): - analysis = curve.BlochTrajectoryAnalysis(name=f"ctrl{control_state}") - analysis.set_options(filter_data={"control_state": control_state}) - analyses.append(analysis) - - super().__init__(analyses=analyses) - - @classmethod - def _default_options(cls): - """Return the default analysis options.""" - default_options = super()._default_options() - default_options.plotter.set_options( - subplots=(3, 1), - style=PlotStyle( - { - "figsize": (8, 10), - "legend_loc": "lower right", - "textbox_rel_pos": (0.28, -0.10), - } - ), - ) - default_options.plotter.set_figure_options( - xlabel="Flat top width", - ylabel=[ - r"$\langle$X(t)$\rangle$", - r"$\langle$Y(t)$\rangle$", - r"$\langle$Z(t)$\rangle$", - ], - xval_unit="s", - ylim=(-1, 1), - series_params={ - "x_ctrl0": { - "canvas": 0, - "color": "blue", - "label": "X (ctrl0)", - "symbol": "o", - }, - "y_ctrl0": { - "canvas": 1, - "color": "blue", - "label": "Y (ctrl0)", - "symbol": "o", - }, - "z_ctrl0": { - "canvas": 2, - "color": "blue", - "label": "Z (ctrl0)", - "symbol": "o", - }, - "x_ctrl1": { - "canvas": 0, - "color": "red", - "label": "X (ctrl1)", - "symbol": "^", - }, - "y_ctrl1": { - "canvas": 1, - "color": "red", - "label": "Y (ctrl1)", - "symbol": "^", - }, - "z_ctrl1": { - "canvas": 2, - "color": "red", - "label": "Z (ctrl1)", - "symbol": "^", - }, - }, - ) - - return default_options - - def _create_analysis_results( - self, - fit_data: Dict[str, curve.CurveFitResult], - quality: str, - **metadata, - ) -> List[AnalysisResultData]: - outcomes = [] - - for control in ("z", "i"): - for target in ("x", "y", "z"): - p0_val = fit_data["ctrl0"].ufloat_params[f"p{target}"] - p1_val = fit_data["ctrl1"].ufloat_params[f"p{target}"] - - 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) - - outcomes.append( - AnalysisResultData( - name=f"omega_{control}{target}", - value=coef_val, - quality=quality, - extra={ - "unit": "Hz", - **metadata, - }, - ) - ) - - return outcomes diff --git a/qiskit_experiments/library/characterization/analysis/drag_analysis.py b/qiskit_experiments/library/characterization/analysis/drag_analysis.py deleted file mode 100644 index 33093be1e9..0000000000 --- a/qiskit_experiments/library/characterization/analysis/drag_analysis.py +++ /dev/null @@ -1,287 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""DRAG pulse calibration experiment.""" - -import warnings -from typing import List, Optional, Union - -import lmfit -import numpy as np - -from qiskit.utils.deprecation import deprecate_func - -import qiskit_experiments.curve_analysis as curve -from qiskit_experiments.framework import ExperimentData -from qiskit_experiments.exceptions import AnalysisError - - -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 multiple series each to a cosine - function. All 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 :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]. - """ - - @classmethod - def _default_options(cls): - """Return the default analysis options.""" - default_options = super()._default_options() - default_options.plotter.set_figure_options( - xlabel="Beta", - ylabel="Signal (arb. units)", - ) - default_options.result_parameters = ["beta"] - default_options.normalization = True - - return default_options - - def set_options(self, **fields): - if "reps" in fields: - warnings.warn( - "Analysis option 'reps' has been dropped and analysis is bootstrapped by " - "circuit metadata. Setting this option no longer impacts analysis result.", - DeprecationWarning, - ) - del fields["reps"] - super().set_options(**fields) - - def _generate_fit_guesses( - self, - user_opt: curve.FitOptions, - curve_data: curve.ScatterTable, - ) -> Union[curve.FitOptions, List[curve.FitOptions]]: - """Create algorithmic initial fit guess from analysis options and curve data. - - Args: - user_opt: Fit options filled with user provided guess and bounds. - curve_data: Formatted data collection to fit. - - Returns: - List of fit options that are passed to the fitter function. - """ - # Use the highest-frequency curve to estimate the oscillation frequency. - max_rep_model_name = self.model_names()[-1] - max_rep = self.options.data_subfit_map[max_rep_model_name]["nrep"] - curve_data = curve_data.filter(series=max_rep_model_name) - - x_data = curve_data.x - min_beta, max_beta = min(x_data), max(x_data) - - freqs_guess = curve.guess.frequency(curve_data.x, curve_data.y) / max_rep - 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.ScatterTable, - ) -> curve.CurveFitResult: - 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. - - Returns: - The best fitting outcome with minimum reduced chi-squared value. - """ - fit_result = super()._run_curve_fit(curve_data) - - if fit_result and fit_result.params is not None: - beta = fit_result.params["beta"] - freq = fit_result.params["freq"] - min_beta = ((beta + 1 / freq / 2) % (1 / freq)) - 1 / freq / 2 - fit_result.params["beta"] = min_beta - - return fit_result - - def _evaluate_quality(self, fit_data: curve.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 and greater than zero, - - 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.ufloat_params["beta"] - fit_freq = fit_data.ufloat_params["freq"] - - criteria = [ - 0 < fit_data.reduced_chisq < 3, - abs(fit_beta.nominal_value) < 1 / fit_freq.nominal_value / 2, - curve.utils.is_error_not_significant(fit_beta), - ] - - if all(criteria): - return "good" - - return "bad" - - def _initialize( - self, - experiment_data: ExperimentData, - ): - reps = set(d.get("metadata", None).get("nrep", None) for d in experiment_data.data()) - if None in reps: - reps.remove(None) - if not reps: - raise AnalysisError( - f"{self.__class__.__name__} expects 'nrep' value in circuit metadata. " - "Please setup your experiment circuits with proper metadata." - ) - reps = sorted(reps) - - # Model is initialized at runtime because - # the experiment option "reps" can be changed before experiment run. - models = [] - data_subfit_map = {} - for nrep in sorted(reps): - name = f"nrep={nrep}" - models.append( - lmfit.models.ExpressionModel( - expr=f"amp * cos(2 * pi * {nrep} * freq * (x - beta)) + base", - name=name, - ) - ) - data_subfit_map[name] = {"nrep": nrep} - self._models = models - self._options.data_subfit_map = data_subfit_map - - super()._initialize(experiment_data) - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments and related classses " - "involving pulse gate calibrations like this one have been deprecated." - ), - ) - def __init__( - self, - models: Optional[List[lmfit.Model]] = None, - name: Optional[str] = None, - ): - """Initialize data fields that are privately accessed by methods. - - Args: - models: List of LMFIT ``Model`` class to define fitting functions and - parameters. If multiple models are provided, the analysis performs - multi-objective optimization where the parameters with the same name - are shared among provided models. When multiple models are provided, - user must specify the ``data_subfit_map`` value in the analysis options - to allocate experimental results to a particular fit model. - name: Optional. Name of this analysis. - """ - super().__init__() - - self._models = models or [] - self._name = name or self.__class__.__name__ - self._plot_config_cache = {} diff --git a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py b/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py deleted file mode 100644 index 6fecffe2f2..0000000000 --- a/qiskit_experiments/library/characterization/analysis/resonator_spectroscopy_analysis.py +++ /dev/null @@ -1,105 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Spectroscopy analysis class for resonators.""" - -from typing import List, Optional, Tuple -import numpy as np - -from qiskit.utils.deprecation import deprecate_func - -import qiskit_experiments.curve_analysis as curve -from qiskit_experiments.framework import AnalysisResultData, ExperimentData -from qiskit_experiments.framework.matplotlib import get_non_gui_ax -from qiskit_experiments.data_processing.nodes import ProjectorType -from qiskit_experiments.database_service.device_component import Resonator - - -class ResonatorSpectroscopyAnalysis(curve.ResonanceAnalysis): - """Class to analysis resonator spectroscopy.""" - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments and related classses " - "involving pulse gate calibrations like this one have been deprecated." - ), - ) - def __init__( - self, - name: Optional[str] = None, - ): - super().__init__(name=name) - - @classmethod - def _default_options(cls): - """Return default analysis options. - - Analysis Options: - dimensionality_reduction (ProjectorType): Type of the data processor node - that will reduce the two-dimensional data to one dimension. - plot_iq_data (bool): Set True to generate IQ plot. - """ - options = super()._default_options() - options.dimensionality_reduction = ProjectorType.ABS - options.result_parameters = [ - curve.ParameterRepr("freq", "res_freq0", "Hz"), - curve.ParameterRepr("kappa", "kappa", "Hz"), - ] - options.plot_iq_data = True - return options - - def _get_experiment_components(self, experiment_data: ExperimentData): - """Return resonators as experiment components.""" - return [Resonator(qubit) for qubit in experiment_data.metadata["physical_qubits"]] - - def _run_analysis( - self, experiment_data: ExperimentData - ) -> Tuple[List[AnalysisResultData], List["pyplot.Figure"]]: - """Wrap the analysis to optionally plot the IQ data.""" - analysis_results, figures = super()._run_analysis(experiment_data) - - if self.options.plot_iq_data: - axis = get_non_gui_ax() - figure = axis.get_figure() - # TODO: Move plotting to a new IQPlotter class. - figure.set_size_inches(*self.plotter.drawer.style["figsize"]) - - iqs = [] - - for datum in experiment_data.data(): - if "memory" in datum: - mem = np.array(datum["memory"]) - - # Average single-shot data. - if len(mem.shape) == 3: - for idx in range(mem.shape[1]): - iqs.append(np.average(mem[:, idx, :], axis=0)) - else: - iqs.append(mem) - - if len(iqs) > 0: - iqs = np.vstack(iqs) - axis.scatter(iqs[:, 0], iqs[:, 1], color="b") - axis.set_xlabel( - "In phase [arb. units]", fontsize=self.plotter.drawer.style["axis_label_size"] - ) - axis.set_ylabel( - "Quadrature [arb. units]", fontsize=self.plotter.drawer.style["axis_label_size"] - ) - axis.tick_params(labelsize=self.plotter.drawer.style["tick_label_size"]) - axis.grid(True) - - figures.append(figure) - - return analysis_results, figures diff --git a/qiskit_experiments/library/characterization/cr_hamiltonian.py b/qiskit_experiments/library/characterization/cr_hamiltonian.py deleted file mode 100644 index dd2c0e0c71..0000000000 --- a/qiskit_experiments/library/characterization/cr_hamiltonian.py +++ /dev/null @@ -1,539 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. -""" -Cross resonance Hamiltonian tomography. -""" - -import warnings -from typing import List, Tuple, Sequence, Optional, Type - -import numpy as np -from qiskit import pulse, circuit, QuantumCircuit -from qiskit.circuit.parameterexpression import ParameterValueType -from qiskit.exceptions import QiskitError -from qiskit.providers import Backend -from qiskit.utils.deprecation import deprecate_func -from qiskit_experiments.framework import ( - BaseExperiment, - BackendTiming, - Options, -) -from qiskit_experiments.library.characterization.analysis import CrossResonanceHamiltonianAnalysis - - -class CrossResonanceHamiltonian(BaseExperiment): - r"""Cross resonance Hamiltonian tomography experiment. - - # section: overview - - This experiment assumes the two qubit Hamiltonian in the form - - .. math:: - - H = \frac{I \otimes A}{2} + \frac{Z \otimes B}{2} - - where :math:`A` and :math:`B` are linear combinations of - the Pauli operators :math:`\in {X, Y, Z}`. - The coefficient of each Pauli term in the Hamiltonian - can be estimated with this experiment. - - This experiment is performed by stretching the pulse duration of a cross resonance pulse - and measuring the target qubit by projecting onto the x, y, and z bases. - The control qubit state dependent (controlled-) Rabi oscillation on the - target qubit is observed by repeating the experiment with the control qubit - both in the ground and excited states. The fit for the oscillations in the - three bases with the two control qubit preparations tomographically - reconstructs the Hamiltonian in the form shown above. - See Ref. [1] for more details. - - More specifically, the following circuits are executed in this experiment. - - .. parsed-literal:: - - (X measurement) - - ┌───┐┌────────────────────┐ - q_0: ┤ P ├┤0 ├──────────────────── - └───┘│ cr_tone(duration) │┌─────────┐┌────┐┌─┐ - q_1: ─────┤1 ├┤ Rz(π/2) ├┤ √X ├┤M├ - └────────────────────┘└─────────┘└────┘└╥┘ - c: 1/═════════════════════════════════════════════╩═ - 0 - - (Y measurement) - - ┌───┐┌────────────────────┐ - q_0: ┤ P ├┤0 ├───────── - └───┘│ cr_tone(duration) │┌────┐┌─┐ - q_1: ─────┤1 ├┤ √X ├┤M├ - └────────────────────┘└────┘└╥┘ - c: 1/══════════════════════════════════╩═ - 0 - - (Z measurement) - - ┌───┐┌────────────────────┐ - q_0: ┤ P ├┤0 ├─── - └───┘│ cr_tone(duration) │┌─┐ - q_1: ─────┤1 ├┤M├ - └────────────────────┘└╥┘ - c: 1/════════════════════════════╩═ - 0 - - The ``P`` gate on the control qubit (``q_0``) indicates the state preparation. - Since this experiment requires two sets of sub experiments with the control qubit in the - excited and ground state, ``P`` will become ``X`` gate or just be omitted, respectively. - Here ``cr_tone`` is implemented by a single cross resonance tone - driving the control qubit at the frequency of the target qubit. - The pulse envelope might be a flat-topped Gaussian implemented by the parametric pulse - :class:`~qiskit.pulse.library.parametric_pulses.GaussianSquare`. - - This experiment scans the total duration of the cross resonance pulse - including the pulse ramps at both edges. The pulse shape is defined by the - :class:`~qiskit.pulse.library.parametric_pulses.GaussianSquare`, and - an effective length of these Gaussian ramps with :math:`\sigma` can be computed by - - .. math:: - - \tau_{\rm edges}' = \sqrt{2 \pi} \sigma, - - which is usually shorter than the actual edge duration of - - .. math:: - - \tau_{\rm edges} = 2 r \sigma, - - where the :math:`r` is the ratio of the actual edge duration to :math:`\sigma`. - This effect must be considered in the following curve analysis to estimate - interaction rates. - - # section: analysis_ref - :class:`CrossResonanceHamiltonianAnalysis` - - # section: reference - .. ref_arxiv:: 1 1603.04821 - - # section: manual - .. ref_website:: Qiskit Textbook 6.7, - https://github.com/Qiskit/textbook/blob/main/notebooks/quantum-hardware-pulses/hamiltonian-tomography.ipynb - """ - - # Number of CR pulses. The flat top duration per pulse is divided by this number. - num_pulses = 1 - - class CRPulseGate(circuit.Gate): - """A pulse gate of cross resonance. Definition should be provided via calibration.""" - - def __init__(self, width: ParameterValueType): - super().__init__("cr_gate", 2, [width]) - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments involving pulse " - "gate calibrations like this one have been deprecated." - ), - ) - def __init__( - self, - physical_qubits: Tuple[int, int], - backend: Optional[Backend] = None, - cr_gate: Optional[Type[circuit.Gate]] = None, - durations: Optional[Sequence[int]] = None, - **kwargs, - ): - """Create a new experiment. - - Args: - physical_qubits: Two-value tuple of qubit indices on which to run tomography. - The first index stands for the control qubit. - backend: Optional, the backend to run the experiment on. - cr_gate: Optional, circuit gate class representing the cross resonance pulse. - Providing this object allows us to run this experiment with circuit simulator, - and this object might be used for testing, development of analysis protocol, - and educational purpose without needing to wait for hardware queueing. - Note that this instance must provide matrix representation, such as - unitary gate or Hamiltonian gate, and the class is expected to be instantiated - with a single parameter ``width`` in units of sec. - durations: Optional. The total duration of cross resonance pulse(s) including - rising and falling edges. The minimum number should be larger than the - total lengths of these ramps. If not provided, then ``num_durations`` evenly - spaced durations between ``min_durations`` and ``max_durations`` are - automatically generated from these experiment options. The default numbers - are chosen to have a good sensitivity for the Hamiltonian coefficient - of interest at the rate around 1 MHz. - This argument should be provided in units of sec. - kwargs: Pulse parameters. See :meth:`experiment_options` for details. - - Raises: - QiskitError: When ``qubits`` length is not 2. - """ - if len(physical_qubits) != 2: - raise QiskitError( - "Length of qubits is not 2. Please provide index for control and target qubit." - ) - - self._gate_cls = cr_gate or self.CRPulseGate - self._backend_timing = None - - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message="deprecation of Qiskit Pulse", - module="qiskit_experiments", - category=DeprecationWarning, - ) - analysis = CrossResonanceHamiltonianAnalysis() - super().__init__(physical_qubits, analysis=analysis, backend=backend) - self.set_experiment_options(durations=durations, **kwargs) - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default experiment options. - - Experiment Options: - durations (np.ndarray): The total duration of the cross resonance pulse(s) to scan, - in units of sec. Values should be longer than pulse ramps. - min_durations (int): The minimum default pulse duration in samples. - max_durations (int): The maximum default pulse duration in samples. - num_durations (int): The number of measured durations. The experiment automatically - creates durations of linear increment along with ``min_durations`` and - ``max_durations`` when user doesn't explicitly provide ``durations``. - amp (complex): Amplitude of the cross resonance tone. - amp_t (complex): Amplitude of the cancellation or rotary drive on target qubit. - sigma (float): Sigma of Gaussian rise and fall edges, in units of dt. - risefall (float): Ratio of edge durations to sigma. - """ - options = super()._default_experiment_options() - options.durations = None - options.min_durations = 60e-9 - options.max_durations = 1200e-9 - options.num_durations = 48 - options.amp = 0.5 - options.amp_t = 0.0 - options.sigma = 64 - options.risefall = 2 - - return options - - def _set_backend(self, backend: Backend): - """Set the backend for the experiment with timing analysis.""" - super()._set_backend(backend) - self._backend_timing = BackendTiming(backend) - - def _get_dt(self) -> float: - """A helper function to get finite dt. - - Returns: - Backend dt value. - """ - if not self._backend or self._backend_timing.dt is None: - # When backend timing is not initialized or backend doesn't report dt. - return 1.0 - return self._backend_timing.dt - - def _get_width(self, duration: ParameterValueType) -> ParameterValueType: - """A helper function to get flat top width. - - Args: - duration: Cross resonance pulse duration in units of sec. - - Returns: - A flat top widths of cross resonance pulse in units of sec. - """ - sigma_sec = self.experiment_options.sigma * self._get_dt() - - return duration - 2 * sigma_sec * self.experiment_options.risefall - - def _get_durations(self) -> np.ndarray: - """Return cross resonance pulse durations in units of sec.""" - opt = self.experiment_options - - if opt.durations is None: - return np.linspace(opt.min_durations, opt.max_durations, opt.num_durations) - - return np.asarray(opt.durations, dtype=float) - - def _build_cr_circuit(self, pulse_gate: circuit.Gate) -> QuantumCircuit: - """Single tone cross resonance. - - Args: - pulse_gate: A pulse gate to represent a single cross resonance pulse. - - Returns: - A circuit definition for the cross resonance pulse to measure. - """ - cr_circuit = QuantumCircuit(2) - cr_circuit.append(pulse_gate, [0, 1]) - - return cr_circuit - - def _build_default_schedule(self) -> pulse.ScheduleBlock: - """GaussianSquared cross resonance pulse. - - Returns: - A schedule definition for the cross resonance pulse to measure. - """ - opt = self.experiment_options - duration = circuit.Parameter("duration") - - cr_drive = self._backend_data.control_channel(self.physical_qubits)[0] - c_drive = self._backend_data.drive_channel(self.physical_qubits[0]) - t_drive = self._backend_data.drive_channel(self.physical_qubits[1]) - - with pulse.build(default_alignment="left", name="cr") as cross_resonance: - # add cross resonance tone - pulse.play( - pulse.GaussianSquare( - duration=duration, - amp=opt.amp, - sigma=opt.sigma, - risefall_sigma_ratio=opt.risefall, - ), - cr_drive, - ) - # add cancellation tone - if not np.isclose(opt.amp_t, 0.0): - pulse.play( - pulse.GaussianSquare( - duration=duration, - amp=opt.amp_t, - sigma=opt.sigma, - risefall_sigma_ratio=opt.risefall, - ), - t_drive, - ) - else: - pulse.delay(duration, t_drive) - - # place holder for empty drive channels. this is necessary due to known pulse gate bug. - pulse.delay(duration, c_drive) - - return cross_resonance - - def circuits(self) -> List[QuantumCircuit]: - """Return a list of experiment circuits. - - Returns: - A list of :class:`QuantumCircuit`. - - Raises: - QiskitError: When the backend is not set and cr gate is ``CRPulseGate`` type. - """ - if self._gate_cls is self.CRPulseGate: - if not self.backend: - # Backend is not set, but trying to provide CR gate as a pulse gate. - raise QiskitError( - "This experiment requires to have backend set to convert durations into samples " - "with backend reported dt value and also it requires the channel mapping from " - "the backend to build cross resonance pulse schedule. " - "Please provide valid backend object supporting 2Q pulse gate." - ) - return self._pulse_gate_circuits() - return self._unitary_circuits() - - def _pulse_gate_circuits(self): - """Protocol to create circuits with pulse gate. - - Pulse gate has backend timing constraints and duration should be in units of dt. - This method calls :meth:`_build_default_schedule` to generate actual schedule. - We assume backend has been set in this method call. - """ - schedule = self._build_default_schedule() - - # Assume this parameter is in units of dt, because this controls pulse samples. - param_duration = next(iter(schedule.get_parameters("duration"))) - - # Gate duration will be shown in sec, which is more intuitive. - cr_gate = self._gate_cls(width=self._get_width(self._backend_timing.dt * param_duration)) - - # Create parameterized circuits with calibration. - tmp_circs = [] - for control_state in (0, 1): - for meas_basis in ("x", "y", "z"): - tmp_qc = QuantumCircuit(2, 1) - if control_state: - tmp_qc.x(0) - tmp_qc.compose( - other=self._build_cr_circuit(cr_gate), - qubits=[0, 1], - inplace=True, - ) - if meas_basis == "x": - tmp_qc.rz(np.pi / 2, 1) - if meas_basis in ("x", "y"): - tmp_qc.sx(1) - tmp_qc.measure(1, 0) - tmp_qc.metadata = { - "control_state": control_state, - "meas_basis": meas_basis, - } - tmp_qc.add_calibration(cr_gate, self.physical_qubits, schedule) - tmp_circs.append(tmp_qc) - - circs = [] - for duration in self._get_durations(): - # Need to round pulse to satisfy hardware timing constraints. - # Convert into samples for assignment and validation. - valid_duration_dt = self._backend_timing.round_pulse(time=duration) - - # Convert into sec to pass xval to analysis. - # Analysis expects xval of flat top widths in units of sec. - flat_top_width_sec = self._get_width(self._backend_timing.dt * valid_duration_dt) - if flat_top_width_sec < 0: - raise ValueError( - f"Input duration={duration} is less than pulse ramps lengths, resulting in " - f"a negative flat top length of {flat_top_width_sec} sec. " - f"This cross resonance schedule is invalid." - ) - - for circ in tmp_circs: - # Assign duration in dt to create pulse schedule. - assigned_circ = circ.assign_parameters( - {param_duration: valid_duration_dt}, - inplace=False, - ) - assigned_circ.metadata["xval"] = self.num_pulses * flat_top_width_sec - circs.append(assigned_circ) - - return circs - - def _unitary_circuits(self): - """Protocol to create circuits with unitary gate. - - Unitary gate has no timing constraints and accepts duration in sec. - Basically, this method doesn't require backend apart from conversion of - sigma in samples into sec. - """ - # Assume this parameter is in units of sec. - param_duration = circuit.Parameter("duration") - - # Gate duration will be shown in sec, which is more intuitive. - cr_gate = self._gate_cls(width=self._get_width(param_duration)) - - # Create parameterized circuits without calibration. - tmp_circs = [] - for control_state in (0, 1): - for meas_basis in ("x", "y", "z"): - tmp_qc = QuantumCircuit(2, 1) - if control_state: - tmp_qc.x(0) - tmp_qc.compose( - other=self._build_cr_circuit(cr_gate), - qubits=[0, 1], - inplace=True, - ) - if meas_basis == "x": - tmp_qc.rz(np.pi / 2, 1) - if meas_basis in ("x", "y"): - tmp_qc.sx(1) - tmp_qc.measure(1, 0) - tmp_qc.metadata = { - "control_state": control_state, - "meas_basis": meas_basis, - } - tmp_circs.append(tmp_qc) - - circs = [] - for duration in self._get_durations(): - flat_top_width_sec = self._get_width(duration) - if flat_top_width_sec < 0: - raise ValueError( - f"Input duration={duration} is less than pulse ramps lengths, resulting in " - f"a negative flat top length of {flat_top_width_sec} sec. " - f"This cross resonance schedule is invalid." - ) - - for circ in tmp_circs: - # Assign duration in sec since this is unitary gate. - assigned_circ = circ.assign_parameters( - {param_duration: duration}, - inplace=False, - ) - assigned_circ.metadata["xval"] = self.num_pulses * flat_top_width_sec - circs.append(assigned_circ) - - return circs - - def _finalize(self): - """Set analysis option for initial guess that depends on experiment option values.""" - edge_duration = np.sqrt(2 * np.pi) * self.experiment_options.sigma * self.num_pulses - - for analysis in self.analysis.analyses(): - init_guess = analysis.options.p0.copy() - if "t_off" in init_guess: - continue - init_guess["t_off"] = self._get_dt() * edge_duration - analysis.set_options(p0=init_guess) - - def _metadata(self): - metadata = super()._metadata() - # Store measurement level and meas return if they have been - # set for the experiment - for run_opt in ["meas_level", "meas_return"]: - if hasattr(self.run_options, run_opt): - metadata[run_opt] = getattr(self.run_options, run_opt) - return metadata - - -class EchoedCrossResonanceHamiltonian(CrossResonanceHamiltonian): - r"""Echoed cross resonance Hamiltonian tomography experiment. - - # section: overview - - This is a variant of :class:`CrossResonanceHamiltonian` - for which the experiment framework is identical but the - cross resonance operation is realized as an echoed sequence - to remove unwanted single qubit rotations. The cross resonance - circuit looks like: - - .. parsed-literal:: - - ┌────────────────────┐ ┌───┐ ┌────────────────────┐ - q_0: ┤0 ├──┤ X ├──┤0 ├────────── - │ cr_tone(duration) │┌─┴───┴─┐│ cr_tone(duration) │┌────────┐ - q_1: ┤1 ├┤ Rz(π) ├┤1 ├┤ Rz(-π) ├ - └────────────────────┘└───────┘└────────────────────┘└────────┘ - - Here two ``cr_tone`` are applied, where the latter one is with the - control qubit state flipped and with a phase flip of the target qubit frame. - This operation is equivalent to applying the ``cr_tone`` with a negative amplitude. - The Hamiltonian for this decomposition has no IX and ZI interactions, - and also a reduced IY interaction to some extent (not completely eliminated) [1]. - Note that the CR Hamiltonian tomography experiment cannot detect the ZI term. - However, it is sensitive to the IX and IY terms. - - # section: reference - .. ref_arxiv:: 1 2007.02925 - - """ - - num_pulses = 2 - - def _build_cr_circuit(self, pulse_gate: circuit.Gate) -> QuantumCircuit: - """Single tone cross resonance. - - Args: - pulse_gate: A pulse gate to represent a single cross resonance pulse. - - Returns: - A circuit definition for the cross resonance pulse to measure. - """ - cr_circuit = QuantumCircuit(2) - cr_circuit.append(pulse_gate, [0, 1]) - cr_circuit.x(0) - cr_circuit.rz(np.pi, 1) - cr_circuit.append(pulse_gate, [0, 1]) - cr_circuit.rz(-np.pi, 1) - - return cr_circuit diff --git a/qiskit_experiments/library/characterization/drag.py b/qiskit_experiments/library/characterization/drag.py deleted file mode 100644 index 3c6c531081..0000000000 --- a/qiskit_experiments/library/characterization/drag.py +++ /dev/null @@ -1,247 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Rough drag experiment.""" - -import warnings -from typing import Iterable, List, Optional, Sequence -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit import Gate -from qiskit.exceptions import QiskitError -from qiskit.providers.backend import Backend -from qiskit.pulse import ScheduleBlock -from qiskit.utils.deprecation import deprecate_func - -from qiskit_experiments.framework import BaseExperiment, Options -from qiskit_experiments.framework.restless_mixin import RestlessMixin -from qiskit_experiments.library.characterization.analysis import DragCalAnalysis - - -class RoughDrag(BaseExperiment, RestlessMixin): - r"""An experiment that scans the DRAG parameter to find the optimal value. - - # section: overview - - A Derivative Removal by Adiabatic Gate (DRAG) pulse is designed to minimize phase - errors and leakage resulting from the presence of a neighbouring transition. DRAG - is a standard pulse with an additional derivative component. The optimal value of - the DRAG parameter, :math:`\beta`, is chosen to primarily minimize phase errors - resulting from the AC Stark shift and potentially leakage errors. The DRAG pulse is - - .. math:: - - f(t) = \Omega(t) + 1j \beta d/dt \Omega(t) - - Here, :math:`\Omega` is the envelop of the in-phase component of the pulse and - :math:`\beta` is the strength of the quadrature which we refer to as the DRAG - parameter and seek to calibrate in this experiment. The DRAG calibration will run - several series of circuits. In a given circuit a Rp(β) - Rm(β) block is repeated - :math:`N` times. Here, Rp is a rotation with a positive angle and Rm is the same rotation - with a native angle and is implemented by the gate sequence Rz(π) - Rp(β) - Rz(π) where - the Z rotations are virtual. As example the circuit of a single repetition, i.e. - :math:`N=1`, is shown below. - - .. parsed-literal:: - - ┌───────┐┌───────┐┌───────┐┌───────┐ ░ ┌─┐ - q_0: ┤ Rp(β) ├┤ Rz(π) ├┤ Rp(β) ├┤ Rz(π) ├─░─┤M├ - └───────┘└───────┘└───────┘└───────┘ ░ └╥┘ - measure: 1/════════════════════════════════════════╩═ - 0 - - The parameter β is scanned to find the value that minimizes the unwanted Z-rotation. - Note that the analysis class requires this experiment to run with three repetition numbers. - - # section: analysis_ref - :class:`DragCalAnalysis` - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - - warnings.filterwarnings( - "ignore", - message=".*Due to the deprecation of Qiskit Pulse.*", - category=DeprecationWarning, - ) - warnings.filterwarnings( - "ignore", - message=".*The entire Qiskit Pulse package is being deprecated.*", - category=DeprecationWarning, - ) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=101) - - .. jupyter-execute:: - - import numpy as np - from qiskit import pulse - from qiskit.circuit import Parameter - from qiskit_experiments.library import RoughDrag - - with pulse.build() as build_sched: - pulse.play(pulse.Drag(160, 0.50, 40, Parameter("beta")), pulse.DriveChannel(0)) - - exp = RoughDrag(physical_qubits=(0,), - schedule=build_sched, - betas = np.linspace(-4, 4, 51), - backend=backend,) - exp.set_experiment_options(reps=[3, 5, 7]) - - exp_data = exp.run().block_for_results() - display(exp_data.figure(0)) - exp_data.analysis_results(dataframe=True) - - # section: reference - .. ref_arxiv:: 1 1011.1949 - .. ref_arxiv:: 2 0901.0534 - .. ref_arxiv:: 3 1509.05470 - - # section: manual - :ref:`DRAG Calibration` - - """ - - @classmethod - def _default_experiment_options(cls) -> Options: - r"""Default values for the rough drag experiment. - - Experiment Options: - schedule (ScheduleBlock): The schedule of the rotation. - reps (List[int]): The number of times the Rp - Rm gate sequence is repeated in - each series. Note that this list must always have a length of three as - otherwise the analysis class will not run. - betas (Iterable): the values of the DRAG parameter to scan. - """ - options = super()._default_experiment_options() - options.schedule = None - options.reps = [1, 3, 5] - options.betas = np.linspace(-5, 5, 51) - - return options - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments involving pulse " - "gate calibrations like this one have been deprecated." - ), - ) - def __init__( - self, - physical_qubits: Sequence[int], - schedule: ScheduleBlock, - betas: Optional[Iterable[float]] = None, - backend: Optional[Backend] = None, - ): - """Initialize a Drag experiment in the given qubit. - - Args: - physical_qubits: Sequence containing the qubit for which to run the - Drag calibration. - schedule: The schedule to run. This schedule should have one free parameter - corresponding to a DRAG parameter. - betas: The values of the DRAG parameter to scan. If None is given the default range - :code:`linspace(-5, 5, 51)` is used. - backend: Optional, the backend to run the experiment on. - - Raises: - QiskitError: If the schedule does not have a free parameter. - """ - - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message="deprecation of Qiskit Pulse", - module="qiskit_experiments", - category=DeprecationWarning, - ) - analysis = DragCalAnalysis() - super().__init__(physical_qubits, analysis=analysis, backend=backend) - - if betas is not None: - self.set_experiment_options(betas=betas) - - if len(schedule.parameters) != 1: - raise QiskitError( - f"Schedule {schedule} for {self.__class__.__name__} experiment must have " - f"exactly one free parameter, found {schedule.parameters} parameters." - ) - - self.set_experiment_options(schedule=schedule) - - def _pre_circuit(self) -> QuantumCircuit: - """A circuit with operations to perform before the Drag.""" - return QuantumCircuit(1) - - def circuits(self) -> List[QuantumCircuit]: - """Create the circuits for the Drag calibration. - - Returns: - circuits: The circuits that will run the Drag calibration. - - Raises: - QiskitError: If the number of different repetition series is not three. - """ - schedule = self.experiment_options.schedule - - beta = next(iter(schedule.parameters)) - - # Note: If the pulse has a reserved name, e.g. x, which does not have parameters - # then we cannot directly call the gate x and attach a schedule to it. Doing so - # would results in QObj errors. - drag_gate = Gate(name="Drag(" + schedule.name + ")", num_qubits=1, params=[beta]) - - circuits = [] - for rep in self.experiment_options.reps: - circuit = self._pre_circuit() - for _ in range(rep): - circuit.append(drag_gate, (0,)) - circuit.rz(np.pi, 0) - circuit.append(drag_gate, (0,)) - circuit.rz(np.pi, 0) - - circuit.measure_active() - - circuit.add_calibration( - "Drag(" + schedule.name + ")", self.physical_qubits, schedule, params=[beta] - ) - - for beta_val in self.experiment_options.betas: - beta_val = float(np.round(beta_val, decimals=6)) - - assigned_circuit = circuit.assign_parameters({beta: beta_val}, inplace=False) - - assigned_circuit.metadata = { - "xval": beta_val, - "nrep": rep, - } - - circuits.append(assigned_circuit) - - return circuits - - def _metadata(self): - metadata = super()._metadata() - # Store measurement level and meas return if they have been - # set for the experiment - for run_opt in ["meas_level", "meas_return"]: - if hasattr(self.run_options, run_opt): - metadata[run_opt] = getattr(self.run_options, run_opt) - return metadata diff --git a/qiskit_experiments/library/characterization/ef_spectroscopy.py b/qiskit_experiments/library/characterization/ef_spectroscopy.py deleted file mode 100644 index 400927a0d5..0000000000 --- a/qiskit_experiments/library/characterization/ef_spectroscopy.py +++ /dev/null @@ -1,85 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Spectroscopy for the e-f transition.""" - -from typing import Iterable, Optional, Sequence -from qiskit import QuantumCircuit -from qiskit.providers import Backend -from qiskit.circuit import Gate - -from qiskit_experiments.curve_analysis import ParameterRepr -from qiskit_experiments.library.characterization.qubit_spectroscopy import QubitSpectroscopy - - -class EFSpectroscopy(QubitSpectroscopy): - """A spectroscopy experiment to obtain a frequency sweep of the qubit's e-f transition. - - # section: overview - The circuits produced by spectroscopy, i.e. - - .. parsed-literal:: - - ┌───┐┌────────────┐ ░ ┌─┐ - q_0: ┤ X ├┤ Spec(freq) ├─░─┤M├ - └───┘└────────────┘ ░ └╥┘ - measure: 1/═══════════════════════╩═ - 0 - # section: example - .. jupyter-execute:: - :hide-code: - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, noise=False, seed=100) - - .. jupyter-execute:: - - import numpy as np - from qiskit_experiments.library.characterization import EFSpectroscopy - - qubit = 0 - freq01_estimate = backend.defaults().qubit_freq_est[qubit] - freq12_estimate = freq01_estimate + (-.25e9) - frequencies = np.linspace(freq12_estimate-15e6, freq12_estimate+15e6, 51) - - exp = EFSpectroscopy( - physical_qubits = (0,), - frequencies = frequencies, - backend = backend, - absolute = True, - ) - exp.set_experiment_options(amp=0.005) - - exp_data = exp.run().block_for_results() - display(exp_data.figure(0)) - exp_data.analysis_results(dataframe=True) - """ - - def __init__( - self, - physical_qubits: Sequence[int], - frequencies: Iterable[float], - backend: Optional[Backend] = None, - absolute: bool = True, - ): - super().__init__(physical_qubits, frequencies, backend=backend, absolute=absolute) - self.analysis.set_options(result_parameters=[ParameterRepr("freq", "f12")]) - - def _template_circuit(self, freq_param) -> QuantumCircuit: - """Return the template quantum circuit.""" - circuit = QuantumCircuit(1) - circuit.x(0) - circuit.append(Gate(name=self.__spec_gate_name__, num_qubits=1, params=[freq_param]), (0,)) - circuit.measure_active() - - return circuit diff --git a/qiskit_experiments/library/characterization/fine_amplitude.py b/qiskit_experiments/library/characterization/fine_amplitude.py index 000acfb724..31fda5ca46 100644 --- a/qiskit_experiments/library/characterization/fine_amplitude.py +++ b/qiskit_experiments/library/characterization/fine_amplitude.py @@ -19,6 +19,8 @@ from qiskit.circuit import Gate from qiskit.circuit.library import XGate, SXGate from qiskit.providers.backend import Backend +from qiskit.utils import deprecate_func + from qiskit_experiments.data_processing import DataProcessor, nodes from qiskit_experiments.framework import BaseExperiment, Options from qiskit_experiments.framework.restless_mixin import RestlessMixin @@ -66,8 +68,20 @@ class FineAmplitude(BaseExperiment, RestlessMixin): :hide-code: # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e6, noise=True, seed=185) + from qiskit.circuit.library import RXGate + + from qiskit_aer import AerSimulator + from qiskit_aer.noise import NoiseModel, coherent_unitary_error + + + error = 0.05 + + x_error = coherent_unitary_error(RXGate(error).to_matrix()) + + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error(x_error, ["x"]) + + backend = AerSimulator(noise_model=noise_model) .. jupyter-execute:: @@ -88,9 +102,6 @@ class FineAmplitude(BaseExperiment, RestlessMixin): # section: reference .. ref_arxiv:: 1 1504.06597 - # section: manual - :ref:`fine-amplitude-cal` - """ @classmethod @@ -202,9 +213,6 @@ def circuits(self) -> List[QuantumCircuit]: Returns: A list of circuits with a variable number of gates. - - Raises: - CalibrationError: If the analysis options do not contain the angle_per_gate. """ repetitions = self.experiment_options.get("repetitions") @@ -261,8 +269,20 @@ class FineXAmplitude(FineAmplitude): :hide-code: # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=198) + from qiskit.circuit.library import RXGate + + from qiskit_aer import AerSimulator + from qiskit_aer.noise import NoiseModel, coherent_unitary_error + + + error = 0.05 + + x_error = coherent_unitary_error(RXGate(error).to_matrix()) + + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error(x_error, ["x"]) + + backend = AerSimulator(noise_model=noise_model) .. jupyter-execute:: @@ -317,8 +337,20 @@ class FineSXAmplitude(FineAmplitude): :hide-code: # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=198) + from qiskit.circuit.library import RXGate + + from qiskit_aer import AerSimulator + from qiskit_aer.noise import NoiseModel, coherent_unitary_error + + + error = 0.05 + + sx_error = coherent_unitary_error(RXGate(error).to_matrix()) + + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error(sx_error, ["sx"]) + + backend = AerSimulator(noise_model=noise_model) .. jupyter-execute:: @@ -373,24 +405,22 @@ class FineZXAmplitude(FineAmplitude): :class:`FineZXAmplitude` is a subclass of :class:`FineAmplitude` and is used to set the appropriate values for the default options to calibrate a :code:`RZXGate(np.pi / 2)`. - # section: example - - To run this experiment, the user will have to provide the instruction schedule - map in the transpile options that contains the schedule for the experiment. - - .. code-block:: python - - qubits = (1, 2) - inst_map = InstructionScheduleMap() - inst_map.add("szx", qubits, my_schedule) + .. note:: - fine_amp = FineZXAmplitude(qubits, backend) - fine_amp.set_transpile_options(inst_map=inst_map) + This experiment assumes a gate named ``szx`` which is not standard. + It was written work with a custom pulse calibration. - Here, :code:`my_schedule` is the pulse schedule that will implement the - :code:`RZXGate(np.pi / 2)` rotation. """ + @deprecate_func( + since="0.9", + additional_msg=( + "This experiment requires an RZXGate which is not standard and had " + "previously been implemented using Qiskit Pulse which was removed " + "in Qiskit 2.0." + ), + package_name="qiskit-experiments", + ) def __init__(self, physical_qubits: Sequence[int], backend: Optional[Backend] = None): """Initialize the experiment.""" diff --git a/qiskit_experiments/library/characterization/fine_drag.py b/qiskit_experiments/library/characterization/fine_drag.py index b9f9229074..4170618a31 100644 --- a/qiskit_experiments/library/characterization/fine_drag.py +++ b/qiskit_experiments/library/characterization/fine_drag.py @@ -132,8 +132,28 @@ class FineDrag(BaseExperiment, RestlessMixin): :hide-code: # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=199) + from math import pi + + from scipy.linalg import expm + + from qiskit.circuit.library import RXGate, XGate, ZGate + + from qiskit_aer import AerSimulator + from qiskit_aer.noise import NoiseModel, coherent_unitary_error + + + err = 0.01 * pi / 4 + x_with_err = expm(-1j * 1 / 2 * (pi / 2 * XGate().to_matrix() + err * ZGate().to_matrix())) + + err_mat = x_with_err @ RXGate(-pi/2).to_matrix() + + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error( + coherent_unitary_error(err_mat), + ["x"], + ) + + backend = AerSimulator(noise_model=noise_model) .. jupyter-execute:: @@ -158,12 +178,10 @@ def _default_experiment_options(cls) -> Options: Experiment Options: repetitions (List[int]): A list of the number of times that Rp - Rm gate sequence is repeated. - schedule (ScheduleBlock): The schedule for the plus rotation. gate (Gate): This is the gate such as XGate() that will be in the circuits. """ options = super()._default_experiment_options() options.repetitions = list(range(20)) - options.schedule = None options.gate = None return options @@ -211,10 +229,9 @@ def circuits(self) -> List[QuantumCircuit]: """Create the circuits for the fine DRAG calibration experiment. Returns: - A list of circuits with a variable number of gates. Each gate has the same - pulse schedule. + A list of circuits with a variable number of gates. """ - schedule, circuits = self.experiment_options.schedule, [] + circuits = [] for repetition in self.experiment_options.repetitions: circuit = self._pre_circuit() @@ -229,14 +246,6 @@ def circuits(self) -> List[QuantumCircuit]: circuit.measure_all() - if schedule is not None: - circuit.add_calibration( - self.experiment_options.gate.name, - self.physical_qubits, - schedule, - params=[], - ) - circuit.metadata = {"xval": repetition} circuits.append(circuit) @@ -261,8 +270,40 @@ class FineXDrag(FineDrag): :hide-code: # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=199) + from math import pi + + from scipy.linalg import expm + + from qiskit.circuit.library import RXGate, XGate, ZGate + + from qiskit_aer import AerSimulator + from qiskit_aer.noise import NoiseModel, coherent_unitary_error + + from qiskit_experiments.library import FineXDrag + + + err = 0.01 * pi / 4 + + # To first order in err this can be just + # x_with_err = expm(1j * 1/2 * (pi * XGate().to_matrix() - err * ZGate().to_matrix())) + # The (pi**2 - err**2)**0.5 is to keep the total rotation exactly pi, but + # keeping the full `X` gate might be more correct. + # The question is what matches best with the way the qubit is + # measured when `X` is calibrated first by Rabi drives + # that are being applied with some `Z` component. What `X` component + # matches the observed periodicity? + rot_matrix = ((pi**2 - err**2)**0.5 * XGate().to_matrix() + err * ZGate().to_matrix()) + x_with_err = expm(-1j * 1/2 * rot_matrix) + + err_mat = x_with_err @ RXGate(-pi).to_matrix() + + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error( + coherent_unitary_error(err_mat), + ["x"], + ) + + backend = AerSimulator(noise_model=noise_model) .. jupyter-execute:: @@ -305,8 +346,28 @@ class FineSXDrag(FineDrag): :hide-code: # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=199) + from math import pi + + from scipy.linalg import expm + + from qiskit.circuit.library import RXGate, XGate, ZGate + + from qiskit_aer import AerSimulator + from qiskit_aer.noise import NoiseModel, coherent_unitary_error + + + err = 0.01 * pi / 4 + x_with_err = expm(-1j * 1 / 2 * (pi / 2 * XGate().to_matrix() + err * ZGate().to_matrix())) + + err_mat = x_with_err @ RXGate(-pi/2).to_matrix() + + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error( + coherent_unitary_error(err_mat), + ["sx"], + ) + + backend = AerSimulator(noise_model=noise_model) .. jupyter-execute:: @@ -314,14 +375,6 @@ class FineSXDrag(FineDrag): from qiskit_experiments.library.characterization import FineSXDrag exp = FineSXDrag(physical_qubits=(0,), backend=backend) - exp.analysis.set_options(normalization= True, - fixed_parameters={ - "angle_per_gate" : 0.0, - "phase_offset" : np.pi/2, - "amp" : 0.6 - }, - ) - exp_data = exp.run().block_for_results() display(exp_data.figure(0)) exp_data.analysis_results(dataframe=True) diff --git a/qiskit_experiments/library/characterization/fine_frequency.py b/qiskit_experiments/library/characterization/fine_frequency.py index ce39417d67..6a694bc54e 100644 --- a/qiskit_experiments/library/characterization/fine_frequency.py +++ b/qiskit_experiments/library/characterization/fine_frequency.py @@ -54,22 +54,17 @@ class FineFrequency(BaseExperiment): :hide-code: # backend - from qiskit_ibm_runtime.fake_provider import FakePerth - from qiskit_aer import AerSimulator - backend =AerSimulator.from_backend(FakePerth()) + from qiskit_experiments.test import T2HahnBackend - .. jupyter-execute:: + # AerSimulator can not mimic a freqeuncy offset + backend = T2HahnBackend(frequency=1e5) - from qiskit_experiments.library.characterization import FineFrequency - repetitions = list(range(40)) + .. jupyter-execute:: - exp = FineFrequency((0,), - delay_duration=320, - backend=backend, - repetitions=repetitions) - exp.set_transpile_options(optimization_level=0, basis_gates=['sx', 'rz', 'delay']) + from qiskit_experiments.library.characterization import FineFrequency + exp = FineFrequency([0], delay_duration=int(30e-9 / backend.dt), backend=backend) exp_data = exp.run().block_for_results() display(exp_data.figure(0)) exp_data.analysis_results(dataframe=True) diff --git a/qiskit_experiments/library/characterization/half_angle.py b/qiskit_experiments/library/characterization/half_angle.py index ccd94cfe46..6667c81c24 100644 --- a/qiskit_experiments/library/characterization/half_angle.py +++ b/qiskit_experiments/library/characterization/half_angle.py @@ -94,8 +94,39 @@ class HalfAngle(BaseExperiment): :hide-code: # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=199) + from math import pi + + import numpy as np + from scipy.linalg import expm + + from qiskit.circuit.library import RXGate, RZGate, XGate, ZGate + + from qiskit_aer import AerSimulator + from qiskit_aer.noise import NoiseModel, coherent_unitary_error + + + err = 0.01 + + err_mat = ( + RZGate(err).to_matrix() + @ RXGate(pi/2).to_matrix() + @ RZGate(-err).to_matrix() + @ RXGate(-pi/2).to_matrix() + ) + + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error( + coherent_unitary_error(err_mat), + ["sx"], + ) + # Add neglibile x error becuase otherwise x gets dropped from the target and + # the x's get transpiled as two sx's, spoiling the calibration. + noise_model.add_all_qubit_quantum_error( + coherent_unitary_error([[np.exp(1j * 1e-4), 0], [0, np.exp(-1j * 1e-4)]]), + ["x"], + ) + + backend = AerSimulator(noise_model=noise_model) .. jupyter-execute:: diff --git a/qiskit_experiments/library/characterization/multi_state_discrimination.py b/qiskit_experiments/library/characterization/multi_state_discrimination.py index 37e72331ab..e3adadcbf6 100644 --- a/qiskit_experiments/library/characterization/multi_state_discrimination.py +++ b/qiskit_experiments/library/characterization/multi_state_discrimination.py @@ -13,13 +13,12 @@ """Multi state discrimination experiment.""" import warnings -from typing import Dict, List, Optional, Sequence +from typing import Any, Dict, List, Optional, Sequence from qiskit import QuantumCircuit from qiskit.circuit import Gate from qiskit.providers import Backend from qiskit.providers.options import Options -from qiskit.pulse import ScheduleBlock from qiskit.qobj.utils import MeasLevel, MeasReturnType from qiskit_experiments.framework import BaseExperiment from qiskit_experiments.library.characterization import MultiStateDiscriminationAnalysis @@ -59,8 +58,10 @@ class MultiStateDiscrimination(BaseExperiment): :hide-code: # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=199) + from qiskit_experiments.test.mock_iq_backend import MockMultiStateBackend + + + backend = MockMultiStateBackend([1, 1j, -1 + 1j], iq_noise=0.2, state_noise=0.2) .. jupyter-execute:: @@ -94,14 +95,10 @@ def _default_experiment_options(cls) -> Options: Experiment Options: n_states (int): The number of states to discriminate. - schedules (dict): A dictionary of the schedules for the gates in the experiment. Each key is - a gate name of the form ``xii+1`` which should implement an x-rotation between level - ``i`` and ``i+1``. """ options = super()._default_experiment_options() options.n_states = 2 - options.schedules = None return options @@ -110,7 +107,7 @@ def __init__( physical_qubits: Sequence[int], backend: Optional[Backend] = None, n_states: Optional[int] = None, - schedules: Optional[Dict[str, ScheduleBlock]] = None, + schedules: Optional[Dict[str, Any]] = None, ): """Setup an experiment to prepare different energy states on a given qubit. @@ -119,14 +116,15 @@ def __init__( experiment. backend: Optional, the backend to run the experiment on. n_states: The number of energy levels to prepare. - schedules: The schedules of the x gates between neighboring energy levels. + schedules: Deprecated and unused """ super().__init__( physical_qubits, analysis=MultiStateDiscriminationAnalysis(), backend=backend ) - self.experiment_options.schedules = schedules + if schedules: + warnings.warn("MultiStateDiscrimination no longer supports custom pulse schedules.") if n_states is not None: self.set_experiment_options(n_states=n_states) @@ -138,14 +136,6 @@ def circuits(self) -> List[QuantumCircuit]: Returns: A list of circuits preparing the different energy states. """ - warnings.warn( - ( - "Setting pulse schedules for x gates is deprecated as of " - "version 0.8 due to the deprecation of Qiskit Pulse. It will be " - "removed in a future release." - ), - DeprecationWarning, - ) circuits = [] for level in range(self.experiment_options.n_states): circuit = QuantumCircuit(1) @@ -160,12 +150,6 @@ def circuits(self) -> List[QuantumCircuit]: gate_name = f"x{idx}{idx + 1}" gate = Gate(name=gate_name, num_qubits=1, params=[]) circuit.append(gate, (0,)) - if self.experiment_options.schedules is not None: - circuit.add_calibration( - gate_name, - self._physical_qubits, - self.experiment_options.schedules[gate_name], - ) # label the circuit circuit.metadata = {"label": level} diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py deleted file mode 100644 index 1ca8b1b61c..0000000000 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ /dev/null @@ -1,165 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Spectroscopy experiment class.""" - -from typing import Tuple - -import numpy as np -from qiskit import QuantumCircuit -from qiskit.circuit import Gate, Parameter -from qiskit.exceptions import QiskitError -from qiskit import pulse - -from qiskit_experiments.framework import BackendTiming -from qiskit_experiments.library.characterization.spectroscopy import Spectroscopy - - -class QubitSpectroscopy(Spectroscopy): - """A spectroscopy experiment to obtain a frequency sweep of the qubit. - - # section: overview - The circuits produced by spectroscopy, i.e. - - .. parsed-literal:: - - ┌────────────┐ ░ ┌─┐ - q_0: ┤ Spec(freq) ├─░─┤M├ - └────────────┘ ░ └╥┘ - measure: 1/══════════════════╩═ - 0 - - have a spectroscopy pulse-schedule embedded in a spectroscopy gate. The - pulse-schedule consists of a set frequency instruction followed by a GaussianSquare - pulse. A list of circuits is generated, each with a different frequency "freq". - - # section: analysis_ref - :class:`~qiskit_experiments.curve_analysis.ResonanceAnalysis` - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - - warnings.filterwarnings( - "ignore", - message=".*Due to the deprecation of Qiskit Pulse.*", - category=DeprecationWarning, - ) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=199) - - .. jupyter-execute:: - - import numpy as np - from qiskit_experiments.library.characterization import QubitSpectroscopy - - qubit = 0 - freq01_estimate = backend.defaults().qubit_freq_est[qubit] - frequencies = np.linspace(freq01_estimate-15e6, freq01_estimate+15e6, 51) - - exp = QubitSpectroscopy(physical_qubits = (qubit,), - frequencies = frequencies, - backend = backend, - ) - exp.set_experiment_options(amp=0.005) - - exp_data = exp.run().block_for_results() - display(exp_data.figure(0)) - exp_data.analysis_results(dataframe=True) - """ - - __spec_gate_name__ = "Spec" - - @property - def _backend_center_frequency(self) -> float: - """Returns the center frequency of the experiment. - - Returns: - The center frequency of the experiment. - - Raises: - QiskitError: If the experiment does not have a backend set. - """ - if self.backend is None: - raise QiskitError("backend not set. Cannot determine the center frequency.") - - return self._backend_data.drive_freqs[self.physical_qubits[0]] - - def _template_circuit(self, freq_param) -> QuantumCircuit: - """Return the template quantum circuit.""" - circuit = QuantumCircuit(1) - circuit.append(Gate(name=self.__spec_gate_name__, num_qubits=1, params=[freq_param]), (0,)) - circuit.measure_active() - - return circuit - - def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: - """Create the spectroscopy schedule.""" - timing = BackendTiming(self.backend) - - if timing.dt is None: - raise QiskitError(f"{self.__class__.__name__} requires a backend with a dt value.") - - duration = timing.round_pulse(time=self.experiment_options.duration) - sigma = self.experiment_options.sigma / timing.dt - width = self.experiment_options.width / timing.dt - - freq_param = Parameter("frequency") - - with pulse.build(backend=self.backend, name="spectroscopy") as schedule: - pulse.shift_frequency(freq_param, pulse.DriveChannel(self.physical_qubits[0])) - pulse.play( - pulse.GaussianSquare( - duration=duration, - amp=self.experiment_options.amp, - sigma=sigma, - width=width, - ), - pulse.DriveChannel(self.physical_qubits[0]), - ) - pulse.shift_frequency(-freq_param, pulse.DriveChannel(self.physical_qubits[0])) - - return schedule, freq_param - - def circuits(self): - """Create the circuit for the spectroscopy experiment. - - The circuits are based on a GaussianSquare pulse and a frequency_shift instruction - encapsulated in a gate. - - Returns: - circuits: The circuits that will run the spectroscopy experiment. - """ - - # Create a template circuit - sched, freq_param = self._schedule() - circuit = self._template_circuit(freq_param) - circuit.add_calibration( - self.__spec_gate_name__, self.physical_qubits, sched, params=[freq_param] - ) - - # Create the circuits to run - circs = [] - for freq in self._frequencies: - freq_shift = freq - self._backend_center_frequency if self._absolute else freq - freq_shift = np.round(freq_shift, decimals=3) - - assigned_circ = circuit.assign_parameters({freq_param: freq_shift}, inplace=False) - self._add_metadata(assigned_circ, freq) - - circs.append(assigned_circ) - - return circs diff --git a/qiskit_experiments/library/characterization/rabi.py b/qiskit_experiments/library/characterization/rabi.py deleted file mode 100644 index 2c547ba7fa..0000000000 --- a/qiskit_experiments/library/characterization/rabi.py +++ /dev/null @@ -1,300 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Rabi amplitude experiment.""" - -from typing import Iterable, List, Optional, Sequence, Tuple -import numpy as np - -from qiskit import QuantumCircuit -from qiskit.circuit import Gate, Parameter -from qiskit.qobj.utils import MeasLevel -from qiskit.providers import Backend -from qiskit.pulse import ScheduleBlock -from qiskit.exceptions import QiskitError -from qiskit.utils.deprecation import deprecate_func - -from qiskit_experiments.framework import BaseExperiment, Options -from qiskit_experiments.framework.restless_mixin import RestlessMixin -from qiskit_experiments.curve_analysis import ParameterRepr, OscillationAnalysis - - -class Rabi(BaseExperiment, RestlessMixin): - r"""An experiment that scans a pulse amplitude to calibrate rotations on the :math:`|0\rangle` - <-> :math:`|1\rangle` transition. - - # section: overview - - The circuits have a custom rabi gate with the pulse schedule attached to it - through the calibrations. The circuits are of the form: - - .. parsed-literal:: - - ┌───────────┐ ░ ┌─┐ - q_0: ┤ Rabi(amp) ├─░─┤M├ - └───────────┘ ░ └╥┘ - measure: 1/═════════════════╩═ - 0 - - The user provides his own schedule for the Rabi at initialization which must have one - free parameter, i.e. the amplitude to scan and a drive channel which matches the qubit. - - # section: manual - :ref:`Rabi Calibration` - - See also the `Qiskit Textbook - `_ - for the pulse level programming of a Rabi experiment. - - # section: analysis_ref - :class:`~qiskit_experiments.curve_analysis.OscillationAnalysis` - - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - - warnings.filterwarnings( - "ignore", - message=".*Due to the deprecation of Qiskit Pulse.*", - category=DeprecationWarning, - ) - warnings.filterwarnings( - "ignore", - message=".*The entire Qiskit Pulse package is being deprecated.*", - category=DeprecationWarning, - ) - - # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-0.25e9, 1e9, 0.8e9, 1e4, noise=True, seed=199) - - .. jupyter-execute:: - - import numpy as np - from qiskit import pulse - from qiskit.circuit import Parameter - from qiskit_experiments.library import Rabi - - with pulse.build() as build_sched: - pulse.play(pulse.Gaussian(160, Parameter("amp"), 40), pulse.DriveChannel(0)) - - exp = Rabi(physical_qubits=(0,), - schedule=build_sched, - amplitudes=np.linspace(-0.1, 0.1, 21), - backend=backend,) - - exp_data = exp.run().block_for_results() - display(exp_data.figure(0)) - exp_data.analysis_results(dataframe=True) - """ - - __gate_name__ = "Rabi" - __outcome__ = "rabi_rate" - - @classmethod - def _default_run_options(cls) -> Options: - """Default option values for the experiment :meth:`run` method.""" - options = super()._default_run_options() - - options.meas_level = MeasLevel.KERNELED - options.meas_return = "single" - - return options - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default values for the pulse if no schedule is given. - - Experiment Options: - amplitudes (iterable): The list of amplitude values to scan. - schedule (ScheduleBlock): The schedule for the Rabi pulse. This schedule must have - exactly one free parameter. The drive channel should match the qubit. - - """ - options = super()._default_experiment_options() - - options.amplitudes = np.linspace(-0.95, 0.95, 51) - options.schedule = None - - return options - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments involving pulse " - "gate calibrations like this one have been deprecated." - ), - ) - def __init__( - self, - physical_qubits: Sequence[int], - schedule: ScheduleBlock, - amplitudes: Optional[Iterable[float]] = None, - backend: Optional[Backend] = None, - ): - """Initialize a Rabi experiment on the given qubit. - - Args: - physical_qubits: List with the qubit on which to run the Rabi experiment. - schedule: The schedule that will be used in the Rabi experiment. This schedule - should have one free parameter namely the amplitude. - amplitudes: The pulse amplitudes that one wishes to scan. If this variable is not - specified it will default to :code:`np.linspace(-0.95, 0.95, 51)`. - backend: Optional, the backend to run the experiment on. - """ - super().__init__(physical_qubits, analysis=OscillationAnalysis(), backend=backend) - - self.analysis.set_options( - result_parameters=[ParameterRepr("freq", self.__outcome__)], - normalization=True, - ) - self.analysis.plotter.set_figure_options( - xlabel="Amplitude", - ylabel="Signal (arb. units)", - ) - - if amplitudes is not None: - self.experiment_options.amplitudes = amplitudes - - self.experiment_options.schedule = schedule - - def _pre_circuit(self) -> QuantumCircuit: - """A circuit with operations to perform before the Rabi.""" - return QuantumCircuit(1) - - def _template_circuit(self) -> Tuple[QuantumCircuit, Parameter]: - """Return the template quantum circuit.""" - sched = self.experiment_options.schedule - param = next(iter(sched.parameters)) - - if len(sched.parameters) != 1: - raise QiskitError( - f"Schedule {sched} for {self.__class__.__name__} experiment must have " - f"exactly one free parameter, found {sched.parameters} parameters." - ) - - gate = Gate(name=self.__gate_name__, num_qubits=1, params=[param]) - - circuit = self._pre_circuit() - circuit.append(gate, (0,)) - circuit.measure_active() - circuit.add_calibration(gate, self._physical_qubits, sched, params=[param]) - - return circuit, param - - def circuits(self) -> List[QuantumCircuit]: - """Create the circuits for the Rabi experiment. - - Returns: - A list of circuits with a rabi gate with an attached schedule. Each schedule - will have a different value of the scanned amplitude. - """ - - # Create template circuit - circuit, param = self._template_circuit() - - # Create the circuits to run - circs = [] - for amp in self.experiment_options.amplitudes: - # casting is needed because for amplitude '0', np.round method return datatype of int32 - # which isn't serializable in the metadata. - amp = float(np.round(amp, decimals=6)) - assigned_circ = circuit.assign_parameters({param: amp}, inplace=False) - assigned_circ.metadata = {"xval": amp} - - circs.append(assigned_circ) - - return circs - - def _metadata(self): - metadata = super()._metadata() - # Store measurement level and meas return if they have been - # set for the experiment - for run_opt in ["meas_level", "meas_return"]: - if hasattr(self.run_options, run_opt): - metadata[run_opt] = getattr(self.run_options, run_opt) - return metadata - - -class EFRabi(Rabi): - r"""An experiment that scans the amplitude of a pulse inducing rotations on the - :math:`|1\rangle` <-> :math:`|2\rangle` transition. - - # section: overview - - This experiment is a subclass of the :class:`Rabi` experiment but takes place between - the first and second excited state. An initial X gate populates the first excited state. - The Rabi pulse is applied on the :math:`|1\rangle` <-> :math:`|2\rangle` transition - (sometimes also labeled the e <-> f transition). The necessary frequency shift (typically - the qubit anharmonicity) is given through the pulse schedule given at initialization. The - schedule is then also stored in the experiment options. The circuits are of the form: - - .. parsed-literal:: - - ┌───┐┌───────────┐ ░ ┌─┐ - q_0: ┤ X ├┤ Rabi(amp) ├─░─┤M├ - └───┘└───────────┘ ░ └╥┘ - measure: 1/══════════════════════╩═ - 0 - # section: example - .. jupyter-execute:: - :hide-code: - - import warnings - - warnings.filterwarnings( - "ignore", - message=".*Due to the deprecation of Qiskit Pulse.*", - category=DeprecationWarning, - ) - warnings.filterwarnings( - "ignore", - message=".*The entire Qiskit Pulse package is being deprecated.*", - category=DeprecationWarning, - ) - - # backend - from qiskit_experiments.test import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5e9, -0.25e9, 1e9, 0.8e9, 1e4, noise=True) - - .. jupyter-execute:: - - import numpy as np - from qiskit import pulse - from qiskit.circuit import Parameter - from qiskit_experiments.library import EFRabi - - with pulse.build() as build_sched: - with pulse.align_left(): - pulse.shift_frequency(-0.25e9, pulse.DriveChannel(0)) - pulse.play(pulse.Gaussian(160, Parameter("amp"), 40), pulse.DriveChannel(0)) - - exp = EFRabi(physical_qubits=(0,), - schedule=build_sched, - amplitudes=np.linspace(-0.1, 0.1, 21), - backend=backend,) - - exp_data = exp.run().block_for_results() - display(exp_data.figure(0)) - exp_data.analysis_results(dataframe=True) - """ - - __outcome__ = "rabi_rate_12" - - def _pre_circuit(self) -> QuantumCircuit: - """A circuit with operations to perform before the Rabi.""" - circ = QuantumCircuit(1) - circ.x(0) - return circ diff --git a/qiskit_experiments/library/characterization/ramsey_xy.py b/qiskit_experiments/library/characterization/ramsey_xy.py index 40566580f9..b5b5ae334e 100644 --- a/qiskit_experiments/library/characterization/ramsey_xy.py +++ b/qiskit_experiments/library/characterization/ramsey_xy.py @@ -86,10 +86,10 @@ class RamseyXY(BaseExperiment, RestlessMixin): :hide-code: # backend - from qiskit_aer import AerSimulator - from qiskit_ibm_runtime.fake_provider import FakePerth + from qiskit_experiments.test import T2HahnBackend - backend = AerSimulator.from_backend(FakePerth()) + # AerSimulator can not mimic a freqeuncy offset + backend = T2HahnBackend(frequency=1e5) .. jupyter-execute:: diff --git a/qiskit_experiments/library/characterization/readout_angle.py b/qiskit_experiments/library/characterization/readout_angle.py index 3902c147e1..bce173b705 100644 --- a/qiskit_experiments/library/characterization/readout_angle.py +++ b/qiskit_experiments/library/characterization/readout_angle.py @@ -54,8 +54,13 @@ class ReadoutAngle(BaseExperiment): :hide-code: # backend - from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=199) + from qiskit_experiments.test.mock_iq_backend import MockIQBackend + from qiskit_experiments.test.mock_iq_helpers import MockIQReadoutAngleHelper + + + backend = MockIQBackend( + MockIQReadoutAngleHelper(iq_cluster_centers=[((-3.0, 3.0), (5.0, 5.0))]), + ) .. jupyter-execute:: diff --git a/qiskit_experiments/library/characterization/resonator_spectroscopy.py b/qiskit_experiments/library/characterization/resonator_spectroscopy.py deleted file mode 100644 index f1f5f65fa5..0000000000 --- a/qiskit_experiments/library/characterization/resonator_spectroscopy.py +++ /dev/null @@ -1,298 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Spectroscopy experiment class for resonators.""" - -import warnings -from typing import Iterable, Optional, Sequence, Tuple -import numpy as np - -from qiskit import QuantumCircuit -from qiskit import pulse -from qiskit.circuit import Parameter -from qiskit.exceptions import QiskitError -from qiskit.providers import Backend - -from qiskit_experiments.framework import BackendData, BackendTiming, Options -from qiskit_experiments.library.characterization.spectroscopy import Spectroscopy -from qiskit_experiments.database_service import Resonator -from .analysis.resonator_spectroscopy_analysis import ResonatorSpectroscopyAnalysis - - -class ResonatorSpectroscopy(Spectroscopy): - """An experiment to perform frequency spectroscopy of the readout resonator. - - # section: overview - This experiment does spectroscopy on the readout resonator. It applies the following - circuit - - .. parsed-literal:: - - ┌─┐ - q: ┤M├ - └╥┘ - c: 1/═╩═ - 0 - - where a spectroscopy pulse is attached to the measurement instruction. An initial circuit - can be added before the measurement by setting the ``initial_circuit`` experiment option. If - set, the experiment applies the following circuit: - - .. parsed-literal:: - - ┌─────────────────┐┌─┐ - q: ┤ initial_circuit ├┤M├ - └─────────────────┘└╥┘ - c: 1/════════════════════╩═ - 0 - - Side note 1: when doing readout resonator spectroscopy, each measured IQ point has a - frequency dependent phase. Close to the resonance, the IQ points start rotating around - in the IQ plan. This effect must be accounted for in the data processing to produce a - meaningful signal. The default data processing workflow will therefore reduce the two- - dimensional IQ data to one-dimensional data using the magnitude of each IQ point. - - Side node 2: when running readout resonator spectroscopy in a parallel experiment the - user will need to specify the memory slot to use. This can easily be done with the code - shown below. - - .. code:: - - specs = [] - for slot, qubit in enumerate(qubits): - specs.append(ResonatorSpectroscopy( - physical_qubits=[qubit], - backend=backend2, - memory_slot=slot, - )) - - exp = ParallelExperiment(specs, backend=backend2) - - # section: warning - Some backends may not have the required functionality to properly support resonator - spectroscopy experiments. The experiment may not work or the resulting resonance - may not properly reflect the properties of the readout resonator. - - # section: example - - The resonator spectroscopy experiment can be run by doing: - - .. code:: python - - qubit = 1 - spec = ResonatorSpectroscopy([qubit], backend) - exp_data = spec.run().block_for_results() - exp_data.figure(0) - - This will measure the resonator attached to qubit 1 and report the resonance frequency - as well as the kappa, i.e. the line width, of the resonator. - - # section: analysis_ref - :class:`ResonatorSpectroscopyAnalysis` - """ - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default option values used for the spectroscopy pulse. - - All units of the resonator spectroscopy experiment are given in seconds. - - Experiment Options: - amp (float): The amplitude of the spectroscopy pulse. Defaults to 1 and must - be between 0 and 1. - duration (float): The duration in seconds of the spectroscopy pulse. - sigma (float): The standard deviation of the spectroscopy pulse in seconds. - width (float): The width of the flat-top part of the GaussianSquare pulse in - seconds. - initial_circuit (QuantumCircuit): A single-qubit initial circuit to run before the - measurement/spectroscopy pulse. The circuit must contain only a single qubit and zero - classical bits. If None, no circuit is appended before the measurement. Defaults to None. - memory_slot (int): The memory slot that the acquire instruction uses in the pulse schedule. - The default value is ``0``. This argument allows the experiment to function in a - :class:`.ParallelExperiment`. - """ - options = super()._default_experiment_options() - - options.amp = 1 - options.duration = 480e-9 - options.sigma = 60e-9 - options.width = 360e-9 - options.initial_circuit = None - options.memory_slot = 0 - - return options - - def set_experiment_options(self, **fields): - # Check that the initial circuit is for a single qubit only. - if "initial_circuit" in fields: - initial_circuit = fields["initial_circuit"] - if ( - initial_circuit is not None - and initial_circuit.num_qubits != 1 - or initial_circuit.num_clbits != 0 - ): - raise QiskitError( - "Initial circuit must be for exactly one qubit and zero classical bits. Got " - f"{initial_circuit.num_qubits} qubits and {initial_circuit.num_clbits} " - "classical bits instead." - ) - return super().set_experiment_options(**fields) - - def __init__( - self, - physical_qubits: Sequence[int], - backend: Optional[Backend] = None, - frequencies: Optional[Iterable[float]] = None, - absolute: bool = True, - **experiment_options, - ): - """Initialize a resonator spectroscopy experiment. - - A spectroscopy experiment run by setting the frequency of the readout drive. - The parameters of the GaussianSquare spectroscopy pulse can be specified at run-time - through the experiment options. - - Args: - physical_qubits: List containing the resonator on which to run readout - spectroscopy. - backend: Optional, the backend to run the experiment on. - frequencies: The frequencies to scan in the experiment, in Hz. The default values - range from -20 MHz to 20 MHz in 51 steps. If the ``absolute`` variable is - set to True then a center frequency obtained from the backend's defaults is - added to each value of this range. - absolute: Boolean to specify if the frequencies are absolute or relative to the - resonator frequency in the backend. The default value is True. - experiment_options: Key word arguments used to set the experiment options. - - Raises: - QiskitError: If no frequencies are given and absolute frequencies are desired and - no backend is given or the backend does not have default measurement frequencies. - """ - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message="deprecation of Qiskit Pulse", - module="qiskit_experiments", - category=DeprecationWarning, - ) - analysis = ResonatorSpectroscopyAnalysis() - - if frequencies is None: - frequencies = np.linspace(-20.0e6, 20.0e6, 51) - - if absolute: - frequencies += self._get_backend_meas_freq( - BackendData(backend) if backend is not None else None, - physical_qubits[0], - ) - - super().__init__( - physical_qubits, frequencies, backend, absolute, analysis, **experiment_options - ) - - @staticmethod - def _get_backend_meas_freq(backend_data: Optional[BackendData], qubit: int) -> float: - """Get backend meas_freq with error checking""" - if backend_data is None: - raise QiskitError( - "Cannot automatically compute absolute frequencies without a backend." - ) - - if len(backend_data.meas_freqs) < qubit + 1: - raise QiskitError( - "Cannot retrieve default measurement frequencies from backend. " - "Please set frequencies explicitly or set `absolute` to `False`." - ) - return backend_data.meas_freqs[qubit] - - @property - def _backend_center_frequency(self) -> float: - """Returns the center frequency of the experiment. - - Returns: - The center frequency of the experiment. - - Raises: - QiskitError: If the experiment does not have a backend set. - """ - return self._get_backend_meas_freq(self._backend_data, self.physical_qubits[0]) - - def _template_circuit(self) -> QuantumCircuit: - """Return the template quantum circuit.""" - circuit = QuantumCircuit(1, 1) - if self.experiment_options.initial_circuit is not None: - circuit.append(self.experiment_options.initial_circuit, [0]) - circuit.measure(0, 0) - - return circuit - - def _schedule(self) -> Tuple[pulse.ScheduleBlock, Parameter]: - """Create the spectroscopy schedule.""" - timing = BackendTiming(self.backend) - - if timing.dt is None: - raise QiskitError(f"{self.__class__.__name__} requires a backend with a dt value.") - - duration = timing.round_pulse(time=self.experiment_options.duration) - sigma = self.experiment_options.sigma / timing.dt - width = self.experiment_options.width / timing.dt - - qubit = self.physical_qubits[0] - - freq_param = Parameter("frequency") - - with pulse.build(backend=self.backend, name="spectroscopy") as schedule: - pulse.shift_frequency(freq_param, pulse.MeasureChannel(qubit)) - pulse.play( - pulse.GaussianSquare( - duration=duration, - amp=self.experiment_options.amp, - sigma=sigma, - width=width, - ), - pulse.MeasureChannel(qubit), - ) - pulse.acquire(duration, qubit, pulse.MemorySlot(self.experiment_options.memory_slot)) - - return schedule, freq_param - - def _metadata(self): - """Update metadata with the resonator components.""" - metadata = super()._metadata() - metadata["device_components"] = list(map(Resonator, self.physical_qubits)) - return metadata - - def circuits(self): - """Create the circuit for the spectroscopy experiment. - - The circuits are based on a GaussianSquare pulse and a frequency_shift instruction - encapsulated in a measurement instruction. - - Returns: - circuits: The circuits that will run the spectroscopy experiment. - """ - sched, freq_param = self._schedule() - - circs = [] - for freq in self._frequencies: - freq_shift = freq - self._backend_center_frequency if self._absolute else freq - freq_shift = np.round(freq_shift, decimals=3) - - sched_ = sched.assign_parameters({freq_param: freq_shift}, inplace=False) - - circuit = self._template_circuit() - circuit.add_calibration("measure", self.physical_qubits, sched_) - self._add_metadata(circuit, freq) - - circs.append(circuit) - - return circs diff --git a/qiskit_experiments/library/characterization/spectroscopy.py b/qiskit_experiments/library/characterization/spectroscopy.py deleted file mode 100644 index ab0cd01e4d..0000000000 --- a/qiskit_experiments/library/characterization/spectroscopy.py +++ /dev/null @@ -1,143 +0,0 @@ -# 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. - -"""Abstract spectroscopy experiment base class.""" - -import warnings -from abc import ABC, abstractmethod -from typing import Iterable, Optional, Sequence - -import numpy as np -from qiskit import QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.providers import Backend -from qiskit.qobj.utils import MeasLevel -from qiskit.utils.deprecation import deprecate_func - -from qiskit_experiments.framework import BaseAnalysis, BaseExperiment, Options -from qiskit_experiments.curve_analysis import ResonanceAnalysis - - -class Spectroscopy(BaseExperiment, ABC): - """An abstract class for spectroscopy experiments.""" - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default option values used for the spectroscopy pulse. - - Experiment Options: - amp (float): The amplitude of the spectroscopy pulse. Defaults to 0.1 and must - be between 0 and 1. - duration (int): The duration of the spectroscopy pulse. Defaults to 1024 samples. - sigma (float): The standard deviation of the flanks of the spectroscopy pulse. - Defaults to 256. - width (int): The width of the flat-top part of the GaussianSquare pulse. - Defaults to 0. - """ - options = super()._default_experiment_options() - - options.amp = 0.1 - options.duration = 240e-9 - options.sigma = 60e-9 - options.width = 0 - - return options - - @classmethod - def _default_run_options(cls) -> Options: - """Default options values for the experiment :meth:`run` method.""" - options = super()._default_run_options() - - options.meas_level = MeasLevel.KERNELED - options.meas_return = "avg" - - return options - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments involving pulse " - "gate calibrations like this one have been deprecated." - ), - ) - def __init__( - self, - physical_qubits: Sequence[int], - frequencies: Iterable[float], - backend: Optional[Backend] = None, - absolute: bool = True, - analysis: Optional[BaseAnalysis] = None, - **experiment_options, - ): - """A spectroscopy experiment where the frequency of a pulse is scanned. - - Args: - physical_qubits: List containing the qubit on which to run spectroscopy. - frequencies: The frequencies to scan in the experiment, in Hz. - backend: Optional, the backend to run the experiment on. - absolute: Boolean to specify if the frequencies are absolute or relative to the - qubit frequency in the backend. - analysis: An instance of the analysis class to use. - experiment_options: Key word arguments used to set the experiment options. - - Raises: - QiskitError: If there are less than three frequency shifts. - - """ - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message="deprecation of Qiskit Pulse", - module="qiskit_experiments", - category=DeprecationWarning, - ) - analysis = analysis or ResonanceAnalysis() - - super().__init__(physical_qubits, analysis=analysis, backend=backend) - - if len(frequencies) < 3: - raise QiskitError("Spectroscopy requires at least three frequencies.") - - self._frequencies = frequencies - self._absolute = absolute - - self.set_experiment_options(**experiment_options) - - @property - @abstractmethod - def _backend_center_frequency(self) -> float: - """The default frequency for the channel of the spectroscopy pulse. - - This frequency is used to calculate the appropriate frequency shifts to apply to the - spectroscopy pulse as its frequency is scanned in the experiment. Spectroscopy experiments - should implement schedules using frequency shifts. Therefore, if an absolute frequency - range is given the frequency shifts need to be corrected by the backend default frequency - which depends on the nature of the spectroscopy experiment. - """ - - def _add_metadata(self, circuit: QuantumCircuit, freq: float): - """Helper method to add the metadata to avoid code duplication with subclasses.""" - - if not self._absolute: - freq += self._backend_center_frequency - - circuit.metadata = {"xval": np.round(freq, decimals=3)} - - def _metadata(self): - metadata = super()._metadata() - # Store measurement level and meas return if they have been - # set for the experiment - for run_opt in ["meas_level", "meas_return"]: - if hasattr(self.run_options, run_opt): - metadata[run_opt] = getattr(self.run_options, run_opt) - return metadata diff --git a/qiskit_experiments/library/characterization/zz_ramsey.py b/qiskit_experiments/library/characterization/zz_ramsey.py index 69cc0a1306..5355861538 100644 --- a/qiskit_experiments/library/characterization/zz_ramsey.py +++ b/qiskit_experiments/library/characterization/zz_ramsey.py @@ -130,17 +130,9 @@ class ZZRamsey(BaseExperiment): :hide-code: # backend - from qiskit_ibm_runtime.fake_provider import FakePerth - from qiskit_aer import AerSimulator - from qiskit_aer.noise import NoiseModel + from qiskit_experiments.test.zzramsey_test_backend import ZZRamseyTestBackend - noise_model = NoiseModel.from_backend(FakePerth(), - thermal_relaxation=True, - gate_error=False, - readout_error=False, - ) - - backend = AerSimulator.from_backend(FakePerth(), noise_model=noise_model) + backend = ZZRamseyTestBackend(zz_frequency=50e3) .. jupyter-execute:: diff --git a/qiskit_experiments/library/driven_freq_tuning/__init__.py b/qiskit_experiments/library/driven_freq_tuning/__init__.py deleted file mode 100644 index 0bef002700..0000000000 --- a/qiskit_experiments/library/driven_freq_tuning/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -=============================================================================================== -Driven Frequency Tuning (:mod:`qiskit_experiments.library.driven_freq_tuning`) -=============================================================================================== - -.. currentmodule:: qiskit_experiments.library.driven_freq_tuning - -Experiments -=========== -.. autosummary:: - :toctree: ../stubs/ - :template: autosummary/experiment.rst - - StarkRamseyXY - StarkRamseyXYAmpScan - StarkP1Spectroscopy - - -Analysis -======== - -.. autosummary:: - :toctree: ../stubs/ - :template: autosummary/analysis.rst - - StarkRamseyXYAmpScanAnalysis - StarkP1SpectAnalysis - - -Stark Coefficient -================= - -.. autosummary:: - :toctree: ../stubs/ - - StarkCoefficients - retrieve_coefficients_from_backend - retrieve_coefficients_from_service -""" - -from .ramsey_amp_scan_analysis import StarkRamseyXYAmpScanAnalysis -from .p1_spect_analysis import StarkP1SpectAnalysis -from .ramsey import StarkRamseyXY -from .ramsey_amp_scan import StarkRamseyXYAmpScan -from .p1_spect import StarkP1Spectroscopy - -from .coefficients import ( - StarkCoefficients, - retrieve_coefficients_from_backend, - retrieve_coefficients_from_service, -) diff --git a/qiskit_experiments/library/driven_freq_tuning/coefficients.py b/qiskit_experiments/library/driven_freq_tuning/coefficients.py deleted file mode 100644 index c487f87f0a..0000000000 --- a/qiskit_experiments/library/driven_freq_tuning/coefficients.py +++ /dev/null @@ -1,280 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Coefficients characterizing Stark shift.""" - -from __future__ import annotations -from typing import Any - -import numpy as np - -from qiskit.providers.backend import Backend -from qiskit_ibm_experiment.service import IBMExperimentService -from qiskit_ibm_experiment.exceptions import IBMApiError - -from qiskit_experiments.framework.json import ExperimentDecoder -from qiskit_experiments.framework.backend_data import BackendData -from qiskit_experiments.framework.experiment_data import ExperimentData - - -class StarkCoefficients: - """A collection of coefficients characterizing Stark shift.""" - - def __init__( - self, - pos_coef_o1: float, - pos_coef_o2: float, - pos_coef_o3: float, - neg_coef_o1: float, - neg_coef_o2: float, - neg_coef_o3: float, - offset: float, - ): - """Create new coefficients object. - - Args: - pos_coef_o1: The first order shift coefficient on positive amplitude. - pos_coef_o2: The second order shift coefficient on positive amplitude. - pos_coef_o3: The third order shift coefficient on positive amplitude. - neg_coef_o1: The first order shift coefficient on negative amplitude. - neg_coef_o2: The second order shift coefficient on negative amplitude. - neg_coef_o3: The third order shift coefficient on negative amplitude. - offset: Offset frequency. - """ - self.pos_coef_o1 = pos_coef_o1 - self.pos_coef_o2 = pos_coef_o2 - self.pos_coef_o3 = pos_coef_o3 - self.neg_coef_o1 = neg_coef_o1 - self.neg_coef_o2 = neg_coef_o2 - self.neg_coef_o3 = neg_coef_o3 - self.offset = offset - - def positive_coeffs(self) -> list[float]: - """Positive coefficients.""" - return [self.pos_coef_o3, self.pos_coef_o2, self.pos_coef_o1] - - def negative_coeffs(self) -> list[float]: - """Negative coefficients.""" - return [self.neg_coef_o3, self.neg_coef_o2, self.neg_coef_o1] - - def convert_freq_to_amp( - self, - freqs: np.ndarray, - ) -> np.ndarray: - """A helper function to convert Stark frequency to amplitude. - - Args: - freqs: Target frequency shifts to compute required Stark amplitude. - - Returns: - Estimated Stark amplitudes to induce input frequency shifts. - - Raises: - ValueError: When amplitude value cannot be solved. - """ - amplitudes = np.zeros_like(freqs) - for idx, freq in enumerate(freqs): - shift = freq - self.offset - if np.isclose(shift, 0.0): - amplitudes[idx] = 0.0 - continue - if shift > 0: - fit = [*self.positive_coeffs(), -shift] - else: - fit = [*self.negative_coeffs(), -shift] - amp_candidates = np.roots(fit) - # Because the fit function is third order, we get three solutions here. - criteria = np.all( - [ - # Frequency shift and tone have the same sign by definition - np.sign(amp_candidates.real) == np.sign(shift), - # Tone amplitude is a real value - np.isclose(amp_candidates.imag, 0.0), - # The absolute value of tone amplitude must be less than 1.0 + 10 mp - np.abs(amp_candidates.real) < 1.0 + 10 * np.finfo(float).eps, - ], - axis=0, - ) - valid_amps = amp_candidates[criteria] - if len(valid_amps) == 0: - raise ValueError(f"Stark shift at frequency value of {freq} Hz is not available.") - if len(valid_amps) > 1: - # We assume a monotonic trend but sometimes a large third-order term causes - # inflection point and inverts the trend in larger amplitudes. - # In this case we would have more than one solution, but we can - # take the smallest amplitude before reaching to the inflection point. - before_inflection = np.argmin(np.abs(valid_amps.real)) - valid_amp = float(valid_amps[before_inflection].real) - else: - valid_amp = float(valid_amps[0].real) - amplitudes[idx] = min(valid_amp, 1.0) - return amplitudes - - def convert_amp_to_freq( - self, - amps: np.ndarray, - ) -> np.ndarray: - """A helper function to convert Stark amplitude to frequency shift. - - Args: - amps: Amplitude values to convert into frequency shift. - - Returns: - Calculated frequency shift at given Stark amplitude. - """ - pos_fit = np.poly1d([*self.positive_coeffs(), self.offset]) - neg_fit = np.poly1d([*self.negative_coeffs(), self.offset]) - - return np.where(amps > 0, pos_fit(amps), neg_fit(amps)) - - def find_min_max_frequency( - self, - min_amp: float, - max_amp: float, - ) -> tuple[float, float]: - """A helper function to estimate maximum frequency shift within given amplitude budget. - - Args: - min_amp: Minimum Stark amplitude. - max_amp: Maximum Stark amplitude. - - Returns: - Minimum and maximum frequency shift available within the amplitude range. - """ - trim_amps = [] - for amp in [min_amp, max_amp]: - if amp > 0: - fit = self.positive_coeffs() - else: - fit = self.negative_coeffs() - # Solve for inflection points by computing the point where derivative becomes zero. - solutions = np.roots([deriv * coeff for deriv, coeff in zip((3, 2, 1), fit)]) - inflection_points = solutions[ - (solutions.imag == 0) & (np.sign(solutions) == np.sign(amp)) - ] - if len(inflection_points) > 0: - # When multiple inflection points are found, use the most outer one. - # There could be a small inflection point around amp=0, - # when the first order term is significant. - amp = min([amp, max(inflection_points, key=abs)], key=abs) - trim_amps.append(amp) - return tuple(self.convert_amp_to_freq(np.asarray(trim_amps))) - - def __str__(self): - # Short representation for dataframe - return "StarkCoefficients(...)" - - def __eq__(self, other): - return all( - [ - self.pos_coef_o1 == other.pos_coef_o1, - self.pos_coef_o2 == other.pos_coef_o2, - self.pos_coef_o3 == other.pos_coef_o3, - self.neg_coef_o1 == other.neg_coef_o1, - self.neg_coef_o2 == other.neg_coef_o2, - self.neg_coef_o3 == other.neg_coef_o3, - self.offset == other.offset, - ] - ) - - def __json_encode__(self) -> dict[str, Any]: - return { - "class": "StarkCoefficients", - "data": { - "pos_coef_o1": self.pos_coef_o1, - "pos_coef_o2": self.pos_coef_o2, - "pos_coef_o3": self.pos_coef_o3, - "neg_coef_o1": self.neg_coef_o1, - "neg_coef_o2": self.neg_coef_o2, - "neg_coef_o3": self.neg_coef_o3, - "offset": self.offset, - }, - } - - @classmethod - def __json_decode__(cls, value: dict[str, Any]) -> "StarkCoefficients": - if not value.get("class", None) == "StarkCoefficients": - raise ValueError("JSON decoded value for StarkCoefficients is not valid class type.") - return StarkCoefficients(**value.get("data", {})) - - -def retrieve_coefficients_from_service( - service: IBMExperimentService, - backend_name: str, - qubit: int, -) -> StarkCoefficients: - """Retrieve StarkCoefficients object from experiment service. - - Args: - service: IBM Experiment service instance interfacing with result database. - backend_name: Name of target backend. - qubit: Index of qubit. - - Returns: - StarkCoefficients object. - - Raises: - RuntimeError: When stark_coefficients entry doesn't exist in the service. - """ - try: - retrieved = service.analysis_results( - device_components=[f"Q{qubit}"], - result_type="stark_coefficients", - backend_name=backend_name, - sort_by=["creation_datetime:desc"], - json_decoder=ExperimentDecoder, - # Returns the latest value only. IBM service returns 10 entries by default. - # This could contain old data from previous version, which might not be deserialized. - limit=1, - ) - except (IBMApiError, ValueError) as ex: - raise RuntimeError( - f"Failed to retrieve the result of stark_coefficients: {ex.message}" - ) from ex - if len(retrieved) == 0: - raise RuntimeError( - "Analysis result of stark_coefficients is not found in the " - "experiment service. Run and save the result of StarkRamseyXYAmpScan." - ) - - result_data_dict = retrieved[0].result_data - if "_value" in result_data_dict: - # In IBM Experiment service, the result_data["value"] returns - # a display value for the experiment service webpage. - # Original data is stored in "_value". - # TODO: this must be handled by experiment service. - return result_data_dict["_value"] - return result_data_dict["value"] - - -def retrieve_coefficients_from_backend( - backend: Backend, - qubit: int, -) -> StarkCoefficients: - """Retrieve StarkCoefficients object from the Qiskit backend. - - Args: - backend: Qiskit backend object. - qubit: Index of qubit. - - Returns: - StarkCoefficients object. - - Raises: - RuntimeError: When experiment service cannot be loaded from backend. - """ - name = BackendData(backend).name - service = ExperimentData.get_service_from_backend(backend) - - if service is None: - raise RuntimeError(f"Valid experiment service is not found for the backend {name}.") - - return retrieve_coefficients_from_service(service, name, qubit) diff --git a/qiskit_experiments/library/driven_freq_tuning/p1_spect.py b/qiskit_experiments/library/driven_freq_tuning/p1_spect.py deleted file mode 100644 index 5a2e867e54..0000000000 --- a/qiskit_experiments/library/driven_freq_tuning/p1_spect.py +++ /dev/null @@ -1,287 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""P1 experiment at various qubit frequencies.""" - -from __future__ import annotations - -import warnings -from collections.abc import Sequence - -import numpy as np -from qiskit import pulse -from qiskit.circuit import QuantumCircuit, Gate, Parameter, ParameterExpression -from qiskit.providers.backend import Backend -from qiskit.utils import optionals as _optional -from qiskit.utils.deprecation import deprecate_func - -from qiskit_experiments.framework import BackendTiming, BaseExperiment, Options -from .p1_spect_analysis import StarkP1SpectAnalysis - -from .coefficients import ( - StarkCoefficients, - retrieve_coefficients_from_backend, -) - -if _optional.HAS_SYMENGINE: - import symengine as sym -else: - import sympy as sym - - -class StarkP1Spectroscopy(BaseExperiment): - """P1 spectroscopy experiment with Stark tone. - - # section: overview - - This experiment measures a probability of the excitation state of the qubit - with a certain delay after excitation. - A Stark tone is applied during this delay to move the - qubit frequency to conduct a spectroscopy of qubit relaxation quantity. - - .. parsed-literal:: - - ┌───┐┌──────────────────┐┌─┐ - q: ┤ X ├┤ Stark(stark_amp) ├┤M├ - └───┘└──────────────────┘└╥┘ - c: 1/══════════════════════════╩═ - 0 - - Since the qubit relaxation rate may depend on the qubit frequency due to the - coupling to nearby energy levels, this experiment is useful to find out - lossy operation frequency that might be harmful to the gate fidelity [1]. - - # section: analysis_ref - :class:`qiskit_experiments.library.driven_freq_tuning.StarkP1SpectAnalysis` - - # section: reference - .. ref_arxiv:: 1 2105.15201 - - # section: see_also - :class:`qiskit_experiments.library.driven_freq_tuning.ramsey.StarkRamseyXY` - :class:`qiskit_experiments.library.driven_freq_tuning.ramsey_amp_scan.StarkRamseyXYAmpScan` - - # section: manual - :doc:`/manuals/characterization/stark_experiment` - """ - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments involving pulse " - "gate calibrations like this one have been deprecated." - ), - ) - def __init__( - self, - physical_qubits: Sequence[int], - backend: Backend | None = None, - **experiment_options, - ): - """ - Initialize new experiment class. - - Args: - physical_qubits: Sequence with the index of the physical qubit. - backend: Optional, the backend to run the experiment on. - experiment_options: Experiment options. See the class documentation or - ``self._default_experiment_options`` for descriptions. - """ - self._timing = None - - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message="deprecation of Qiskit Pulse", - module="qiskit_experiments", - category=DeprecationWarning, - ) - analysis = StarkP1SpectAnalysis() - - super().__init__( - physical_qubits=physical_qubits, - analysis=analysis, - backend=backend, - ) - self.set_experiment_options(**experiment_options) - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default experiment options. - - Experiment Options: - t1_delay (float): The T1 delay time after excitation pulse. The delay must be - sufficiently greater than the edge duration determined by the stark_sigma. - stark_channel (PulseChannel): Pulse channel to apply Stark tones. - If not provided, the same channel with the qubit drive is used. - stark_freq_offset (float): Offset of Stark tone frequency from the qubit frequency. - This must be greater than zero not to apply Rabi drive. - stark_sigma (float): Gaussian sigma of the rising and falling edges - of the Stark tone, in seconds. - stark_risefall (float): Ratio of sigma to the duration of - the rising and falling edges of the Stark tone. - min_xval (float): Minimum x value. - max_xval (float): Maximum x value. - num_xvals (int): Number of x-values to scan. - xval_type (str): Type of x-value. Either ``amplitude`` or ``frequency``. - Setting to frequency requires pre-calibration of Stark shift coefficients. - spacing (str): A policy for the spacing to create an amplitude list from - ``min_stark_amp`` to ``max_stark_amp``. Either ``linear`` or ``quadratic`` - must be specified. - xvals (list[float]): The list of x-values that will be scanned in the experiment. - If not set, then ``num_xvals`` parameters spaced according to - the ``spacing`` policy between ``min_xval`` and ``max_xval`` are used. - If ``xvals`` is set, these parameters are ignored. - stark_coefficients (StarkCoefficients): Calibrated Stark shift coefficients. - This value is necessary when xval_type is "frequency". - When this value is None, a search for the "stark_coefficients" in the - result database is run. This requires having the experiment service - available in the backend set for the experiment. - """ - options = super()._default_experiment_options() - options.update_options( - t1_delay=20e-6, - stark_channel=None, - stark_freq_offset=80e6, - stark_sigma=15e-9, - stark_risefall=2, - min_xval=-1.0, - max_xval=1.0, - num_xvals=201, - xval_type="amplitude", - spacing="quadratic", - xvals=None, - stark_coefficients=None, - ) - options.set_validator("spacing", ["linear", "quadratic"]) - options.set_validator("xval_type", ["amplitude", "frequency"]) - options.set_validator("stark_freq_offset", (0, np.inf)) - options.set_validator("stark_channel", pulse.channels.PulseChannel) - options.set_validator("stark_coefficients", StarkCoefficients) - return options - - def _set_backend(self, backend: Backend): - super()._set_backend(backend) - self._timing = BackendTiming(backend) - - def parameters(self) -> np.ndarray: - """Stark tone amplitudes to use in circuits. - - Returns: - The list of amplitudes to use for the different circuits based on the - experiment options. - - Raises: - ValueError: When invalid xval spacing is specified. - """ - opt = self.experiment_options # alias - - if opt.xvals is None: - if opt.spacing == "linear": - params = np.linspace(opt.min_xval, opt.max_xval, opt.num_xvals) - elif opt.spacing == "quadratic": - min_sqrt = np.sign(opt.min_xval) * np.sqrt(np.abs(opt.min_xval)) - max_sqrt = np.sign(opt.max_xval) * np.sqrt(np.abs(opt.max_xval)) - lin_params = np.linspace(min_sqrt, max_sqrt, opt.num_xvals) - params = np.sign(lin_params) * lin_params**2 - else: - raise ValueError(f"Spacing option {opt.spacing} is not valid.") - else: - params = np.asarray(opt.xvals, dtype=float) - - if opt.xval_type == "frequency": - coeffs = opt.stark_coefficients - if coeffs is None: - coeffs = retrieve_coefficients_from_backend( - backend=self.backend, - qubit=self.physical_qubits[0], - ) - return coeffs.convert_freq_to_amp(freqs=params) - return params - - def parameterized_circuits(self) -> tuple[QuantumCircuit, ...]: - """Create circuits with parameters for P1 experiment with Stark shift. - - Returns: - Quantum template circuit for P1 experiment. - """ - opt = self.experiment_options # alias - param = Parameter("stark_amp") - sym_param = param._symbol_expr - - # Pulse gates - stark = Gate("Stark", 1, [param]) - - # Note that Stark tone yields negative (positive) frequency shift - # when the Stark tone frequency is higher (lower) than qubit f01 frequency. - # This choice gives positive frequency shift with positive Stark amplitude. - qubit_f01 = self._backend_data.drive_freqs[self.physical_qubits[0]] - neg_sign_of_amp = ParameterExpression( - symbol_map={param: sym_param}, - expr=-sym.sign(sym_param), - ) - abs_of_amp = ParameterExpression( - symbol_map={param: sym_param}, - expr=sym.Abs(sym_param), - ) - stark_freq = qubit_f01 + neg_sign_of_amp * opt.stark_freq_offset - stark_channel = opt.stark_channel or pulse.DriveChannel(self.physical_qubits[0]) - sigma_dt = opt.stark_sigma / self._backend_data.dt - delay_dt = self._timing.round_pulse(time=opt.t1_delay) - - with pulse.build() as stark_schedule: - pulse.set_frequency(stark_freq, stark_channel) - pulse.play( - pulse.GaussianSquare( - duration=delay_dt, - amp=abs_of_amp, - sigma=sigma_dt, - risefall_sigma_ratio=opt.stark_risefall, - ), - stark_channel, - ) - - temp_t1 = QuantumCircuit(1, 1) - temp_t1.x(0) - temp_t1.append(stark, [0]) - temp_t1.measure(0, 0) - temp_t1.add_calibration( - gate=stark, - qubits=self.physical_qubits, - schedule=stark_schedule, - ) - - return (temp_t1,) - - def circuits(self) -> list[QuantumCircuit]: - """Create circuits. - - Returns: - A list of P1 circuits with a variable Stark tone amplitudes. - """ - (t1_circ,) = self.parameterized_circuits() - param = next(iter(t1_circ.parameters)) - - circs = [] - for amp in self.parameters(): - t1_assigned = t1_circ.assign_parameters({param: amp}, inplace=False) - t1_assigned.metadata = {"xval": amp} - circs.append(t1_assigned) - - return circs - - def _metadata(self) -> dict[str, any]: - """Return experiment metadata for ExperimentData.""" - metadata = super()._metadata() - metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset - - return metadata diff --git a/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py b/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py deleted file mode 100644 index 154f0715aa..0000000000 --- a/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py +++ /dev/null @@ -1,179 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""P1 spectroscopy analyses.""" - -from __future__ import annotations - -import numpy as np -from uncertainties import unumpy as unp - -from qiskit.utils.deprecation import deprecate_func - -import qiskit_experiments.data_processing as dp -import qiskit_experiments.visualization as vis -from qiskit_experiments.data_processing.exceptions import DataProcessorError -from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData, Options -from .coefficients import ( - StarkCoefficients, - retrieve_coefficients_from_service, -) - - -class StarkP1SpectAnalysis(BaseAnalysis): - """Analysis class for StarkP1Spectroscopy. - - # section: overview - - The P1 landscape is hardly predictable because of the random appearance of - lossy TLS notches, and hence this analysis doesn't provide any - generic mathematical model to fit the measurement data. - A developer may subclass this to conduct own analysis. - The :meth:`StarkP1SpectAnalysis._run_spect_analysis` is a hook method where - you can define a custom analysis protocol. - - By default, this analysis just visualizes the measured P1 values against Stark tone amplitudes. - The tone amplitudes can be converted into the amount of Stark shift - when the calibrated coefficients are provided in the analysis option, - or the calibration experiment results are available in the result database. - - # section: see_also - :class:`qiskit_experiments.library.driven_freq_tuning.StarkRamseyXYAmpScan` - - """ - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments and related classses " - "involving pulse gate calibrations like this one have been deprecated." - ), - ) - def __init__(self): - """Initialize the analysis object.""" - # Pass through to parent. This method is only here to be decorated by - # deprecate_func - super().__init__() - - @property - def plotter(self) -> vis.CurvePlotter: - """Curve plotter instance.""" - return self.options.plotter - - @classmethod - def _default_options(cls) -> Options: - """Default analysis options. - - Analysis Options: - plotter (Plotter): Plotter to visualize P1 landscape. - data_processor (DataProcessor): Data processor to compute P1 value. - stark_coefficients (Union[Dict, str]): Dictionary of Stark shift coefficients to - convert tone amplitudes into amount of Stark shift. This dictionary must include - all keys defined in :attr:`.StarkP1SpectAnalysis.stark_coefficients_names`, - which are calibrated with :class:`.StarkRamseyXYAmpScan`. - Alternatively, it searches for these coefficients in the result database - when "latest" is set. This requires having the experiment service set in - the experiment data to analyze. - x_key (str): Key of the circuit metadata to represent x value. - """ - options = super()._default_options() - - p1spect_plotter = vis.CurvePlotter(vis.MplDrawer()) - p1spect_plotter.set_figure_options( - xlabel="Stark amplitude", - ylabel="P(1)", - xscale="quadratic", - ) - - options.update_options( - plotter=p1spect_plotter, - data_processor=dp.DataProcessor("counts", [dp.Probability("1")]), - stark_coefficients=None, - x_key="xval", - ) - options.set_validator("stark_coefficients", StarkCoefficients) - - return options - - # pylint: disable=unused-argument - def _run_spect_analysis( - self, - xdata: np.ndarray, - ydata: np.ndarray, - ydata_err: np.ndarray, - ) -> list[AnalysisResultData]: - """Run further analysis on the spectroscopy data. - - .. note:: - A subclass can overwrite this method to conduct analysis. - - Args: - xdata: X values. This is either amplitudes or frequencies. - ydata: Y values. This is P1 values measured at different Stark tones. - ydata_err: Sampling error of the Y values. - - Returns: - A list of analysis results. - """ - return [] - - def _run_analysis( - self, - experiment_data: ExperimentData, - ) -> tuple[list[AnalysisResultData], list["matplotlib.figure.Figure"]]: - - x_key = self.options.x_key - - # Get calibrated Stark tone coefficients - if self.options.stark_coefficients is None and experiment_data.service is not None: - # Get value from service - stark_coeffs = retrieve_coefficients_from_service( - service=experiment_data.service, - backend_name=experiment_data.backend_name, - qubit=experiment_data.metadata["physical_qubits"][0], - ) - else: - stark_coeffs = self.options.stark_coefficients - - # Compute P1 value and sampling error - data = experiment_data.data() - try: - xdata = np.asarray([datum["metadata"][x_key] for datum in data], dtype=float) - except KeyError as ex: - raise DataProcessorError( - f"X value key {x_key} is not defined in circuit metadata." - ) from ex - ydata_ufloat = self.options.data_processor(data) - ydata = unp.nominal_values(ydata_ufloat) - ydata_err = unp.std_devs(ydata_ufloat) - - # Convert x-axis of amplitudes into Stark shift by consuming calibrated parameters. - if isinstance(stark_coeffs, StarkCoefficients): - xdata = stark_coeffs.convert_amp_to_freq(amps=xdata) - self.plotter.set_figure_options( - xlabel="Stark shift", - xval_unit="Hz", - xscale="linear", - ) - - # Draw figures and create analysis results. - self.plotter.set_series_data( - series_name="stark_p1", - x_formatted=xdata, - y_formatted=ydata, - y_formatted_err=ydata_err, - x_interp=xdata, - y_interp=ydata, - ) - analysis_results = self._run_spect_analysis(xdata, ydata, ydata_err) - - return analysis_results, [self.plotter.figure()] diff --git a/qiskit_experiments/library/driven_freq_tuning/ramsey.py b/qiskit_experiments/library/driven_freq_tuning/ramsey.py deleted file mode 100644 index fc46b198cf..0000000000 --- a/qiskit_experiments/library/driven_freq_tuning/ramsey.py +++ /dev/null @@ -1,368 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Stark Ramsey experiment.""" - -from __future__ import annotations - -import warnings -from collections.abc import Sequence - -import numpy as np -from qiskit import pulse -from qiskit.circuit import QuantumCircuit, Gate, Parameter -from qiskit.providers.backend import Backend -from qiskit.utils import optionals as _optional -from qiskit.utils.deprecation import deprecate_func - -from qiskit_experiments.framework import BaseExperiment, Options, BackendTiming -from qiskit_experiments.library.characterization.analysis import RamseyXYAnalysis - -if _optional.HAS_SYMENGINE: - pass -else: - pass - - -class StarkRamseyXY(BaseExperiment): - """A sign-sensitive experiment to measure the frequency of a qubit under a pulsed Stark tone. - - # section: overview - - This experiment is a variant of :class:`.RamseyXY` with a pulsed Stark tone - and consists of the following two circuits: - - .. parsed-literal:: - - (Ramsey X) The pulse before measurement rotates by pi-half around the X axis - - ┌────┐┌────────┐┌───┐┌───────────────┐┌────────┐┌────┐┌─┐ - q: ┤ √X ├┤ StarkV ├┤ X ├┤ StarkU(delay) ├┤ Rz(-π) ├┤ √X ├┤M├ - └────┘└────────┘└───┘└───────────────┘└────────┘└────┘└╥┘ - c: 1/═══════════════════════════════════════════════════════╩═ - 0 - - (Ramsey Y) The pulse before measurement rotates by pi-half around the Y axis - - ┌────┐┌────────┐┌───┐┌───────────────┐┌───────────┐┌────┐┌─┐ - q: ┤ √X ├┤ StarkV ├┤ X ├┤ StarkU(delay) ├┤ Rz(-3π/2) ├┤ √X ├┤M├ - └────┘└────────┘└───┘└───────────────┘└───────────┘└────┘└╥┘ - c: 1/══════════════════════════════════════════════════════════╩═ - 0 - - In principle, the sequence is a variant of :class:`.RamseyXY` circuit. - However, the delay in between √X gates is replaced with an off-resonant drive. - This off-resonant drive shifts the qubit frequency due to the - Stark effect and causes it to accumulate phase during the - Ramsey sequence. This frequency shift is a function of the - offset of the Stark tone frequency from the qubit frequency - and of the magnitude of the tone. - - Note that the Stark tone pulse (StarkU) takes the form of a flat-topped Gaussian envelope. - The magnitude of the pulse varies in time during its rising and falling edges. - It is difficult to characterize the net phase accumulation of the qubit during the - edges of the pulse when the frequency shift is varying with the pulse amplitude. - In order to simplify the analysis, an additional pulse (StarkV) - involving only the edges of StarkU is added to the sequence. - The sign of the phase accumulation is inverted for StarkV relative to that of StarkU - by inserting an X gate in between the two pulses. - - This technique allows the experiment to accumulate only the net phase - during the flat-top part of the StarkU pulse with constant magnitude. - - # section: analysis_ref - :class:`qiskit_experiments.library.characterization.RamseyXYAnalysis` - - # section: see_also - :class:`qiskit_experiments.library.characterization.ramsey_xy.RamseyXY` - - # section: manual - :doc:`/manuals/characterization/stark_experiment` - - """ - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments involving pulse " - "gate calibrations like this one have been deprecated." - ), - ) - def __init__( - self, - physical_qubits: Sequence[int], - backend: Backend | None = None, - **experiment_options, - ): - """Create new experiment. - - Args: - physical_qubits: Index of physical qubit. - backend: Optional, the backend to run the experiment on. - experiment_options: Experiment options. See the class documentation or - ``self._default_experiment_options`` for descriptions. - """ - self._timing = None - - super().__init__( - physical_qubits=physical_qubits, - analysis=RamseyXYAnalysis(), - backend=backend, - ) - self.set_experiment_options(**experiment_options) - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default experiment options. - - Experiment Options: - stark_amp (float): A single float parameter to represent the magnitude of Stark tone - and the sign of expected Stark shift. - See :ref:`stark_tone_implementation` for details. - stark_channel (PulseChannel): Pulse channel to apply Stark tones. - If not provided, the same channel with the qubit drive is used. - See :ref:`stark_channel_consideration` for details. - stark_freq_offset (float): Offset of Stark tone frequency from the qubit frequency. - This offset should be sufficiently large so that the Stark pulse - does not Rabi drive the qubit. - See :ref:`stark_frequency_consideration` for details. - stark_sigma (float): Gaussian sigma of the rising and falling edges - of the Stark tone, in seconds. - stark_risefall (float): Ratio of sigma to the duration of - the rising and falling edges of the Stark tone. - min_freq (float): Minimum frequency that this experiment is guaranteed to resolve. - Note that fitter algorithm :class:`.RamseyXYAnalysis` of this experiment - is still capable of fitting experiment data with lower frequency. - max_freq (float): Maximum frequency that this experiment can resolve. - delays (list[float]): The list of delays if set that will be scanned in the - experiment. If not set, then evenly spaced delays with interval - computed from ``min_freq`` and ``max_freq`` are used. - See :meth:`StarkRamseyXY.delays` for details. - """ - options = super()._default_experiment_options() - options.update_options( - stark_amp=0.0, - stark_channel=None, - stark_freq_offset=80e6, - stark_sigma=15e-9, - stark_risefall=2, - min_freq=5e6, - max_freq=100e6, - delays=None, - ) - options.set_validator("stark_freq_offset", (0, np.inf)) - options.set_validator("stark_channel", pulse.channels.PulseChannel) - return options - - def _set_backend(self, backend: Backend): - super()._set_backend(backend) - self._timing = BackendTiming(backend) - - def set_experiment_options(self, **fields): - _warning_circuit_length = 300 - - # Do validation for circuit number - min_freq = fields.get("min_freq", None) - max_freq = fields.get("max_freq", None) - delays = fields.get("delays", None) - if min_freq is not None and max_freq is not None: - if delays: - warnings.warn( - "Experiment option 'min_freq' and 'max_freq' are ignored " - "when 'delays' are explicitly specified.", - UserWarning, - ) - else: - n_expr_circs = 2 * int(2 * max_freq / min_freq) # delays * (x, y) - max_circs_per_job = None - if self._backend_data: - max_circs_per_job = self._backend_data.max_circuits() - if n_expr_circs > (max_circs_per_job or _warning_circuit_length): - warnings.warn( - f"Provided configuration generates {n_expr_circs} circuits. " - "You can set smaller 'max_freq' or larger 'min_freq' to reduce this number. " - "This experiment is still executable but your execution may consume " - "unnecessary long quantum device time, and result may suffer " - "device parameter drift in consequence of the long execution time.", - UserWarning, - ) - # Do validation for spectrum overlap to avoid real excitation - stark_freq_offset = fields.get("stark_freq_offset", None) - stark_sigma = fields.get("stark_sigma", None) - if stark_freq_offset is not None and stark_sigma is not None: - if stark_freq_offset < 1 / stark_sigma: - warnings.warn( - "Provided configuration may induce coherent state exchange between qubit levels " - "because of the potential spectrum overlap. You can avoid this by " - "increasing the 'stark_sigma' or 'stark_freq_offset'. " - "Note that this experiment is still executable.", - UserWarning, - ) - pass - - super().set_experiment_options(**fields) - - def parameters(self) -> np.ndarray: - """Delay values to use in circuits. - - .. note:: - - The delays are computed with the ``min_freq`` and ``max_freq`` experiment options. - The maximum point is computed from the ``min_freq`` to guarantee the result - contains at least one Ramsey oscillation cycle at this frequency. - The interval is computed from the ``max_freq`` to sample with resolution - such that the Nyquist frequency is twice ``max_freq``. - - Returns: - The list of delays to use for the different circuits based on the - experiment options. - - Raises: - ValueError: When ``min_freq`` is larger than ``max_freq``. - """ - opt = self.experiment_options # alias - - if opt.delays is None: - if opt.min_freq > opt.max_freq: - raise ValueError("Experiment option 'min_freq' must be smaller than 'max_freq'.") - # Delay is longer enough to capture 1 cycle of the minimum frequency. - # Fitter can still accurately fit samples shorter than 1 cycle. - max_period = 1 / opt.min_freq - # Inverse of interval should be greater than Nyquist frequency. - sampling_freq = 2 * opt.max_freq - interval = 1 / sampling_freq - return np.arange(0, max_period, interval) - return opt.delays - - def parameterized_circuits(self) -> tuple[QuantumCircuit, ...]: - """Create circuits with parameters for Ramsey XY experiment with Stark tone. - - Returns: - Quantum template circuits for Ramsey X and Ramsey Y experiment. - """ - opt = self.experiment_options # alias - param = Parameter("delay") - - # Pulse gates - stark_v = Gate("StarkV", 1, []) - stark_u = Gate("StarkU", 1, [param]) - - # Note that Stark tone yields negative (positive) frequency shift - # when the Stark tone frequency is higher (lower) than qubit f01 frequency. - # This choice gives positive frequency shift with positive Stark amplitude. - qubit_f01 = self._backend_data.drive_freqs[self.physical_qubits[0]] - stark_freq = qubit_f01 - np.sign(opt.stark_amp) * opt.stark_freq_offset - stark_amp = np.abs(opt.stark_amp) - stark_channel = opt.stark_channel or pulse.DriveChannel(self.physical_qubits[0]) - ramps_dt = self._timing.round_pulse(time=2 * opt.stark_risefall * opt.stark_sigma) - sigma_dt = ramps_dt / 2 / opt.stark_risefall - - with pulse.build() as stark_v_schedule: - pulse.set_frequency(stark_freq, stark_channel) - pulse.play( - pulse.Gaussian( - duration=ramps_dt, - amp=stark_amp, - sigma=sigma_dt, - name="StarkV", - ), - stark_channel, - ) - - with pulse.build() as stark_u_schedule: - pulse.set_frequency(stark_freq, stark_channel) - pulse.play( - pulse.GaussianSquare( - duration=ramps_dt + param, - amp=stark_amp, - sigma=sigma_dt, - risefall_sigma_ratio=opt.stark_risefall, - name="StarkU", - ), - stark_channel, - ) - - ram_x = QuantumCircuit(1, 1) - ram_x.sx(0) - ram_x.append(stark_v, [0]) - ram_x.x(0) - ram_x.append(stark_u, [0]) - ram_x.rz(-np.pi, 0) - ram_x.sx(0) - ram_x.measure(0, 0) - ram_x.metadata = {"series": "X"} - ram_x.add_calibration( - gate=stark_v, - qubits=self.physical_qubits, - schedule=stark_v_schedule, - ) - ram_x.add_calibration( - gate=stark_u, - qubits=self.physical_qubits, - schedule=stark_u_schedule, - ) - - ram_y = QuantumCircuit(1, 1) - ram_y.sx(0) - ram_y.append(stark_v, [0]) - ram_y.x(0) - ram_y.append(stark_u, [0]) - ram_y.rz(-np.pi * 3 / 2, 0) - ram_y.sx(0) - ram_y.measure(0, 0) - ram_y.metadata = {"series": "Y"} - ram_y.add_calibration( - gate=stark_v, - qubits=self.physical_qubits, - schedule=stark_v_schedule, - ) - ram_y.add_calibration( - gate=stark_u, - qubits=self.physical_qubits, - schedule=stark_u_schedule, - ) - - return ram_x, ram_y - - def circuits(self) -> list[QuantumCircuit]: - """Create circuits. - - Returns: - A list of circuits with a variable delay. - """ - timing = BackendTiming(self.backend, min_length=0) - - ramx_circ, ramy_circ = self.parameterized_circuits() - param = next(iter(ramx_circ.parameters)) - - circs = [] - for delay in self.parameters(): - valid_delay_dt = timing.round_pulse(time=delay) - net_delay_sec = timing.pulse_time(time=delay) - - ramx_circ_assigned = ramx_circ.assign_parameters({param: valid_delay_dt}, inplace=False) - ramx_circ_assigned.metadata["xval"] = net_delay_sec - - ramy_circ_assigned = ramy_circ.assign_parameters({param: valid_delay_dt}, inplace=False) - ramy_circ_assigned.metadata["xval"] = net_delay_sec - - circs.extend([ramx_circ_assigned, ramy_circ_assigned]) - - return circs - - def _metadata(self) -> dict[str, any]: - """Return experiment metadata for ExperimentData.""" - metadata = super()._metadata() - metadata["stark_amp"] = self.experiment_options.stark_amp - metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset - - return metadata diff --git a/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py deleted file mode 100644 index 494718e08d..0000000000 --- a/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py +++ /dev/null @@ -1,329 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Stark Ramsey experiment directly scanning Stark amplitude.""" - -from __future__ import annotations - -import warnings -from collections.abc import Sequence - -import numpy as np -from qiskit import pulse -from qiskit.circuit import QuantumCircuit, Gate, ParameterExpression, Parameter -from qiskit.providers.backend import Backend -from qiskit.utils import optionals as _optional -from qiskit.utils.deprecation import deprecate_func - -from qiskit_experiments.framework import BaseExperiment, Options, BackendTiming -from .ramsey_amp_scan_analysis import StarkRamseyXYAmpScanAnalysis - -if _optional.HAS_SYMENGINE: - import symengine as sym -else: - import sympy as sym - - -class StarkRamseyXYAmpScan(BaseExperiment): - r"""A fast characterization of Stark frequency shift by varying Stark tone amplitude. - - # section: overview - - This experiment scans Stark tone amplitude at a fixed tone duration. - The experimental circuits are identical to the :class:`.StarkRamseyXY` experiment - except that the Stark pulse amplitude is the scanned parameter rather than the pulse width. - - .. parsed-literal:: - - (Ramsey X) The pulse before measurement rotates by pi-half around the X axis - - ┌────┐┌───────────────────┐┌───┐┌───────────────────┐┌────────┐┌────┐┌─┐ - q: ┤ √X ├┤ StarkV(stark_amp) ├┤ X ├┤ StarkU(stark_amp) ├┤ Rz(-π) ├┤ √X ├┤M├ - └────┘└───────────────────┘└───┘└───────────────────┘└────────┘└────┘└╥┘ - c: 1/══════════════════════════════════════════════════════════════════════╩═ - 0 - - (Ramsey Y) The pulse before measurement rotates by pi-half around the Y axis - - ┌────┐┌───────────────────┐┌───┐┌───────────────────┐┌───────────┐┌────┐┌─┐ - q: ┤ √X ├┤ StarkV(stark_amp) ├┤ X ├┤ StarkU(stark_amp) ├┤ Rz(-3π/2) ├┤ √X ├┤M├ - └────┘└───────────────────┘└───┘└───────────────────┘└───────────┘└────┘└╥┘ - c: 1/═════════════════════════════════════════════════════════════════════════╩═ - 0 - - The AC Stark effect can be used to shift the frequency of a qubit with a microwave drive. - To calibrate a specific frequency shift, the :class:`.StarkRamseyXY` experiment can be run - to scan the Stark pulse duration at every amplitude, but such a two dimensional scan of - the tone duration and amplitude may require many circuit executions. - To avoid this overhead, the :class:`.StarkRamseyXYAmpScan` experiment fixes the - tone duration and scans only amplitude. - - Recall that an observed Ramsey oscillation in each quadrature may be represented by - - .. math:: - - {\cal E}_X(\Omega, t_S) = A e^{-t_S/\tau} \cos \left( 2\pi f_S(\Omega) t_S \right), \\ - {\cal E}_Y(\Omega, t_S) = A e^{-t_S/\tau} \sin \left( 2\pi f_S(\Omega) t_S \right), - - where :math:`f_S(\Omega)` denotes the amount of Stark shift - at a constant tone amplitude :math:`\Omega`, and :math:`t_S` is the duration of the - applied tone. For a fixed tone duration, - one can still observe the Ramsey oscillation by scanning the tone amplitude. - However, since :math:`f_S` is usually a higher order polynomial of :math:`\Omega`, - one must manage to fit the y-data for trigonometric functions with - phase which non-linearly changes with the x-data. - The :class:`.StarkRamseyXYAmpScan` experiment thus drastically reduces the number of - circuits to run in return for greater complexity in the fitting model. - - # section: analysis_ref - :class:`qiskit_experiments.library.driven_freq_tuning.StarkRamseyXYAmpScanAnalysis` - - # section: see_also - :class:`qiskit_experiments.library.driven_freq_tuning.ramsey.StarkRamseyXY` - :class:`qiskit_experiments.library.characterization.ramsey_xy.RamseyXY` - - # section: manual - :doc:`/manuals/characterization/stark_experiment` - """ - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments involving pulse " - "gate calibrations like this one have been deprecated." - ), - ) - def __init__( - self, - physical_qubits: Sequence[int], - backend: Backend | None = None, - **experiment_options, - ): - """Create new experiment. - - Args: - physical_qubits: Sequence with the index of the physical qubit. - backend: Optional, the backend to run the experiment on. - experiment_options: Experiment options. See the class documentation or - ``self._default_experiment_options`` for descriptions. - """ - self._timing = None - - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - message="deprecation of Qiskit Pulse", - module="qiskit_experiments", - category=DeprecationWarning, - ) - analysis = StarkRamseyXYAmpScanAnalysis() - - super().__init__( - physical_qubits=physical_qubits, - analysis=analysis, - backend=backend, - ) - self.set_experiment_options(**experiment_options) - - @classmethod - def _default_experiment_options(cls) -> Options: - """Default experiment options. - - Experiment Options: - stark_channel (PulseChannel): Pulse channel on which to apply Stark tones. - If not provided, the same channel with the qubit drive is used. - See :ref:`stark_channel_consideration` for details. - stark_freq_offset (float): Offset of Stark tone frequency from the qubit frequency. - This offset should be sufficiently large so that the Stark pulse - does not Rabi drive the qubit. - See :ref:`stark_frequency_consideration` for details. - stark_sigma (float): Gaussian sigma of the rising and falling edges - of the Stark tone, in seconds. - stark_risefall (float): Ratio of sigma to the duration of - the rising and falling edges of the Stark tone. - stark_length (float): Time to accumulate Stark shifted phase in seconds. - min_stark_amp (float): Minimum Stark tone amplitude. - max_stark_amp (float): Maximum Stark tone amplitude. - num_stark_amps (int): Number of Stark tone amplitudes to scan. - stark_amps (list[float]): The list of amplitude that will be scanned in the experiment. - If not set, then ``num_stark_amps`` evenly spaced amplitudes - between ``min_stark_amp`` and ``max_stark_amp`` are used. If ``stark_amps`` - is set, these parameters are ignored. - """ - options = super()._default_experiment_options() - options.update_options( - stark_channel=None, - stark_freq_offset=80e6, - stark_sigma=15e-9, - stark_risefall=2, - stark_length=50e-9, - min_stark_amp=-1.0, - max_stark_amp=1.0, - num_stark_amps=101, - stark_amps=None, - ) - options.set_validator("stark_freq_offset", (0, np.inf)) - options.set_validator("stark_channel", pulse.channels.PulseChannel) - return options - - def _set_backend(self, backend: Backend): - super()._set_backend(backend) - self._timing = BackendTiming(backend) - - def parameters(self) -> np.ndarray: - """Stark tone amplitudes to use in circuits. - - Returns: - The list of amplitudes to use for the different circuits based on the - experiment options. - """ - opt = self.experiment_options # alias - - if opt.stark_amps is None: - params = np.linspace(opt.min_stark_amp, opt.max_stark_amp, opt.num_stark_amps) - else: - params = np.asarray(opt.stark_amps, dtype=float) - - return params - - def parameterized_circuits(self) -> tuple[QuantumCircuit, ...]: - """Create circuits with parameters for Ramsey XY experiment with Stark tone. - - Returns: - Quantum template circuits for Ramsey X and Ramsey Y experiment. - """ - opt = self.experiment_options # alias - param = Parameter("stark_amp") - sym_param = param._symbol_expr - - # Pulse gates - stark_v = Gate("StarkV", 1, [param]) - stark_u = Gate("StarkU", 1, [param]) - - # Note that Stark tone yields negative (positive) frequency shift - # when the Stark tone frequency is higher (lower) than qubit f01 frequency. - # This choice gives positive frequency shift with positive Stark amplitude. - qubit_f01 = self._backend_data.drive_freqs[self.physical_qubits[0]] - neg_sign_of_amp = ParameterExpression( - symbol_map={param: sym_param}, - expr=-sym.sign(sym_param), - ) - abs_of_amp = ParameterExpression( - symbol_map={param: sym_param}, - expr=sym.Abs(sym_param), - ) - stark_freq = qubit_f01 + neg_sign_of_amp * opt.stark_freq_offset - stark_channel = opt.stark_channel or pulse.DriveChannel(self.physical_qubits[0]) - ramps_dt = self._timing.round_pulse(time=2 * opt.stark_risefall * opt.stark_sigma) - sigma_dt = ramps_dt / 2 / opt.stark_risefall - width_dt = self._timing.round_pulse(time=opt.stark_length) - - with pulse.build() as stark_v_schedule: - pulse.set_frequency(stark_freq, stark_channel) - pulse.play( - pulse.Gaussian( - duration=ramps_dt, - amp=abs_of_amp, - sigma=sigma_dt, - ), - stark_channel, - ) - - with pulse.build() as stark_u_schedule: - pulse.set_frequency(stark_freq, stark_channel) - pulse.play( - pulse.GaussianSquare( - duration=ramps_dt + width_dt, - amp=abs_of_amp, - sigma=sigma_dt, - risefall_sigma_ratio=opt.stark_risefall, - ), - stark_channel, - ) - - ram_x = QuantumCircuit(1, 1) - ram_x.sx(0) - ram_x.append(stark_v, [0]) - ram_x.x(0) - ram_x.append(stark_u, [0]) - ram_x.rz(-np.pi, 0) - ram_x.sx(0) - ram_x.measure(0, 0) - ram_x.metadata = {"series": "X"} - ram_x.add_calibration( - gate=stark_v, - qubits=self.physical_qubits, - schedule=stark_v_schedule, - ) - ram_x.add_calibration( - gate=stark_u, - qubits=self.physical_qubits, - schedule=stark_u_schedule, - ) - - ram_y = QuantumCircuit(1, 1) - ram_y.sx(0) - ram_y.append(stark_v, [0]) - ram_y.x(0) - ram_y.append(stark_u, [0]) - ram_y.rz(-np.pi * 3 / 2, 0) - ram_y.sx(0) - ram_y.measure(0, 0) - ram_y.metadata = {"series": "Y"} - ram_y.add_calibration( - gate=stark_v, - qubits=self.physical_qubits, - schedule=stark_v_schedule, - ) - ram_y.add_calibration( - gate=stark_u, - qubits=self.physical_qubits, - schedule=stark_u_schedule, - ) - - return ram_x, ram_y - - def circuits(self) -> list[QuantumCircuit]: - """Create circuits. - - Returns: - A list of circuits with a variable Stark tone amplitudes. - """ - ramx_circ, ramy_circ = self.parameterized_circuits() - param = next(iter(ramx_circ.parameters)) - - circs = [] - for amp in self.parameters(): - # Add metadata "direction" to ease the filtering of the data - # by curve analysis. Indeed, the fit parameters are amplitude sign dependent. - - ramx_circ_assigned = ramx_circ.assign_parameters({param: amp}, inplace=False) - ramx_circ_assigned.metadata["xval"] = amp - ramx_circ_assigned.metadata["direction"] = "pos" if amp > 0 else "neg" - - ramy_circ_assigned = ramy_circ.assign_parameters({param: amp}, inplace=False) - ramy_circ_assigned.metadata["xval"] = amp - ramy_circ_assigned.metadata["direction"] = "pos" if amp > 0 else "neg" - - circs.extend([ramx_circ_assigned, ramy_circ_assigned]) - - return circs - - def _metadata(self) -> dict[str, any]: - """Return experiment metadata for ExperimentData.""" - metadata = super()._metadata() - metadata["stark_length"] = self._timing.pulse_time( - time=self.experiment_options.stark_length - ) - metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset - - return metadata diff --git a/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py deleted file mode 100644 index fb24772514..0000000000 --- a/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py +++ /dev/null @@ -1,452 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Ramsey amplitude scan analysis.""" - -from __future__ import annotations - -from typing import List, Union - -import lmfit -import numpy as np -from uncertainties import unumpy as unp - -from qiskit.utils.deprecation import deprecate_func - -import qiskit_experiments.curve_analysis as curve -import qiskit_experiments.visualization as vis -from qiskit_experiments.framework import ExperimentData, AnalysisResultData -from .coefficients import StarkCoefficients - - -class StarkRamseyXYAmpScanAnalysis(curve.CurveAnalysis): - r"""Ramsey XY analysis for the Stark shifted phase sweep. - - # section: overview - - This analysis is a variant of :class:`RamseyXYAnalysis`. In both cases, the X and Y - data are treated as the real and imaginary parts of a complex oscillating signal. - In :class:`RamseyXYAnalysis`, the data are fit assuming a phase varying linearly with - the x-data corresponding to a constant frequency and assuming an exponentially - decaying amplitude. By contrast, in this model, the phase is assumed to be - a third order polynomial :math:`\theta(x)` of the x-data. - Additionally, the amplitude is not assumed to follow a specific form. - Techniques to compute a good initial guess for the polynomial coefficients inside - a trigonometric function like this are not trivial. Instead, this analysis extracts the - raw phase and runs fits on the extracted data to a polynomial :math:`\theta(x)` directly. - - The measured P1 values for a Ramsey X and Y experiment can be written in the form of - a trignometric function taking the phase polynomial :math:`\theta(x)`: - - .. math:: - - P_X = \text{amp}(x) \cdot \cos \theta(x) + \text{offset},\\ - P_Y = \text{amp}(x) \cdot \sin \theta(x) + \text{offset}. - - Hence the phase polynomial can be extracted as follows - - .. math:: - - \theta(x) = \tan^{-1} \frac{P_Y}{P_X}. - - Because the arctangent is implemented by the ``atan2`` function - defined in :math:`[-\pi, \pi]`, the computed :math:`\theta(x)` is unwrapped to - ensure continuous phase evolution. - - We call attention to the fact that :math:`\text{amp}(x)` is also Stark tone amplitude - dependent because of the qubit frequency dependence of the dephasing rate. - In general :math:`\text{amp}(x)` is unpredictable due to dephasing from - two-level systems distributed randomly in frequency - or potentially due to qubit heating. This prevents us from precisely fitting - the raw :math:`P_X`, :math:`P_Y` data. Fitting only the phase data makes the - analysis robust to amplitude dependent dephasing. - - In this analysis, the phase polynomial is defined as - - .. math:: - - \theta(x) = 2 \pi t_S f_S(x) - - where - - .. math:: - - f_S(x) = c_1 x + c_2 x^2 + c_3 x^3 + f_{\rm err}, - - denotes the Stark shift. For the lowest order perturbative expansion of a single driven qubit, - the Stark shift is a quadratic function of :math:`x`, but linear and cubic terms - and a constant offset are also considered to account for - other effects, e.g. strong drive, collisions, TLS, and so forth, - and frequency mis-calibration, respectively. - - # section: fit_model - - .. math:: - - \theta^\nu(x) = c_1^\nu x + c_2^\nu x^2 + c_3^\nu x^3 + f_{\rm err}, - - where :math:`\nu \in \{+, -\}`. - The Stark shift is asymmetric with respect to :math:`x=0`, because of the - anti-crossings of higher energy levels. In a typical transmon qubit, - these levels appear only in :math:`f_S < 0` because of the negative anharmonicity. - To precisely fit the results, this analysis uses different model parameters - for positive (:math:`x > 0`) and negative (:math:`x < 0`) shift domains. - - # section: fit_parameters - - defpar c_1^+: - desc: The linear term coefficient of the positive Stark shift - (fit parameter: ``stark_pos_coef_o1``). - init_guess: 0. - bounds: None - - defpar c_2^+: - desc: The quadratic term coefficient of the positive Stark shift. - This parameter must be positive because Stark amplitude is chosen to - induce blue shift when its sign is positive. - Note that the quadratic term is the primary term - (fit parameter: ``stark_pos_coef_o2``). - init_guess: 1e6. - bounds: [0, inf] - - defpar c_3^+: - desc: The cubic term coefficient of the positive Stark shift - (fit parameter: ``stark_pos_coef_o3``). - init_guess: 0. - bounds: None - - defpar c_1^-: - desc: The linear term coefficient of the negative Stark shift. - (fit parameter: ``stark_neg_coef_o1``). - init_guess: 0. - bounds: None - - defpar c_2^-: - desc: The quadratic term coefficient of the negative Stark shift. - This parameter must be negative because Stark amplitude is chosen to - induce red shift when its sign is negative. - Note that the quadratic term is the primary term - (fit parameter: ``stark_neg_coef_o2``). - init_guess: -1e6. - bounds: [-inf, 0] - - defpar c_3^-: - desc: The cubic term coefficient of the negative Stark shift - (fit parameter: ``stark_neg_coef_o3``). - init_guess: 0. - bounds: None - - defpar f_{\rm err}: - desc: Constant phase accumulation which is independent of the Stark tone amplitude. - (fit parameter: ``stark_ferr``). - init_guess: 0 - bounds: None - - # section: see_also - - :class:`qiskit_experiments.library.characterization.analysis.ramsey_xy_analysis.RamseyXYAnalysis` - - """ - - @deprecate_func( - since="0.8", - package_name="qiskit-experiments", - additional_msg=( - "Due to the deprecation of Qiskit Pulse, experiments and related classses " - "involving pulse gate calibrations like this one have been deprecated." - ), - ) - def __init__(self): - - models = [ - lmfit.models.ExpressionModel( - expr="c1_pos * x + c2_pos * x**2 + c3_pos * x**3 + f_err", - name="FREQpos", - ), - lmfit.models.ExpressionModel( - expr="c1_neg * x + c2_neg * x**2 + c3_neg * x**3 + f_err", - name="FREQneg", - ), - ] - super().__init__(models=models) - - @classmethod - def _default_options(cls): - """Default analysis options. - - Analysis Options: - pulse_len (float): Duration of effective Stark pulse in units of sec. - """ - ramsey_plotter = vis.CurvePlotter(vis.MplDrawer()) - ramsey_plotter.set_figure_options( - xlabel="Stark tone amplitude", - ylabel=["Stark shift", "P1"], - yval_unit=["Hz", None], - series_params={ - "Fpos": { - "color": "#123FE8", - "symbol": "^", - "label": "", - "canvas": 0, - }, - "Fneg": { - "color": "#123FE8", - "symbol": "v", - "label": "", - "canvas": 0, - }, - "Xpos": { - "color": "#123FE8", - "symbol": "o", - "label": "Ramsey X", - "canvas": 1, - }, - "Ypos": { - "color": "#6312E8", - "symbol": "^", - "label": "Ramsey Y", - "canvas": 1, - }, - "Xneg": { - "color": "#E83812", - "symbol": "o", - "label": "Ramsey X", - "canvas": 1, - }, - "Yneg": { - "color": "#E89012", - "symbol": "^", - "label": "Ramsey Y", - "canvas": 1, - }, - }, - sharey=False, - ) - ramsey_plotter.set_options(subplots=(2, 1), style=vis.PlotStyle({"figsize": (10, 8)})) - - options = super()._default_options() - options.update_options( - data_subfit_map={ - "Xpos": {"series": "X", "direction": "pos"}, - "Ypos": {"series": "Y", "direction": "pos"}, - "Xneg": {"series": "X", "direction": "neg"}, - "Yneg": {"series": "Y", "direction": "neg"}, - }, - plotter=ramsey_plotter, - fit_category="freq", - pulse_len=None, - ) - - return options - - def _freq_phase_coef(self) -> float: - """Return a coefficient to convert frequency into phase value.""" - try: - return 2 * np.pi * self.options.pulse_len - except TypeError as ex: - raise TypeError( - "A float-value duration in units of sec of the Stark pulse must be provided. " - f"The pulse_len option value {self.options.pulse_len} is not valid." - ) from ex - - def _format_data( - self, - curve_data: curve.ScatterTable, - category: str = "freq", - ) -> curve.ScatterTable: - - curve_data = super()._format_data(curve_data, category="ramsey_xy") - ramsey_xy = curve_data.filter(category="ramsey_xy") - y_mean = ramsey_xy.y.mean() - - # Create phase data by arctan(Y/X) - for data_id, direction in enumerate(("pos", "neg")): - x_quadrature = ramsey_xy.filter(series=f"X{direction}") - y_quadrature = ramsey_xy.filter(series=f"Y{direction}") - if not np.array_equal(x_quadrature.x, y_quadrature.x): - raise ValueError( - "Amplitude values of X and Y quadrature are different. " - "Same values must be used." - ) - x_uarray = unp.uarray(x_quadrature.y, x_quadrature.y_err) - y_uarray = unp.uarray(y_quadrature.y, y_quadrature.y_err) - amplitudes = x_quadrature.x - - # pylint: disable=no-member - phase = unp.arctan2(y_uarray - y_mean, x_uarray - y_mean) - phase_n = unp.nominal_values(phase) - phase_s = unp.std_devs(phase) - - # Unwrap phase - # We assume a smooth slope and correct 2pi phase jump to minimize the change of the slope. - unwrapped_phase = np.unwrap(phase_n) - if amplitudes[0] < 0: - # Preserve phase value closest to 0 amplitude - unwrapped_phase = unwrapped_phase + (phase_n[-1] - unwrapped_phase[-1]) - - # Store new data - unwrapped_phase /= self._freq_phase_coef() - phase_s /= self._freq_phase_coef() - shot_sums = x_quadrature.shots + y_quadrature.shots - for new_x, new_y, new_y_err, shot in zip( - amplitudes, unwrapped_phase, phase_s, shot_sums - ): - curve_data.add_row( - xval=new_x, - yval=new_y, - yerr=new_y_err, - series_name=f"FREQ{direction}", - series_id=data_id, - shots=shot, - category=category, - analysis=self.name, - ) - - return curve_data - - def _generate_fit_guesses( - self, - user_opt: curve.FitOptions, - curve_data: curve.ScatterTable, - ) -> Union[curve.FitOptions, List[curve.FitOptions]]: - """Create algorithmic initial fit guess from analysis options and curve data. - - Args: - user_opt: Fit options filled with user provided guess and bounds. - curve_data: Formatted data collection to fit. - - Returns: - List of fit options that are passed to the fitter function. - """ - user_opt.bounds.set_if_empty(c2_pos=(0, np.inf), c2_neg=(-np.inf, 0)) - user_opt.p0.set_if_empty( - c1_pos=0, c2_pos=1e6, c3_pos=0, c1_neg=0, c2_neg=-1e6, c3_neg=0, f_err=0 - ) - return user_opt - - def _create_analysis_results( - self, - fit_data: curve.CurveFitResult, - quality: str, - **metadata, - ) -> List[AnalysisResultData]: - outcomes = super()._create_analysis_results(fit_data, quality, **metadata) - - # Combine fit coefficients - coeffs = StarkCoefficients( - pos_coef_o1=fit_data.ufloat_params["c1_pos"].nominal_value, - pos_coef_o2=fit_data.ufloat_params["c2_pos"].nominal_value, - pos_coef_o3=fit_data.ufloat_params["c3_pos"].nominal_value, - neg_coef_o1=fit_data.ufloat_params["c1_neg"].nominal_value, - neg_coef_o2=fit_data.ufloat_params["c2_neg"].nominal_value, - neg_coef_o3=fit_data.ufloat_params["c3_neg"].nominal_value, - offset=fit_data.ufloat_params["f_err"].nominal_value, - ) - outcomes.append( - AnalysisResultData( - name="stark_coefficients", - value=coeffs, - chisq=fit_data.reduced_chisq, - quality=quality, - extra=metadata, - ) - ) - return outcomes - - def _create_figures( - self, - curve_data: curve.ScatterTable, - ) -> List["matplotlib.figure.Figure"]: - - # plot unwrapped phase on first axis - for direction in ("pos", "neg"): - sub_data = curve_data.filter(series=f"FREQ{direction}", category="freq") - self.plotter.set_series_data( - series_name=f"F{direction}", - x_formatted=sub_data.x, - y_formatted=sub_data.y, - y_formatted_err=sub_data.y_err, - ) - - # plot raw RamseyXY plot on second axis - for name in ("Xpos", "Ypos", "Xneg", "Yneg"): - sub_data = curve_data.filter(series=name, category="ramsey_xy") - self.plotter.set_series_data( - series_name=name, - x_formatted=sub_data.x, - y_formatted=sub_data.y, - y_formatted_err=sub_data.y_err, - ) - - # find base and amplitude guess - ramsey_xy = curve_data.filter(category="ramsey_xy") - offset_guess = 0.5 * (np.min(ramsey_xy.y) + np.max(ramsey_xy.y)) - amp_guess = 0.5 * np.ptp(ramsey_xy.y) - - # plot frequency and Ramsey fit lines - line_data = curve_data.filter(category="fitted") - for direction in ("pos", "neg"): - sub_data = line_data.filter(series=f"FREQ{direction}") - if len(sub_data) == 0: - continue - xval = sub_data.x - yn = sub_data.y - ys = sub_data.y_err - yval = unp.uarray(yn, ys) * self._freq_phase_coef() - - # Ramsey fit lines are predicted from the phase fit line. - # Note that this line doesn't need to match with the expeirment data - # because Ramsey P1 data may fluctuate due to phase damping. - - # pylint: disable=no-member - ramsey_cos = amp_guess * unp.cos(yval) + offset_guess - ramsey_sin = amp_guess * unp.sin(yval) + offset_guess - - self.plotter.set_series_data( - series_name=f"F{direction}", - x_interp=xval, - y_interp=yn, - ) - self.plotter.set_series_data( - series_name=f"X{direction}", - x_interp=xval, - y_interp=unp.nominal_values(ramsey_cos), - ) - self.plotter.set_series_data( - series_name=f"Y{direction}", - x_interp=xval, - y_interp=unp.nominal_values(ramsey_sin), - ) - - if np.isfinite(ys).all(): - self.plotter.set_series_data( - series_name=f"F{direction}", - y_interp_err=ys, - ) - self.plotter.set_series_data( - series_name=f"X{direction}", - y_interp_err=unp.std_devs(ramsey_cos), - ) - self.plotter.set_series_data( - series_name=f"Y{direction}", - y_interp_err=unp.std_devs(ramsey_sin), - ) - return [self.plotter.figure()] - - def _initialize( - self, - experiment_data: ExperimentData, - ): - super()._initialize(experiment_data) - - # Set scaling factor to convert phase to frequency - if "stark_length" in experiment_data.metadata: - self.set_options(pulse_len=experiment_data.metadata["stark_length"]) diff --git a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py index 5da2024973..ab37c3feb7 100644 --- a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py @@ -13,7 +13,6 @@ Interleaved RB Experiment class. """ import itertools -import warnings from typing import Union, Iterable, Optional, List, Sequence, Dict, Any from numpy.random import Generator @@ -168,9 +167,6 @@ def __init__( f" constraints of the backend {backend}. It could be {valid_duration}[dt]." " Use BackendTiming to set valid duration for delays." ) - # Warnings - if isinstance(interleaved_element, QuantumCircuit) and interleaved_element.calibrations: - warnings.warn("Calibrations in interleaved circuit are ignored", UserWarning) super().__init__( physical_qubits, diff --git a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py index 1d274ca550..44ce0d87b2 100644 --- a/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py +++ b/qiskit_experiments/library/randomized_benchmarking/layer_fidelity.py @@ -14,7 +14,6 @@ """ import functools import logging -from collections import defaultdict from typing import Union, Iterable, Optional, List, Sequence, Tuple, Dict from numpy.random import Generator, default_rng @@ -24,9 +23,8 @@ from qiskit.circuit.library import get_standard_gate_name_mapping from qiskit.exceptions import QiskitError from qiskit.providers import BackendV2Converter -from qiskit.providers.backend import Backend, BackendV1, BackendV2 +from qiskit.providers.backend import Backend, BackendV1 from qiskit.quantum_info import Clifford -from qiskit.pulse.instruction_schedule_map import CalibrationPublisher from qiskit_experiments.framework import BaseExperiment, Options from qiskit_experiments.framework.configs import ExperimentConfig @@ -476,30 +474,6 @@ def __circuit_body( def _transpiled_circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits, transpiled.""" transpiled = [_decompose_clifford_ops(circ) for circ in self.circuits()] - # Set custom calibrations provided in backend - if isinstance(self.backend, BackendV2): - instructions = [] # (op_name, qargs) for each element where qargs mean qubit tuple - for two_qubit_layer in self.experiment_options.two_qubit_layers: - for qpair in two_qubit_layer: - instructions.append((self.experiment_options.two_qubit_gate, tuple(qpair))) - for q in self.physical_qubits: - for gate_1q in self.experiment_options.one_qubit_basis_gates: - instructions.append((gate_1q, (q,))) - - common_calibrations = defaultdict(dict) - for op_name, qargs in instructions: - inst_prop = self.backend.target[op_name].get(qargs, None) - if inst_prop is None: - continue - schedule = inst_prop.calibration - if schedule is None: - continue - publisher = schedule.metadata.get("publisher", CalibrationPublisher.QISKIT) - if publisher != CalibrationPublisher.BACKEND_PROVIDER: - common_calibrations[op_name][(qargs, tuple())] = schedule - - for circ in transpiled: - circ.calibrations = common_calibrations return transpiled diff --git a/qiskit_experiments/library/randomized_benchmarking/standard_rb.py b/qiskit_experiments/library/randomized_benchmarking/standard_rb.py index 7c29059960..4e586bf505 100644 --- a/qiskit_experiments/library/randomized_benchmarking/standard_rb.py +++ b/qiskit_experiments/library/randomized_benchmarking/standard_rb.py @@ -27,7 +27,6 @@ from qiskit.exceptions import QiskitError from qiskit.providers import BackendV2Converter from qiskit.providers.backend import Backend, BackendV1, BackendV2 -from qiskit.pulse.instruction_schedule_map import CalibrationPublisher from qiskit.quantum_info import Clifford from qiskit.quantum_info.random import random_clifford from qiskit.transpiler import CouplingMap @@ -428,41 +427,6 @@ def _transpiled_circuits(self) -> List[QuantumCircuit]: _transpile_clifford_circuit(circ, physical_qubits=self.physical_qubits) for circ in self.circuits() ] - # Set custom calibrations provided in backend (excluding simulators) - if isinstance(self.backend, BackendV2) and "simulator" not in self.backend.name: - qargs_patterns = [self.physical_qubits] # for 1q or 3q+ case - if self.num_qubits == 2: - qargs_patterns = [ - (self.physical_qubits[0],), - (self.physical_qubits[1],), - self.physical_qubits, - (self.physical_qubits[1], self.physical_qubits[0]), - ] - - qargs_supported = self.backend.target.qargs - instructions = [] # (op_name, qargs) for each element where qargs means qubit tuple - for qargs in qargs_patterns: - if qargs in qargs_supported: - for op_name in self.backend.target.operation_names_for_qargs(qargs): - instructions.append((op_name, qargs)) - - common_calibrations = defaultdict(dict) - for op_name, qargs in instructions: - inst_prop = self.backend.target[op_name].get(qargs, None) - if inst_prop is None: - continue - schedule = inst_prop.calibration - if schedule is None: - continue - publisher = schedule.metadata.get("publisher", CalibrationPublisher.QISKIT) - if publisher != CalibrationPublisher.BACKEND_PROVIDER: - common_calibrations[op_name][(qargs, tuple())] = schedule - - for circ in transpiled: - # This logic is inefficient in terms of payload size and backend compilation - # because this binds every custom pulse to a circuit regardless of - # its existence. It works but redundant calibration must be removed -- NK. - circ.calibrations = common_calibrations if self.analysis.options.get("gate_error_ratio", None) is None: # Gate errors are not computed, then counting ops is not necessary. diff --git a/qiskit_experiments/test/mock_iq_backend.py b/qiskit_experiments/test/mock_iq_backend.py index 4436d05754..829b460bb0 100644 --- a/qiskit_experiments/test/mock_iq_backend.py +++ b/qiskit_experiments/test/mock_iq_backend.py @@ -18,6 +18,7 @@ import numpy as np from qiskit import QuantumCircuit +from qiskit.circuit import Gate from qiskit.circuit.library import XGate, SXGate from qiskit.result import Result from qiskit.providers import BackendV2, Provider, convert_to_target @@ -770,3 +771,212 @@ def run(self, run_input: List[QuantumCircuit], **run_options) -> FakeJob: result["results"].append(run_result) return FakeJob(self, Result.from_dict(result)) + + +class MockMultiStateBackend(FakeOpenPulse2QV2): + """A mock backend for testing with multi-state IQ data. + + .. note:: + + This backend does no simulation. It just looks for gates x, x12, and + x23 and sets the qubit state to the highest possible based on the + presence of these gates. + """ + + def __init__( + self, + iq_centers: list[complex], + iq_noise: float = 0.1, + state_noise: float = 0.0, + rng_seed: int = 0, + ): + """ + Initialize the backend. + + Args: + iq_centers: list of points in the complex plane corresponding to + different qubit levels. + iq_noise: Standard deviation of the normally distributed variation in + output around the IQ centers. + state_noise: Noise in the probability of the output state. For + example, 0.2 for a circuit with an x would mean 0.8 probability + of 1 and 0.2 probability 0 when iq_centers has length 2. + rng_seed(int): The random seed value. + """ + + if len(iq_centers) > 4: + raise ValueError("Only 4 qubit levels supported!") + self.iq_centers = iq_centers + self.iq_noise = iq_noise + self.state_noise = state_noise + self._rng = np.random.default_rng(rng_seed) + self.simulator = True + + super().__init__() + + if "x" not in self.target: + self.target.add_instruction(Gate("x", 1, [])) + self.target.add_instruction(Gate("x12", 1, [])) + self.target.add_instruction(Gate("x23", 1, [])) + + @classmethod + def _default_options(cls): + """Default options of the test backend.""" + return Options( + shots=1024, + meas_level=MeasLevel.KERNELED, + meas_return="single", + ) + + def compute_probabilities(self, circuits: List[QuantumCircuit]) -> List[List[float]]: + """Return the probability of being in the various states for each circuit""" + output_dict_list = [] + for circuit in circuits: + ops = circuit.count_ops() + if "x23" in ops: + idx = 3 + elif "x12" in ops: + idx = 2 + elif "x" in ops: + idx = 1 + else: + idx = 0 + + probability_outputs = self._rng.random(len(self.iq_centers)) + probability_outputs[idx] = 0.0 + prob_sum = sum(probability_outputs) + if prob_sum == 0: + probability_outputs[(idx + 1) % 2] = self.state_noise + else: + probability_outputs = self.state_noise * probability_outputs / prob_sum + + probability_outputs[idx] = 1 - self.state_noise + + output_dict_list.append(probability_outputs.tolist()) + + return output_dict_list + + @staticmethod + def _verify_parameters(output_length: int, prob_list: List[float]): + if output_length != 1: + raise ValueError( + f"The output length {output_length} is not 1 (only one measurement supported)." + ) + + if not np.allclose(1, sum(prob_list)): + raise ValueError("The probabilities given don't sum up to 1.") + + def _draw_iq_shots( + self, + prob: List[float], + shots: int, + ) -> List[List[float]]: + """ + Produce an IQ shot. + + Args: + prob: A list of probabilities for each output. + shots: The number of times the circuit will run. + Returns: + List[List[List[float]]]: A list of shots. Each shot consists of a + list of qubits (with 1 qubit only). The qubits are lists with two + values [I,Q]. The output structure is + + List[shot index][qubit index][I,Q] + """ + # Randomize samples (width=1) + samples = self.iq_noise * self._rng.normal(0, 1, size=(shots, 2)) + samples = samples[:, 0] + 1j * samples[:, 1] + samples = samples + self._rng.choice(self.iq_centers, size=(shots,), p=prob) + memory = [[[np.real(s), np.imag(s)]] for s in samples] + + return memory + + def _generate_data( + self, prob_list: Dict[str, float], circuit: QuantumCircuit + ) -> Dict[str, Any]: + """ + Generate data for the circuit. + + Args: + prob_list: A list with probabilities for different qubit states + circuit: The circuit that needs to be simulated. + + Returns: + A dictionary that's filled with the simulated data. The output format is different between + measurement level 1 and measurement level 2. + """ + # The output is proportional to the number of classical bit. + output_length = int(np.sum([creg.size for creg in circuit.cregs])) + self._verify_parameters(output_length, prob_list) + shots = self.options.get("shots") + meas_return = self.options.get("meas_return") + run_result = {} + + memory = self._draw_iq_shots( + prob_list, + shots, + ) + if meas_return == "avg": + memory = np.average(np.array(memory), axis=0).tolist() + + run_result["memory"] = memory + return run_result + + def run(self, run_input: List[QuantumCircuit], **run_options) -> FakeJob: + """ + Run the IQ backend. + + Args: + run_input: A list of QuantumCircuit for which the backend will generate + data. + **run_options: Experiment running options. The options that are supported + in this backend are `meas_level`, `meas_return` and `shots`: + + * meas_level: To generate data in the IQ plane, `meas_level` should be + assigned 1 or ``MeasLevel.KERNELED``. If `meas_level` is 2 or + ``MeasLevel.CLASSIFIED``, the generated data will be in the form + of `counts`. + * meas_return: This option will only take effect if `meas_level` = + ``MeasLevel.CLASSIFIED``. It can get either + ``MeasReturnType.AVERAGE`` or ``MeasReturnType.SINGLE``. For + ``MeasReturnType.SINGLE``, the data of each shot will be stored in + the result. For ``MeasReturnType.AVERAGE``, an average of all the + shots will be calculated and stored in the result. + * shots: The number of times the circuit will run. + + Returns: + FakeJob: A job that contains the simulated data. + + Raises: + QiskitError: Raised if the user try to run the experiment without setting a helper. + ValueError: Raised if ``meas_level`` in ``run_options`` is not 1. + """ + + self.options.update_options(**run_options) + shots = self.options.get("shots") + meas_level = self.options.get("meas_level", 1) + if meas_level != 1: + raise ValueError("Only level 1 data supported!") + + result = { + "backend_name": f"{self.__class__.__name__}", + "backend_version": "0", + "qobj_id": "0", + "job_id": "0", + "success": True, + "results": [], + } + prob_list = self.compute_probabilities(run_input) + for prob, circ in zip(prob_list, run_input): + run_result = { + "shots": shots, + "success": True, + "header": {"metadata": circ.metadata}, + "meas_level": meas_level, + } + + run_result["data"] = self._generate_data(prob, circ) + result["results"].append(run_result) + + return FakeJob(self, Result.from_dict(result)) diff --git a/qiskit_experiments/test/mock_iq_helpers.py b/qiskit_experiments/test/mock_iq_helpers.py index 9c2ed6d663..f7547005b4 100644 --- a/qiskit_experiments/test/mock_iq_helpers.py +++ b/qiskit_experiments/test/mock_iq_helpers.py @@ -14,7 +14,6 @@ from abc import abstractmethod from typing import Any, Dict, List, Optional, Tuple -import warnings import numpy as np from qiskit import QuantumCircuit from qiskit.exceptions import QiskitError @@ -434,71 +433,6 @@ def _parallel_exp_circ_splitter(self, qc_list: List[QuantumCircuit]): return exp_circuits_list -class MockIQDragHelper(MockIQExperimentHelper): - """Functions needed for test_drag""" - - def __init__( - self, - gate_name: str = "Rp", - ideal_beta: float = 2.0, - frequency: float = 0.02, - max_probability: float = 1.0, - offset_probability: float = 0.0, - iq_cluster_centers: Optional[List[Tuple[IQPoint, IQPoint]]] = None, - iq_cluster_width: Optional[List[float]] = None, - ): - """ - Args: - gate_name: name of the gate to count when determining the number of gate repetitions, - i.e., positive rotation followed by negative rotation, in the circuit. - ideal_beta: the beta where the minimum of the Drag patterns will be. - frequency: controls the frequency of the oscillation in the measured Drag pattern. - max_probability: a factor to scale the maximum probability of measuring an excited state to - allow tests to factor in non-ideal situations. - offset_probability: a constant offset applied to all probabilities to reflect non-ideal - measurement situations. - iq_cluster_centers: A list of tuples containing the clusters' centers in the IQ plane. There - are different centers for different logical values of the qubit. - iq_cluster_width: A list of standard deviation values for the sampling of each qubit. - Raises: - ValueError: If probability value is not valid. - """ - super().__init__(iq_cluster_centers, iq_cluster_width) - if max_probability + offset_probability > 1: - raise ValueError("Probabilities need to be between 0 and 1.") - - self.gate_name = gate_name - self.ideal_beta = ideal_beta - self.frequency = frequency - self.max_probability = max_probability - self.offset_probability = offset_probability - - def compute_probabilities(self, circuits: List[QuantumCircuit]) -> List[Dict[str, float]]: - """Returns the probability based on the beta, number of gates, and leakage.""" - - gate_name = self.gate_name - ideal_beta = self.ideal_beta - freq = self.frequency - max_prob = self.max_probability - offset_prob = self.offset_probability - - if max_prob + offset_prob > 1: - raise ValueError("Probabilities need to be between 0 and 1.") - - output_dict_list = [] - for circuit in circuits: - probability_output_dict = {} - n_gates = circuit.count_ops()[gate_name] - beta = next(iter(circuit.calibrations[gate_name].keys()))[1][0] - - # Dictionary of output string vectors and their probability - prob = np.sin(2 * np.pi * n_gates * freq * (beta - ideal_beta) / 4) ** 2 - probability_output_dict["1"] = max_prob * prob + offset_prob - probability_output_dict["0"] = 1 - probability_output_dict["1"] - output_dict_list.append(probability_output_dict) - return output_dict_list - - class MockIQFineDragHelper(MockIQExperimentHelper): """Functions needed for Fine Drag Experiment""" @@ -527,48 +461,6 @@ def compute_probabilities(self, circuits: List[QuantumCircuit]) -> List[Dict[str return output_dict_list -class MockIQRabiHelper(MockIQExperimentHelper): - """Functions needed for Rabi experiment on mock IQ backend""" - - def __init__( - self, - amplitude_to_angle: float = np.pi, - iq_cluster_centers: Optional[List[Tuple[IQPoint, IQPoint]]] = None, - iq_cluster_width: Optional[List[float]] = None, - ): - """ - Args: - amplitude_to_angle: maps a pulse amplitude to a rotation angle. - """ - warnings.warn( - "MockIQRabiHelper has been deprecated. It will be removed " - "in Qiskit Experiments 0.5.", - DeprecationWarning, - stacklevel=2, - ) - - super().__init__(iq_cluster_centers, iq_cluster_width) - self.amplitude_to_angle = amplitude_to_angle - - def compute_probabilities(self, circuits: List[QuantumCircuit]) -> List[Dict[str, float]]: - """Returns the probability based on the rotation angle and amplitude_to_angle.""" - amplitude_to_angle = self.amplitude_to_angle - output_dict_list = [] - for circuit in circuits: - probability_output_dict = {} - amp = next(iter(circuit.calibrations["Rabi"].keys()))[1][0] - - # Dictionary of output string vectors and their probability - probability_output_dict["1"] = np.sin(amplitude_to_angle * amp) ** 2 - probability_output_dict["0"] = 1 - probability_output_dict["1"] - output_dict_list.append(probability_output_dict) - return output_dict_list - - def rabi_rate(self) -> float: - """Returns the rabi rate.""" - return self.amplitude_to_angle / np.pi - - class MockIQFineFreqHelper(MockIQExperimentHelper): """Functions needed for Fine Frequency experiment on mock IQ backend""" @@ -711,66 +603,6 @@ def compute_probabilities(self, circuits: List[QuantumCircuit]) -> List[Dict[str return output_dict_list -class MockIQSpectroscopyHelper(MockIQExperimentHelper): - """Functions needed for Spectroscopy experiment on mock IQ backend""" - - def __init__( - self, - gate_name: str = "Spec", - freq_offset: float = 0.0, - line_width: float = 2e6, - iq_cluster_centers: Optional[List[Tuple[IQPoint, IQPoint]]] = None, - iq_cluster_width: Optional[List[float]] = None, - ): - """ - Args: - gate_name: the gate name to look for when calculating frequency shift. - freq_offset: frequency offset from resonance that this mock backend will mimic. - line_width: line width of the resonance of the spectroscopy signal. - """ - super().__init__(iq_cluster_centers, iq_cluster_width) - self.freq_offset = freq_offset - self.line_width = line_width - self.gate_name = gate_name - - def compute_probabilities(self, circuits: List[QuantumCircuit]) -> List[Dict[str, float]]: - """Returns the probability based on the parameters provided.""" - freq_offset = self.freq_offset - line_width = self.line_width - output_dict_list = [] - for circuit in circuits: - probability_output_dict = {} - if self.gate_name == "measure": - freq_shift = ( - next(iter(circuit.calibrations[self.gate_name].values())).blocks[0].frequency - ) - elif self.gate_name == "Spec": - freq_shift = next(iter(circuit.calibrations[self.gate_name]))[1][0] - else: - raise ValueError(f"The gate name {str(self.gate_name)} isn't supported.") - delta_freq = freq_shift - freq_offset - - probability_output_dict["1"] = np.abs(1 / (1 + 2.0j * delta_freq / line_width)) - probability_output_dict["0"] = 1 - probability_output_dict["1"] - output_dict_list.append(probability_output_dict) - return output_dict_list - - def iq_phase(self, circuits: List[QuantumCircuit]) -> List[float]: - """Add a phase to the IQ point depending on how far we are from the resonance. - This will cause the IQ points to rotate around in the IQ plane when we approach the - resonance, introducing extra complication that the data processor has to - properly handle. - """ - delta_freq_list = [0.0] * len(circuits) - if self.gate_name == "measure": - - for circ_idx, circ in enumerate(circuits): - freq_shift = next(iter(circ.calibrations["measure"].values())).blocks[0].frequency - delta_freq_list[circ_idx] = freq_shift - self.freq_offset - phase = [delta_freq / self.line_width for delta_freq in delta_freq_list] - return phase - - class MockIQReadoutAngleHelper(MockIQExperimentHelper): """Functions needed for Readout angle experiment on mock IQ backend""" diff --git a/qiskit_experiments/test/pulse_backend.py b/qiskit_experiments/test/pulse_backend.py index c41d0d9014..cbda46d788 100644 --- a/qiskit_experiments/test/pulse_backend.py +++ b/qiskit_experiments/test/pulse_backend.py @@ -18,25 +18,22 @@ import numpy as np +import qiskit.version from qiskit import QuantumCircuit from qiskit.circuit import CircuitInstruction from qiskit.circuit.library.standard_gates import RZGate, SXGate, XGate from qiskit.circuit.measure import Measure from qiskit.circuit.parameter import Parameter +from qiskit.exceptions import QiskitError from qiskit.providers import BackendV2, QubitProperties -from qiskit.providers.models import PulseDefaults # pylint: disable=no-name-in-module -from qiskit.providers.models.pulsedefaults import Command from qiskit.providers.options import Options -from qiskit.pulse import Schedule, ScheduleBlock -from qiskit.pulse.transforms import block_to_schedule -from qiskit.qobj.pulse_qobj import PulseQobjInstruction from qiskit.qobj.utils import MeasLevel, MeasReturnType from qiskit.quantum_info.states import DensityMatrix, Statevector from qiskit.result import Result, Counts from qiskit.transpiler import InstructionProperties, Target +from qiskit.utils import deprecate_func from qiskit_experiments.data_processing.discriminator import BaseDiscriminator -from qiskit_experiments.exceptions import QiskitError from qiskit_experiments.framework.package_deps import HAS_DYNAMICS, version_is_at_least from qiskit_experiments.test.utils import FakeJob @@ -64,6 +61,14 @@ class PulseBackend(BackendV2): experiment without having to run on hardware. """ + @deprecate_func( + since="0.9", + additional_msg=( + "Support for Qiskit Pulse has been discontinued. Pin to Qiskit 1 " + "and Qiskit Experiment 0.8 to use pulse experiments." + ), + package_name="qiskit-experiments", + ) def __init__( self, static_hamiltonian: np.ndarray, @@ -92,6 +97,10 @@ def __init__( """ from qiskit_dynamics import Solver + qiskit_version = qiskit.version.get_version_info() + if qiskit_version.partition(".") not in ("0", "1"): + raise QiskitError(f"Pulse backends are not compatible with Qiskit {qiskit_version}") + super().__init__( None, name="PulseBackendV2", @@ -326,16 +335,19 @@ def _state_to_measurement_data( return measurement_data, memory_data - def solve(self, schedule: Union[ScheduleBlock, Schedule], qubits: Tuple[int]) -> np.ndarray: + def solve(self, schedule, qubits: Tuple[int]) -> np.ndarray: """Solves for qubit dynamics under the action of a pulse instruction Args: - schedule: Pulse signal + schedule (Schedule | ScheduleBlock): Pulse signal qubits: (remove after multi-qubit gates is implemented) Returns: Time-evolution unitary operator """ + from qiskit.pulse import ScheduleBlock + from qiskit.pulse.transforms import block_to_schedule + if len(qubits) > 1: raise QiskitError("Multi qubit gates are not yet implemented.") @@ -458,6 +470,14 @@ class SingleTransmonTestBackend(PulseBackend): the raising and lowering operators between levels :math:`j-1` and :math:`j`. """ + @deprecate_func( + since="0.9", + additional_msg=( + "Support for Qiskit Pulse has been discontinued. Pin to Qiskit 1 " + "and Qiskit Experiment 0.8 to use pulse experiments." + ), + package_name="qiskit-experiments", + ) def __init__( self, qubit_frequency: float = 5e9, @@ -483,6 +503,9 @@ def __init__( atol: Absolute tolerance during solving. rtol: Relative tolerance during solving. """ + from qiskit.providers.models import PulseDefaults # pylint: disable=no-name-in-module + from qiskit.providers.models.pulsedefaults import Command + from qiskit.qobj.pulse_qobj import PulseQobjInstruction from qiskit_dynamics.pulse import InstructionToSignals qubit_frequency_02 = 2 * qubit_frequency + anharmonicity diff --git a/qiskit_experiments/test/zzramsey_test_backend.py b/qiskit_experiments/test/zzramsey_test_backend.py new file mode 100644 index 0000000000..7d3802ea3f --- /dev/null +++ b/qiskit_experiments/test/zzramsey_test_backend.py @@ -0,0 +1,216 @@ +# 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. +""" +ZZRamseyTestBackend class. + +Backend for testing the ZZRamsey experiment +""" +import copy +from typing import List, Optional, Sequence, Union + +import numpy as np + +from qiskit import QiskitError, QuantumCircuit +from qiskit.circuit import Delay, Reset, Parameter +from qiskit.circuit.library import Measure, RZGate, RZZGate, SXGate, XGate +from qiskit.dagcircuit import DAGCircuit +from qiskit.providers import BackendV2, Job, Options +from qiskit.transpiler import InstructionProperties, PassManager, Target, TransformationPass +from qiskit.utils.units import apply_prefix + +from qiskit_aer import AerSimulator +from qiskit_aer.noise import NoiseModel, ReadoutError, RelaxationNoisePass, reset_error + +from qiskit_experiments.test.utils import FakeJob + + +class ResetQubits(TransformationPass): + """Pass to inject reset instructions for each qubit + + The resets are used to add qubit initialization error. + """ + + def run(self, dag: DAGCircuit): + new_dag = copy.deepcopy(dag) + + for qreg in new_dag.qregs.values(): + new_dag.apply_operation_front(Reset(), qreg, []) + + return new_dag + + +class ZZDrift(TransformationPass): + """Pass to ZZ rotate qubits during delays + + This pass adds rotations that mimic the qubits undergoing ZZ rotations + during delays in the circuit. + + .. note:: + + This pass only adds ZZ for qubits [0, 1]. Also, it just adds an RZZ + after each delay on 0 or 1. If there are simultaneous delays, it will + double rotate. + """ + + def __init__(self, zz_frequency: float, dt: float): + super().__init__() + self.zz_frequency = zz_frequency + self.dt = dt + + def run(self, dag: DAGCircuit): + if len(dag.qubits) < 2: + return dag + + zz_qubits = dag.qubits[:2] + + new_dag = dag.copy_empty_like() + + for node in dag.topological_op_nodes(): + new_dag.apply_operation_back(node.op, node.qargs, node.cargs) + + if node.name == "delay" and node.qargs[0] in zz_qubits: + if node.op.unit == "dt": + duration = node.op.duration * self.dt + else: + duration = apply_prefix(node.op.duration, node.op.unit) + angle = 2 * np.pi * self.zz_frequency / 2 * duration + angle = angle % (2 * np.pi) + new_dag.apply_operation_back(RZZGate(angle), zz_qubits, []) + + return new_dag + + +class ZZRamseyTestBackend(BackendV2): + """ + A simple and primitive backend, to be run by the ZZRamsey tests + + .. note:: + + See the note on ZZDrift. This class is only intended for testing + ZZRamsey and only (kind of) works for qubits [0, 1]. + """ + + def __init__( + self, + t2hahn: Union[float, Sequence[float]] = float("inf"), + zz_frequency: float = 0.0, + initialization_error: Union[float, Sequence[float]] = 0.0, + readout0to1: Union[float, Sequence[float]] = 0.0, + readout1to0: Union[float, Sequence[float]] = 0.0, + seed: int = 9000, + dt: float = 1 / 4.5e9, + num_qubits: Optional[int] = None, + ): + """ + Initialize the T2Hahn backend + """ + super().__init__( + name="ZZRamsey_simulator", + backend_version="0", + ) + + for arg in (t2hahn, initialization_error, readout0to1, readout1to0): + if isinstance(arg, Sequence): + if num_qubits is None: + num_qubits = len(arg) + elif len(arg) != num_qubits: + raise ValueError( + f"Input lengths are not consistent: {num_qubits} != {len(arg)}" + ) + + if num_qubits is None: + num_qubits = 2 + + self._t2hahn = t2hahn if isinstance(t2hahn, Sequence) else [t2hahn] * num_qubits + self._zz_frequency = zz_frequency + self._initialization_error = ( + initialization_error + if isinstance(initialization_error, Sequence) + else [initialization_error] * num_qubits + ) + self._readout0to1 = ( + readout0to1 if isinstance(readout0to1, Sequence) else [readout0to1] * num_qubits + ) + self._readout1to0 = ( + readout1to0 if isinstance(readout1to0, Sequence) else [readout1to0] * num_qubits + ) + self._seed = seed + + self._target = Target(dt=dt, num_qubits=num_qubits) + for instruction in (Measure(), Reset(), RZGate(Parameter("angle")), SXGate(), XGate()): + self.target.add_instruction( + instruction, + properties={(q,): InstructionProperties(duration=0) for q in range(num_qubits)}, + ) + self.target.add_instruction( + RZZGate(Parameter("angle")), + properties={(0, 1): InstructionProperties(duration=0)}, + ) + self.target.add_instruction(Delay(Parameter("duration"))) + + @property + def target(self) -> Target: + return self._target + + @property + def max_circuits(self) -> None: + return None + + @classmethod + def _default_options(cls) -> Options: + return Options() + + def run( + self, run_input: Union[QuantumCircuit, List[QuantumCircuit]], shots: int = 1024, **options + ) -> Job: + passes = [] + + if isinstance(run_input, QuantumCircuit): + circuits = [run_input] + else: + circuits = run_input + + for circuit in circuits: + if circuit.num_qubits > self.num_qubits: + raise QiskitError( + f"{self.__class__} can only run circuits that match its num_qubits" + ) + + noise_model = NoiseModel() + + if self._initialization_error is not None: + passes.append(ResetQubits()) + for qubit, error in enumerate(self._initialization_error): + if error is None: + continue + noise_model.add_quantum_error(reset_error(1 - error, error), ["reset"], [qubit]) + + if any(self._readout0to1) or any(self._readout1to0): + for qubit, (err0to1, err1to0) in enumerate(zip(self._readout0to1, self._readout1to0)): + error = ReadoutError([[1 - err0to1, err0to1], [err1to0, 1 - err1to0]]) + noise_model.add_readout_error(error, [qubit]) + if any(t2 != float("inf") for t2 in self._t2hahn): + # Make T1 huge so only T2 matters + passes.append( + RelaxationNoisePass([t * 100 for t in self._t2hahn], self._t2hahn, self.dt, Delay) + ) + if self._zz_frequency: + passes.append(ZZDrift(self._zz_frequency, self.dt)) + + pm = PassManager(passes) + new_circuits = pm.run(circuits) + + sim = AerSimulator(noise_model=noise_model, seed_simulator=self._seed) + + job = sim.run(new_circuits, shots=shots, **options) + + return FakeJob(self, job.result()) diff --git a/releasenotes/notes/remove-pulse-deprecations-1abbc72dd1376ec2.yaml b/releasenotes/notes/remove-pulse-deprecations-1abbc72dd1376ec2.yaml new file mode 100644 index 0000000000..fff07255ec --- /dev/null +++ b/releasenotes/notes/remove-pulse-deprecations-1abbc72dd1376ec2.yaml @@ -0,0 +1,69 @@ +--- +upgrade: + - | + Previously deprecated experiments involving pulse gate calibrations have + been removed, due to the upcoming `removal of Qiskit Pulse in Qiskit + 2.0 `_. These experiments + include ``QubitSpectroscopy``, ``EFSpectroscopy``, ``Rabi``, ``EFRabi``, + ``ResonatorSpectroscopy``, ``RoughDrag``, ``StarkRamseyXY``, + ``StarkRamseyXYAmpScan``, ``StarkP1Spectroscopy``, + ``CrossResonanceHamiltonian``, ``EchoedCrossResonanceHamiltonian``, + ``BaseCalibrationExperiment``, + ``RoughFrequencyCal``, ``RoughEFFrequencyCal``, ``FrequencyCal``, + ``FineFrequencyCal``, ``RoughDragCal``, ``FineXDragCal``, + ``FineSXDragCal``, ``FineDragCal``, ``FineAmplitudeCal``, + ``FineXAmplitudeCal``, ``FineSXAmplitudeCal``, ``HalfAngleCal``, + ``RoughAmplitudeCal``, ``RoughXSXAmplitudeCal``, and + ``EFRoughXSXAmplitudeCal``. The associated analysis and helper classes like + ``ResonanceAnalysis``, ``CrossResonanceHamiltonianAnalysis``, + ``DragCalAnalysis``, ``ResonatorSpectroscopyAnalysis``, and + ``StarkCoefficients`` were also removed. + - | + Also due to the deprecation of Qiskit Pulse, support for providing pulse + gate calibrations to excite higher levels has been removed from + :class:`.MultiStateDiscrimination`. + - | + The ``Calibrations`` class and all of Qiskit Experiments' calibration + support (including gate libraries and calibration updaters and experiments) + has been removed. The calibrations features were based on adjusting + parameters of pulses used in gates. With the removal of pulse support in + Qiskit 2.0, it no longer was feasible to keep support for features that + rely on pulse gate calibrations. + - | + The methods of :class:`.BackendData` and :class:`.BackendTiming` that + involved pulse gate features have been removed. The removed methods were + ``control_channel``, ``drive_channel``, ``measure_channel``, + ``acquire_channel``, ``min_length``, ``pulse_alignment``, + ``acquire_alignment``, ``drive_freqs``, and ``meas_freqs`` of + ``BackendData`` and ``round_pulse`` and ``pulse_time`` of + ``BackendTiming``. + - | + The ability for :class:`.ExperimentEncoder` and :class:`.ExperimentDecoder` + to work with the pulse ``ScheduleBlock`` class has been removed. + - | + ``qiskit-dynamics`` has been removed from the ``extras`` extra of the + package. This means that ``pip install qiskit-experiments[extras]`` will no + longer install ``qiskit-dynamics``. :class:`.PulseBackend` and + :class:`.SingleTransmonTestBackend` still require ``qiskit-dynamics`` to be + installed, so it must be installed separately. The reason for this change + is that Qiskit Experiments aims to keep compatibility with the latest + Qiskit and so will not require a package that pins the Qiskit version while + Qiskit Dynamics plans to pin to Qiskit version 1. + +deprecation: + - | + :class:`.FineZXAmplitude` has been deprecated. It was not deprecated along + with the other pulse-related experiments in the previous Qiskit Experiments + release, but it requires an ``ZX`` rotation of :math:`\pi / 2` which is not + easily realizable on any known providers without pulse calibrations. + + - | + :class:`.PulseBackend` and :class:`.SingleTransmonTestBackend` have been + deprecated. These backend classes use `Qiskit Dyanmics + `_ to simulate quantum + circuits using Qiskit Pulse with pulse-level simulation and were mainly + intended for testing purposes. With the removal of pulse features in Qiskit + 2.0, these classes can no longer be maintained. Their deprecation was + missed in the previous round of pulse feature deprecation. While the + classes are kept for one more release cycle, they require Qiskit less than + 2.0 in order to function. diff --git a/requirements-dev.txt b/requirements-dev.txt index 2f524e0067..482501769d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,6 +18,7 @@ multimethod # options. qiskit-ibm-runtime>=0.34 # see above qiskit>=1.3 # see above +qiskit-dynamics>=0.4 # Needed by pylint for linting pulse_backend.py # Documentation tools arxiv diff --git a/requirements-extras.txt b/requirements-extras.txt index b95eeacbc0..6f2f19a46f 100644 --- a/requirements-extras.txt +++ b/requirements-extras.txt @@ -1,4 +1,3 @@ cvxpy>=1.3.2 # for tomography scikit-learn # for discriminators qiskit-aer>=0.13.2 -qiskit-dynamics>=0.4.0 # for the PulseBackend diff --git a/test/calibration/__init__.py b/test/calibration/__init__.py deleted file mode 100644 index 96c0cf22be..0000000000 --- a/test/calibration/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. diff --git a/test/calibration/test_base_calibration_experiment.py b/test/calibration/test_base_calibration_experiment.py deleted file mode 100644 index e924ff3992..0000000000 --- a/test/calibration/test_base_calibration_experiment.py +++ /dev/null @@ -1,368 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Tests for the base class for calibration-type experiments.""" - -from test.base import QiskitExperimentsTestCase - -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit.pulse import Play, Constant, DriveChannel, ScheduleBlock - -from qiskit_experiments.calibration_management.base_calibration_experiment import ( - BaseCalibrationExperiment, - Calibrations, -) -from qiskit_experiments.framework.composite import ParallelExperiment, BatchExperiment -from qiskit_experiments.library import QubitSpectroscopy -from qiskit_experiments.test.fake_backend import FakeBackend - -from .utils import MockCalExperiment, DoNothingAnalysis - - -class TestBaseCalibrationClass(QiskitExperimentsTestCase): - """Tests for base calibration experiment classes.""" - - def test_class_order(self): - """Test warnings when the BaseCalibrationExperiment is not the first parent.""" - - class CorrectOrder(BaseCalibrationExperiment, QubitSpectroscopy): - """A class with the correct order should not produce warnings..""" - - def __init__(self): - """A dummy class for parent order testing.""" - super().__init__(Calibrations(coupling_map=[]), [0], [0, 1, 2]) - - def _attach_calibrations(self, circuit): - """Needed as this method is abstract""" - pass - - CorrectOrder() - - with self.assertWarns(Warning): - - # pylint: disable=unused-variable - class WrongOrder(QubitSpectroscopy, BaseCalibrationExperiment): - """Merely defining this class is enough to raise the warning.""" - - def __init__(self): - """A dummy class for parent order testing.""" - super().__init__(Calibrations(coupling_map=[]), [0], [0, 1, 2]) - - def test_update_calibration(self): - """Test updating calibrations with execution of calibration experiment.""" - backend = FakeBackend() - ref_old_value = 0.1 - ref_new_value = 0.3 - - param = Parameter("to_calibrate") - schedule = ScheduleBlock(name="test") - schedule.append(Play(Constant(100, param), DriveChannel(0)), inplace=True) - cals = Calibrations() - cals.add_schedule(schedule, 0, 1) - - # Add init parameter to the cal table - cals.add_parameter_value( - value=ref_old_value, - param="to_calibrate", - qubits=(0,), - schedule="test", - ) - - # Get old value - old_value = cals.get_parameter_value("to_calibrate", (0,), "test") - - exp = MockCalExperiment( - physical_qubits=(0,), - calibrations=cals, - new_value=ref_new_value, - param_name="to_calibrate", - sched_name="test", - circuits=[QuantumCircuit(1)], - ) - self.assertExperimentDone(exp.run(backend)) - - # Get new value - new_value = cals.get_parameter_value("to_calibrate", (0,), "test") - self.assertNotEqual(old_value, new_value) - - # Validate calibrated schedule - new_schedule = cals.get_schedule("test", (0,)) - ref_schedule = schedule.assign_parameters({param: ref_new_value}, inplace=False) - self.assertEqual(new_schedule, ref_schedule) - - def test_update_calibration_update_analysis(self): - """Test updating calibrations with experiment with updated analysis option. - - This checks if the patched analysis instance is the same object. - """ - backend = FakeBackend() - ref_old_value = 0.1 - ref_new_value = 0.3 - - param = Parameter("to_calibrate") - schedule = ScheduleBlock(name="test") - schedule.append(Play(Constant(100, param), DriveChannel(0)), inplace=True) - cals = Calibrations() - cals.add_schedule(schedule, 0, 1) - - # Add init parameter to the cal table - cals.add_parameter_value( - value=ref_old_value, - param="to_calibrate", - qubits=(0,), - schedule="test", - ) - - # Get old value - old_value = cals.get_parameter_value("to_calibrate", (0,), "test") - - exp = MockCalExperiment( - physical_qubits=(0,), - calibrations=cals, - new_value=999999, - param_name="to_calibrate", - sched_name="test", - circuits=[QuantumCircuit(1)], - ) - exp.analysis.set_options(return_value=ref_new_value) # Update analysis option here - self.assertExperimentDone(exp.run(backend)) - - # Get new value - new_value = cals.get_parameter_value("to_calibrate", (0,), "test") - self.assertNotEqual(old_value, new_value) - - # Validate calibrated schedule - new_schedule = cals.get_schedule("test", (0,)) - ref_schedule = schedule.assign_parameters({param: ref_new_value}, inplace=False) - self.assertEqual(new_schedule, ref_schedule) - - def test_update_calibration_custom_analysis(self): - """Test updating calibrations with experiment instance with user analysis. - - This checks if the patch mechanism works for user provided analysis. - """ - backend = FakeBackend() - ref_old_value = 0.1 - ref_new_value = 0.3 - - param = Parameter("to_calibrate") - schedule = ScheduleBlock(name="test") - schedule.append(Play(Constant(100, param), DriveChannel(0)), inplace=True) - cals = Calibrations() - cals.add_schedule(schedule, 0, 1) - - # Add init parameter to the cal table - cals.add_parameter_value( - value=ref_old_value, - param="to_calibrate", - qubits=(0,), - schedule="test", - ) - - # Get old value - old_value = cals.get_parameter_value("to_calibrate", (0,), "test") - - exp = MockCalExperiment( - physical_qubits=(0,), - calibrations=cals, - new_value=99999, - param_name="to_calibrate", - sched_name="test", - circuits=[QuantumCircuit(1)], - ) - - user_analysis = DoNothingAnalysis() - user_analysis.set_options(return_value=ref_new_value) - exp.analysis = user_analysis # Update analysis instance itself here - self.assertExperimentDone(exp.run(backend)) - - # Get new value - new_value = cals.get_parameter_value("to_calibrate", (0,), "test") - self.assertNotEqual(old_value, new_value) - - # Validate calibrated schedule - new_schedule = cals.get_schedule("test", (0,)) - ref_schedule = schedule.assign_parameters({param: ref_new_value}, inplace=False) - self.assertEqual(new_schedule, ref_schedule) - - def test_update_calibration_batch(self): - """Test updating calibrations from batch experiment.""" - backend = FakeBackend() - ref_old_value1 = 120 - ref_new_value1 = 100 - ref_old_value2 = 0.2 - ref_new_value2 = 0.4 - - param1 = Parameter("to_calibrate1") - param2 = Parameter("to_calibrate2") - schedule = ScheduleBlock(name="test") - schedule.append(Play(Constant(param1, param2), DriveChannel(0)), inplace=True) - cals = Calibrations() - cals.add_schedule(schedule, 0, 1) - - # Add init parameter to the cal table - cals.add_parameter_value( - value=ref_old_value1, - param="to_calibrate1", - qubits=(0,), - schedule="test", - ) - cals.add_parameter_value( - value=ref_old_value2, - param="to_calibrate2", - qubits=(0,), - schedule="test", - ) - - # Get old value - old_value1 = cals.get_parameter_value("to_calibrate1", (0,), "test") - old_value2 = cals.get_parameter_value("to_calibrate2", (0,), "test") - - exp1 = MockCalExperiment( - physical_qubits=(0,), - calibrations=cals, - new_value=ref_new_value1, - param_name="to_calibrate1", - sched_name="test", - circuits=[QuantumCircuit(1)], - ) - exp2 = MockCalExperiment( - physical_qubits=(0,), - calibrations=cals, - new_value=ref_new_value2, - param_name="to_calibrate2", - sched_name="test", - circuits=[QuantumCircuit(1)], - ) - batch_exp = BatchExperiment([exp1, exp2], flatten_results=False, backend=backend) - self.assertExperimentDone(batch_exp.run(backend)) - - # Get new value - new_value1 = cals.get_parameter_value("to_calibrate1", (0,), "test") - self.assertNotEqual(old_value1, new_value1) - new_value2 = cals.get_parameter_value("to_calibrate2", (0,), "test") - self.assertNotEqual(old_value2, new_value2) - - # Validate calibrated schedule - new_schedule = cals.get_schedule("test", (0,)) - ref_schedule = schedule.assign_parameters( - { - param1: ref_new_value1, - param2: ref_new_value2, - }, - inplace=False, - ) - self.assertEqual(new_schedule, ref_schedule) - - def test_update_calibration_parallel(self): - """Test updating calibrations from parallel experiment.""" - backend = FakeBackend() - ref_old_value1 = 0.1 - ref_new_value1 = 0.3 - ref_old_value2 = 0.2 - ref_new_value2 = 0.4 - - param1 = Parameter("to_calibrate1") - param2 = Parameter("to_calibrate2") - schedule1 = ScheduleBlock(name="test1") - schedule1.append(Play(Constant(100, param1), DriveChannel(0)), inplace=True) - schedule2 = ScheduleBlock(name="test2") - schedule2.append(Play(Constant(100, param2), DriveChannel(1)), inplace=True) - cals = Calibrations() - cals.add_schedule(schedule1, 0, 1) - cals.add_schedule(schedule2, 1, 1) - - # Add init parameter to the cal table - cals.add_parameter_value( - value=ref_old_value1, - param="to_calibrate1", - qubits=(0,), - schedule="test1", - ) - cals.add_parameter_value( - value=ref_old_value2, - param="to_calibrate2", - qubits=(1,), - schedule="test2", - ) - - # Get old value - old_value1 = cals.get_parameter_value("to_calibrate1", (0,), "test1") - old_value2 = cals.get_parameter_value("to_calibrate2", (1,), "test2") - - exp1 = MockCalExperiment( - physical_qubits=(0,), - calibrations=cals, - new_value=ref_new_value1, - param_name="to_calibrate1", - sched_name="test1", - circuits=[QuantumCircuit(1)], - ) - exp2 = MockCalExperiment( - physical_qubits=(1,), - calibrations=cals, - new_value=ref_new_value2, - param_name="to_calibrate2", - sched_name="test2", - circuits=[QuantumCircuit(1)], - ) - batch_exp = ParallelExperiment([exp1, exp2], flatten_results=False, backend=backend) - self.assertExperimentDone(batch_exp.run(backend)) - - # Get new value - new_value1 = cals.get_parameter_value("to_calibrate1", (0,), "test1") - self.assertNotEqual(old_value1, new_value1) - new_value2 = cals.get_parameter_value("to_calibrate2", (1,), "test2") - self.assertNotEqual(old_value2, new_value2) - - # Validate calibrated schedules - new_schedule1 = cals.get_schedule("test1", (0,)) - ref_schedule1 = schedule1.assign_parameters({param1: ref_new_value1}, inplace=False) - self.assertEqual(new_schedule1, ref_schedule1) - - new_schedule2 = cals.get_schedule("test2", (1,)) - ref_schedule2 = schedule2.assign_parameters({param2: ref_new_value2}, inplace=False) - self.assertEqual(new_schedule2, ref_schedule2) - - def test_transpiled_circuits_no_coupling_map(self): - """Test transpilation of calibration experiment with no coupling map""" - # This test was added to catch errors found when running calibration - # experiments against DynamicsBackend from qiskit-dynamics for which - # the coupling map could be None. Previously, this led to - # BaseCalibrationExperiment's custom pass manager failing. - backend = FakeBackend(num_qubits=2) - # If the following fails, it should be reassessed if this test is still - # useful - self.assertTrue(backend.coupling_map is None) - - cals = Calibrations() - - # Build a circuit to be passed through transpilation pipeline - qc = QuantumCircuit(1, 1) - qc.x(0) - qc.measure(0, 0) - - exp = MockCalExperiment( - physical_qubits=(1,), - calibrations=cals, - new_value=0.2, - param_name="amp", - sched_name="x", - backend=backend, - circuits=[qc], - ) - transpiled = exp._transpiled_circuits()[0] - # Make sure circuit was expanded with the ancilla on qubit 0 - self.assertEqual(len(transpiled.qubits), 2) - # Make sure instructions were unchanged - self.assertDictEqual(transpiled.count_ops(), qc.count_ops()) diff --git a/test/calibration/test_calibration_utils.py b/test/calibration/test_calibration_utils.py deleted file mode 100644 index 54af6a5513..0000000000 --- a/test/calibration/test_calibration_utils.py +++ /dev/null @@ -1,168 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Class to test utility functions for calibrations.""" - -from test.base import QiskitExperimentsTestCase -import rustworkx as rx - -from qiskit import pulse -from qiskit.circuit import Parameter - -from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.calibration_management import EchoedCrossResonance -from qiskit_experiments.calibration_management.calibration_key_types import ScheduleKey -from qiskit_experiments.calibration_management.calibration_utils import ( - validate_channels, - used_in_references, - update_schedule_dependency, - CHANNEL_PATTERN_REGEX, -) - - -class TestScheduleDAG(QiskitExperimentsTestCase): - """Test the function in CalUtils.""" - - def setUp(self): - """Setup the tests.""" - super().setUp() - - with pulse.build(name="xp") as xp1: - pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(1)) - - with pulse.build(name="xp2") as xp2: - pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(1)) - - with pulse.build(name="ref_xp") as xp_ref: - pulse.reference(xp1.name, "q0") - - self.xp1 = xp1 - self.xp2 = xp2 - self.xp_ref = xp_ref - self.dag = rx.PyDiGraph(check_cycle=True) - - def test_used_in_references_simple(self): - """Test that schedule identification by name with simple references.""" - update_schedule_dependency(self.xp1, self.dag, ScheduleKey(self.xp1.name, tuple())) - update_schedule_dependency(self.xp2, self.dag, ScheduleKey(self.xp2.name, tuple())) - update_schedule_dependency(self.xp_ref, self.dag, ScheduleKey(self.xp_ref.name, tuple())) - - self.assertSetEqual(used_in_references({ScheduleKey("xp", tuple())}, self.dag), {"ref_xp"}) - self.assertSetEqual(used_in_references({ScheduleKey("xp2", tuple())}, self.dag), set()) - - def test_used_in_references_nested(self): - """Test that schedule identification by name with nested references.""" - - with pulse.build(name="ref_ref_xp") as xp_ref_ref: - pulse.play(pulse.Drag(160, 0.5, 40, 0.2), pulse.DriveChannel(1)) - pulse.call(self.xp_ref) - - for sched in [self.xp1, self.xp_ref, xp_ref_ref]: - update_schedule_dependency(sched, self.dag, ScheduleKey(sched.name, tuple())) - - expected = {"ref_xp", "ref_ref_xp"} - self.assertSetEqual(used_in_references({ScheduleKey("xp", tuple())}, self.dag), expected) - - def test_used_in_references(self): - """Test a CR setting.""" - cr_tone_p = pulse.GaussianSquare(640, 0.2, 64, 500) - cr_tone_m = pulse.GaussianSquare(640, -0.2, 64, 500) - - with pulse.build(name="cr") as cr: - with pulse.align_sequential(): - with pulse.align_left(): - pulse.play(cr_tone_p, pulse.ControlChannel(2)) - pulse.reference(self.xp1.name, "q0") - with pulse.align_left(): - pulse.play(cr_tone_m, pulse.ControlChannel(2)) - pulse.reference(self.xp1.name, "q0") - - update_schedule_dependency(self.xp1, self.dag, ScheduleKey(self.xp1.name, tuple())) - update_schedule_dependency(cr, self.dag, ScheduleKey(cr.name, tuple())) - - self.assertSetEqual(used_in_references({ScheduleKey("xp", tuple())}, self.dag), {"cr"}) - - def test_replace(self): - """Test that we can replace a schedule that already exists in the dag.""" - - with pulse.build(name="dref") as double_ref: - pulse.reference(self.xp1.name, "q0") - pulse.reference(self.xp2.name, "q0") - - key1 = ScheduleKey(self.xp1.name, tuple()) - key2 = ScheduleKey(self.xp2.name, tuple()) - key3 = ScheduleKey(double_ref.name, tuple()) - - update_schedule_dependency(self.xp1, self.dag, key1) - update_schedule_dependency(self.xp2, self.dag, key2) - update_schedule_dependency(double_ref, self.dag, key3) - - idx_xp1 = self.dag.nodes().index(key1) - idx_xp2 = self.dag.nodes().index(key2) - idx_drf = self.dag.nodes().index(key3) - - expected = {(idx_drf, idx_xp1), (idx_drf, idx_xp2)} - self.assertSetEqual(set(self.dag.edge_list()), expected) - - # Now replace the double reference schedule to point to only one xp pulse. - with pulse.build(name="dref") as double_ref: - pulse.reference(self.xp1.name, "q0") - pulse.reference(self.xp1.name, "q0") - - update_schedule_dependency(double_ref, self.dag, ScheduleKey(double_ref.name, tuple())) - - expected = {(idx_drf, idx_xp1)} - self.assertSetEqual(set(self.dag.edge_list()), expected) - - -class TestValidateChannels(QiskitExperimentsTestCase): - """Test validate channels.""" - - def test_ecr_lib(self): - """Test channel validation.""" - - # Test schedules with references. - lib = EchoedCrossResonance() - - self.assertEqual(validate_channels(lib["ecr"]), set()) - - # Has a drive channel and a control channel that should be valid - self.assertEqual(len(validate_channels(lib["cr45p"])), 2) - - def test_raise_on_multiple_parameters(self): - """Test that an error is raised on a sum of parameters""" - p1, p2 = Parameter("p1"), Parameter("p2") - - with pulse.build() as sched: - pulse.play(pulse.Drag(160, 0.5, 40, 0), pulse.DriveChannel(p1 + p2)) - - with self.assertRaises(CalibrationError): - validate_channels(sched) - - def test_invalid_name(self): - """Test that an error is raised on an invalid channel name.""" - with pulse.build() as sched: - pulse.play(pulse.Drag(160, 0.5, 40, 0), pulse.DriveChannel(Parameter("0&1"))) - - with self.assertRaises(CalibrationError): - validate_channels(sched) - - def test_regex(self): - """Test that the channel name regex is properly formulated.""" - valid = ["ch0", "ch12", "ch12.2", "ch123.2345.2", "ch1.0$1", "ch1.2$12", "ch0.1.2$12"] - invalid = ["cg0", "ch1.", "ch1.2$", "ch123.23p45.2", "ch0d"] - - for channel in valid: - self.assertTrue(CHANNEL_PATTERN_REGEX.match(channel) is not None) - - for channel in invalid: - self.assertTrue(CHANNEL_PATTERN_REGEX.match(channel) is None) diff --git a/test/calibration/test_calibrations.py b/test/calibration/test_calibrations.py deleted file mode 100644 index 68c6a5de71..0000000000 --- a/test/calibration/test_calibrations.py +++ /dev/null @@ -1,1980 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Class to test the calibrations.""" - -from test.base import QiskitExperimentsTestCase -import os -import uuid -from collections import defaultdict -from datetime import datetime, timezone, timedelta - -from ddt import data, ddt, unpack - -from qiskit.circuit import Parameter, Gate -from qiskit.pulse import ( - Drag, - DriveChannel, - ControlChannel, - AcquireChannel, - Gaussian, - GaussianSquare, - MeasureChannel, - RegisterSlot, - Play, -) -from qiskit import QuantumCircuit, pulse, transpile -from qiskit.circuit.library import CXGate, XGate -from qiskit.pulse.transforms import inline_subroutines, block_to_schedule -from qiskit.providers import BackendV2, Options -from qiskit.transpiler import Target -from qiskit_ibm_runtime.fake_provider import FakeArmonkV2, FakeBelemV2 - -from qiskit_experiments.framework import BackendData -from qiskit_experiments.calibration_management.calibrations import Calibrations, ParameterKey -from qiskit_experiments.calibration_management.parameter_value import ParameterValue -from qiskit_experiments.calibration_management.basis_gate_library import ( - FixedFrequencyTransmon, -) -from qiskit_experiments.exceptions import CalibrationError - - -class MinimalBackend(BackendV2): - """Class for testing a backend with minimal data""" - - def __init__(self, num_qubits=1): - super().__init__() - self._target = Target(num_qubits=num_qubits) - - @property - def max_circuits(self): - """Maximum circuits to run at once""" - return 100 - - @classmethod - def _default_options(cls): - return Options() - - @property - def target(self) -> Target: - """Target instance for the backend""" - return self._target - - def run(self, run_input, **options): - """Empty method to satisfy abstract base class""" - pass - - -@ddt -class TestCalibrationsBasic(QiskitExperimentsTestCase): - """Class to test the management of schedules and parameters for calibrations.""" - - def setUp(self): - """Create the setting to test.""" - super().setUp() - - self.cals = Calibrations(coupling_map=[]) - - self.sigma = Parameter("σ") - self.amp_xp = Parameter("amp") - self.amp_x90p = Parameter("amp") - self.amp_y90p = Parameter("amp") - self.beta = Parameter("β") - self.chan = Parameter("ch0") - self.drive = DriveChannel(self.chan) - self.duration = Parameter("dur") - - # Define and add template schedules. - with pulse.build(name="xp") as xp: - pulse.play(Drag(self.duration, self.amp_xp, self.sigma, self.beta), self.drive) - - with pulse.build(name="xm") as xm: - pulse.play(Drag(self.duration, -self.amp_xp, self.sigma, self.beta), self.drive) - - with pulse.build(name="x90p") as x90p: - pulse.play(Drag(self.duration, self.amp_x90p, self.sigma, self.beta), self.drive) - - with pulse.build(name="y90p") as y90p: - pulse.play(Drag(self.duration, self.amp_y90p, self.sigma, self.beta), self.drive) - - for sched in [xp, x90p, y90p, xm]: - self.cals.add_schedule(sched, num_qubits=1) - - self.xm_pulse = xm - - # Add some parameter values. - self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") - self.cals.add_parameter_value(ParameterValue(160, self.date_time), "dur", schedule="xp") - self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp", 3, "xp") - self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "x90p") - self.cals.add_parameter_value(ParameterValue(0.08, self.date_time), "amp", (3,), "y90p") - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "β", (3,), "xp") - - def test_calibration_save_json(self): - """Test that the calibration under test can be serialized through JSON.""" - filename = self.__class__.__name__ - - try: - self.cals.save(file_type="json", file_prefix=filename) - loaded = self.cals.load(file_path=f"{filename}.json") - self.assertEqual(self.cals, loaded) - finally: - if os.path.exists(f"{filename}.json"): - os.remove(f"{filename}.json") - - def test_setup(self): - """Test that the initial setup behaves as expected.""" - expected = {ParameterKey("amp", (), "xp"), ParameterKey("amp", (), "xm")} - self.assertEqual(self.cals.parameters[self.amp_xp], expected) - - expected = {ParameterKey("amp", (), "x90p")} - self.assertEqual(self.cals.parameters[self.amp_x90p], expected) - - expected = {ParameterKey("amp", (), "y90p")} - self.assertEqual(self.cals.parameters[self.amp_y90p], expected) - - expected = { - ParameterKey("β", (), "xp"), - ParameterKey("β", (), "xm"), - ParameterKey("β", (), "x90p"), - ParameterKey("β", (), "y90p"), - } - self.assertEqual(self.cals.parameters[self.beta], expected) - - expected = { - ParameterKey("σ", (), "xp"), - ParameterKey("σ", (), "xm"), - ParameterKey("σ", (), "x90p"), - ParameterKey("σ", (), "y90p"), - } - self.assertEqual(self.cals.parameters[self.sigma], expected) - - self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.2) - self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xm"), 0.2) - self.assertEqual(self.cals.get_parameter_value("amp", 3, "x90p"), 0.1) - self.assertEqual(self.cals.get_parameter_value("amp", 3, "y90p"), 0.08) - - def test_improper_setup(self): - """Check that an error is raised when coupling map and control channel map do not match.""" - controls = { - (3, 2): [ControlChannel(10), ControlChannel(123)], - (2, 3): [ControlChannel(15), ControlChannel(23)], - } - coupling_map = [[0, 1], [1, 0]] - - with self.assertRaises(CalibrationError): - Calibrations(coupling_map=coupling_map, control_channel_map=controls) - - with self.assertRaises(CalibrationError): - Calibrations(coupling_map=[], control_channel_map=controls) - - def test_preserve_template(self): - """Test that the template schedule is still fully parametric after we get a schedule.""" - - # First get a schedule - xp = self.cals.get_schedule("xp", (3,)) - self.assertEqual(xp.instructions[0][1].operands[0].amp, 0.2) - - # Find the template schedule for xp and test it. - schedule = pulse.Schedule() - for sched_dict in self.cals.schedules(): - if sched_dict["schedule"].name == "xp": - schedule = sched_dict["schedule"] - - for param in (self.amp_xp, self.sigma, self.beta, self.duration, self.chan): - self.assertTrue(param in schedule.parameters) - - self.assertEqual(len(schedule.parameters), 5) - self.assertEqual(len(schedule.blocks), 1) - - def test_remove_schedule(self): - """Test that we can easily remove a schedule.""" - - self.assertEqual(len(self.cals.schedules()), 4) - - self.cals.remove_schedule(self.xm_pulse) - - # Removing xm should remove the schedule but not the parameters as they are shared. - self.assertEqual(len(self.cals.schedules()), 3) - for param in [self.sigma, self.amp_xp, self.amp_x90p, self.amp_y90p, self.beta]: - self.assertTrue(param in self.cals.parameters) - - # Add a schedule with a different parameter and then remove it - with pulse.build(name="error") as sched: - pulse.play(Gaussian(160, Parameter("xyz"), 40), DriveChannel(Parameter("ch0"))) - - self.cals.add_schedule(sched, num_qubits=1) - - self.assertEqual(len(self.cals.schedules()), 4) - self.assertEqual(len(self.cals.parameters), 7) - - self.cals.remove_schedule(sched) - - self.assertEqual(len(self.cals.schedules()), 3) - self.assertEqual(len(self.cals.parameters), 6) - for param in [self.sigma, self.amp_xp, self.amp_x90p, self.amp_y90p, self.beta]: - self.assertTrue(param in self.cals.parameters) - - def test_parameter_dependency(self): - """Check that two schedules that share the same parameter are simultaneously updated.""" - - xp = self.cals.get_schedule("xp", (3,)) - self.assertEqual(xp.instructions[0][1].operands[0].amp, 0.2) - - xm = self.cals.get_schedule("xm", (3,)) - self.assertEqual(xm.instructions[0][1].operands[0].amp, -0.2) - - self.cals.add_parameter_value(ParameterValue(0.25, datetime.now()), "amp", (3,), "xp") - - xp = self.cals.get_schedule("xp", (3,)) - self.assertEqual(xp.instructions[0][1].operands[0].amp, 0.25) - - xm = self.cals.get_schedule("xm", (3,)) - self.assertEqual(xm.instructions[0][1].operands[0].amp, -0.25) - - def test_get_value(self): - """Test the retrieve of parameter values.""" - - self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.2) - self.assertEqual(self.cals.get_parameter_value("amp", (3,), "x90p"), 0.1) - - self.assertEqual(self.cals.get_parameter_value("σ", (3,), "x90p"), 40) - self.assertEqual(self.cals.get_parameter_value("σ", (3,), "xp"), 40) - - self.cals.add_parameter_value(ParameterValue(50, datetime.now()), "σ", (3,), "xp") - self.assertEqual(self.cals.get_parameter_value("σ", (3,), "x90p"), 50) - self.assertEqual(self.cals.get_parameter_value("σ", (3,), "xp"), 50) - - def test_channel_names(self): - """Check the naming of parametric control channels index1.index2.index3...""" - drive_0 = DriveChannel(Parameter("ch0")) - drive_1 = DriveChannel(Parameter("ch1")) - control_bad = ControlChannel(Parameter("u_chan")) - control_good = ControlChannel(Parameter("ch1.0")) - - with pulse.build(name="good_sched") as sched_good: - pulse.play(Drag(160, 0.1, 40, 2), drive_0) - pulse.play(Drag(160, 0.1, 40, 2), drive_1) - pulse.play(Drag(160, 0.1, 40, 2), control_good) - - with pulse.build(name="bad_sched") as sched_bad: - pulse.play(Drag(160, 0.1, 40, 2), drive_0) - pulse.play(Drag(160, 0.1, 40, 2), drive_1) - pulse.play(Drag(160, 0.1, 40, 2), control_bad) - - self.cals.add_schedule(sched_good, num_qubits=2) - - with self.assertRaises(CalibrationError): - self.cals.add_schedule(sched_bad, num_qubits=2) - - def test_unique_parameter_names(self): - """Test that we cannot insert schedules in which parameter names are duplicates.""" - with pulse.build() as sched: - pulse.play(Drag(160, Parameter("a"), Parameter("a"), Parameter("a")), DriveChannel(0)) - - with self.assertRaises(CalibrationError): - self.cals.add_schedule(sched, num_qubits=1) - - def test_parameter_without_schedule(self): - """Test that we can manage parameters that are not bound to a schedule.""" - self.cals._register_parameter(Parameter("a"), ()) - - def test_free_parameters(self): - """Test that we can get a schedule with a free parameter.""" - xp = self.cals.get_schedule("xp", 3, assign_params={"amp": self.amp_xp}) - self.assertEqual(set(xp.parameters), {self.amp_xp}) - - xp = self.cals.get_schedule("xp", 3, assign_params={"amp": self.amp_xp, "σ": self.sigma}) - self.assertEqual(set(xp.parameters), {self.amp_xp, self.sigma}) - - def test_qubit_input(self): - """Test the qubit input.""" - - xp = self.cals.get_schedule("xp", 3) - self.assertEqual(xp.instructions[0][1].operands[0].amp, 0.2) - - val = self.cals.get_parameter_value("amp", 3, "xp") - self.assertEqual(val, 0.2) - - val = self.cals.get_parameter_value("amp", (3,), "xp") - self.assertEqual(val, 0.2) - - with self.assertRaises(CalibrationError): - self.cals.get_parameter_value("amp", ("3",), "xp") - - val = self.cals.get_parameter_value("amp", "3", "xp") - self.assertEqual(val, 0.2) - - with self.assertRaises(CalibrationError): - self.cals.get_parameter_value("amp", "(1, a)", "xp") - - def test_from_backend(self): - """Test that when generating calibrations from backend - the data is passed correctly""" - backend = FakeBelemV2() - cals = Calibrations.from_backend(backend, libraries=[FixedFrequencyTransmon()]) - with self.assertWarns(DeprecationWarning): - config_args = cals.config()["kwargs"] - control_channel_map_size = len(config_args["control_channel_map"].chan_map) - coupling_map_size = len(config_args["coupling_map"]) - self.assertEqual(control_channel_map_size, 8) - self.assertEqual(coupling_map_size, 8) - self.assertEqual(cals.get_parameter_value("drive_freq", 0), 5090167234.445013) - - @data( - (0, None, False), # Edge case. Perhaps does not need to be supported - (1, None, False), # Produces backend.target.qubit_properties is None - (2, None, False), - (1, "x", False), # Produces backend.coupling_map is None - (1, "x", True), - (2, "x", True), - (2, "cx", True), # backend.control_channel raises NotImplementedError - ) - @unpack - def test_from_minimal_backend(self, num_qubits, gate_name, pass_properties): - """Test that from_backend works for a backend with minimal data""" - # We do not use Gate or dict test arguments directly because they do - # not translate to printable test case names, so we translate here. - properties = None - if gate_name == "x": - gate = XGate() - if pass_properties: - properties = {(i,): None for i in range(num_qubits)} - elif gate_name == "cx": - gate = CXGate() - if pass_properties: - properties = {(0, 1): None} - else: - gate = None - - backend = MinimalBackend(num_qubits=num_qubits) - if gate is not None: - backend.target.add_instruction(gate, properties=properties) - Calibrations.from_backend(backend) - - def test_equality(self): - """Test the equal method on calibrations.""" - backend = FakeBelemV2() - library = FixedFrequencyTransmon(basis_gates=["sx", "x"]) - - cals1 = Calibrations.from_backend( - backend, libraries=[library], add_parameter_defaults=False - ) - cals2 = Calibrations.from_backend( - backend, libraries=[library], add_parameter_defaults=False - ) - self.assertTrue(cals1 == cals2) - - date_time = datetime.now(timezone.utc).astimezone() - param_val = ParameterValue(0.12345, date_time=date_time) - cals1.add_parameter_value(param_val, "amp", 3, "x") - - # The two objects are different due to missing parameter value - self.assertFalse(cals1 == cals2) - - # The two objects are different due to time stamps - param_val2 = ParameterValue(0.12345, date_time=date_time - timedelta(seconds=1)) - cals2.add_parameter_value(param_val2, "amp", 3, "x") - self.assertFalse(cals1 == cals2) - - # The two objects are different due to missing parameter value - cals3 = Calibrations.from_backend( - backend, libraries=[library], add_parameter_defaults=False - ) - self.assertFalse(cals1 == cals3) - - # The two objects are identical due to time stamps - cals2.add_parameter_value(param_val, "amp", 3, "x") - self.assertFalse(cals1 == cals3) - - # The schedules contained in the cals are different. - library2 = FixedFrequencyTransmon(basis_gates=["sx", "x", "y"]) - cals1 = Calibrations.from_backend(backend, libraries=[library]) - cals2 = Calibrations.from_backend(backend, libraries=[library2]) - self.assertFalse(cals1 == cals2) - - # Ensure that the equality is not sensitive to parameter adding order. - cals1 = Calibrations.from_backend( - backend, libraries=[library], add_parameter_defaults=False - ) - cals2 = Calibrations.from_backend( - backend, libraries=[library], add_parameter_defaults=False - ) - param_val1 = ParameterValue(0.54321, date_time=date_time) - param_val2 = ParameterValue(0.12345, date_time=date_time - timedelta(seconds=1)) - - cals1.add_parameter_value(param_val2, "amp", 3, "x") - cals1.add_parameter_value(param_val1, "amp", 3, "x") - - cals2.add_parameter_value(param_val1, "amp", 3, "x") - cals2.add_parameter_value(param_val2, "amp", 3, "x") - - self.assertTrue(cals1 == cals2) - - -class TestOverrideDefaults(QiskitExperimentsTestCase): - """ - Test that we can override defaults. For example, this means that all qubits may have a - Gaussian as xp pulse but a specific qubit may have a Drag pulse which overrides the - default Gaussian. - """ - - def setUp(self): - """Create the setting to test.""" - super().setUp() - - self.cals = Calibrations(coupling_map=[]) - - self.sigma = Parameter("σ") - self.amp_xp = Parameter("amp") - self.amp = Parameter("amp") - self.beta = Parameter("β") - self.drive = DriveChannel(Parameter("ch0")) - self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - self.duration = Parameter("dur") - - # Template schedule for qubit 3 - with pulse.build(name="xp") as xp_drag: - pulse.play(Drag(self.duration, self.amp_xp, self.sigma, self.beta), self.drive) - - # Default template schedule for all qubits - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(self.duration, self.amp, self.sigma), self.drive) - - # Add the schedules - self.cals.add_schedule(xp, num_qubits=1) - self.cals.add_schedule(xp_drag, (3,)) - - def test_calibration_save_json(self): - """Test that the calibration under test can be serialized through JSON.""" - filename = self.__class__.__name__ - - try: - self.cals.save(file_type="json", file_prefix=filename) - loaded = self.cals.load(file_path=f"{filename}.json") - self.assertEqual(self.cals, loaded) - finally: - if os.path.exists(f"{filename}.json"): - os.remove(f"{filename}.json") - - def test_parameter_value_adding_and_filtering(self): - """Test that adding parameter values behaves in the expected way.""" - - # Ensure that no parameter values are present when none have been added. - params = self.cals.parameters_table()["data"] - self.assertEqual(params, []) - - # Add a default parameter common to all qubits. - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") - self.assertEqual(len(self.cals.parameters_table()["data"]), 1) - - # Check that we can get a default parameter in the parameter table - self.assertEqual(len(self.cals.parameters_table(parameters=["σ"])["data"]), 1) - self.assertEqual( - len(self.cals.parameters_table(parameters=["σ"], schedules=["xp"])["data"]), 1 - ) - self.assertEqual( - len(self.cals.parameters_table(parameters=["σ"], schedules=["xm"])["data"]), 0 - ) - - # Test behaviour of qubit-specific parameter and without ParameterValue. - self.cals.add_parameter_value(0.25, "amp", (3,), "xp") - self.cals.add_parameter_value(0.15, "amp", (0,), "xp") - - # Check the value for qubit 0 - params = self.cals.parameters_table(parameters=["amp"], qubit_list=[(0,)])["data"] - self.assertEqual(len(params), 1) - self.assertEqual(params[0]["value"], 0.15) - self.assertEqual(params[0]["qubits"], (0,)) - - # Check the value for qubit 3 - params = self.cals.parameters_table(parameters=["amp"], qubit_list=[(3,)])["data"] - self.assertEqual(len(params), 1) - self.assertEqual(params[0]["value"], 0.25) - self.assertEqual(params[0]["qubits"], (3,)) - - def test_complex_parameter_value_deprecation_warning(self): - """Test that complex parameter values raise PendingDeprecationWarning""" - with self.assertWarns(PendingDeprecationWarning): - ParameterValue(40j, self.date_time) - with self.assertWarns(PendingDeprecationWarning): - self.cals.add_parameter_value(40j, "amp", schedule="xp") - - def _add_parameters(self): - """Helper function.""" - - # Add the minimum number of parameter values. Sigma is shared across both schedules. - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") - self.cals.add_parameter_value(ParameterValue(0.25, self.date_time), "amp", (3,), "xp") - self.cals.add_parameter_value(ParameterValue(0.15, self.date_time), "amp", (0,), "xp") - self.cals.add_parameter_value(ParameterValue(10, self.date_time), "β", (3,), "xp") - self.cals.add_parameter_value(160, "dur", schedule="xp") - - def test_default_schedules(self): - """ - In this test we create two xp schedules. A default schedules with a - Gaussian pulse for all qubits and a Drag schedule for qubit three which - should override the default schedule. We also test to see that updating - a common parameter affects both schedules. - """ - self._add_parameters() - - xp0 = self.cals.get_schedule("xp", (0,)) - xp3 = self.cals.get_schedule("xp", (3,)) - - # Check that xp0 is Play(Gaussian(160, 0.15, 40), 0) - self.assertTrue(xp0.instructions[0][1].pulse.pulse_type == "Gaussian") - self.assertEqual(xp0.instructions[0][1].channel, DriveChannel(0)) - self.assertEqual(xp0.instructions[0][1].pulse.amp, 0.15) - self.assertEqual(xp0.instructions[0][1].pulse.sigma, 40) - self.assertEqual(xp0.instructions[0][1].pulse.duration, 160) - - # Check that xp3 is Play(Drag(160, 0.25, 40, 10), 3) - self.assertTrue(xp3.instructions[0][1].pulse.pulse_type == "Drag") - self.assertEqual(xp3.instructions[0][1].channel, DriveChannel(3)) - self.assertEqual(xp3.instructions[0][1].pulse.amp, 0.25) - self.assertEqual(xp3.instructions[0][1].pulse.sigma, 40) - self.assertEqual(xp3.instructions[0][1].pulse.duration, 160) - self.assertEqual(xp3.instructions[0][1].pulse.beta, 10) - - # Check that updating sigma updates both schedules. - later_date_time = datetime.strptime("16/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - self.cals.add_parameter_value(ParameterValue(50, later_date_time), "σ", schedule="xp") - - xp0 = self.cals.get_schedule("xp", (0,)) - xp3 = self.cals.get_schedule("xp", (3,)) - - self.assertEqual(xp0.instructions[0][1].pulse.sigma, 50) - self.assertEqual(xp3.instructions[0][1].pulse.sigma, 50) - - # Check that we have the expected parameters in the calibrations. - expected = { - self.amp_xp, - self.amp, - self.sigma, - self.beta, - self.duration, - } - self.assertEqual(len(set(self.cals.parameters.keys())), len(expected)) - - def test_replace_schedule(self): - """Test that schedule replacement works as expected.""" - - self.cals.add_parameter_value(ParameterValue(0.25, self.date_time), "amp", (3,), "xp") - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") - self.cals.add_parameter_value(ParameterValue(10, self.date_time), "β", (3,), "xp") - - # Let's replace the schedule for qubit 3 with a double Drag pulse. - with pulse.build(name="xp") as sched: - pulse.play(Drag(160, self.amp_xp / 2, self.sigma, self.beta), self.drive) - pulse.play(Drag(160, self.amp_xp / 2, self.sigma, self.beta), self.drive) - - expected = self.cals.parameters - - # Adding this new schedule should not change the parameter mapping - self.cals.add_schedule(sched, (3,)) - - self.assertEqual(self.cals.parameters, expected) - - # For completeness we check that schedule that comes out. - sched_cal = self.cals.get_schedule("xp", (3,)) - - self.assertTrue(sched_cal.instructions[0][1].pulse.pulse_type == "Drag") - self.assertTrue(sched_cal.instructions[1][1].pulse.pulse_type == "Drag") - self.assertEqual(sched_cal.instructions[0][1].pulse.amp, 0.125) - self.assertEqual(sched_cal.instructions[1][1].pulse.amp, 0.125) - - # Let's replace the schedule for qubit 3 with a Gaussian pulse. - # This should change the parameter mapping - with pulse.build(name="xp") as sched2: - pulse.play(Gaussian(160, self.amp_xp / 2, self.sigma), self.drive) - - # Check that beta is in the mapping - self.assertEqual( - self.cals.parameters[self.beta], - {ParameterKey("β", (3,), "xp")}, - ) - - self.cals.add_schedule(sched2, (3,)) - - # Check that beta no longer maps to a schedule - self.assertEqual(self.cals.parameters[self.beta], set()) - - def test_parameter_filtering(self): - """Test that we can properly filter parameter values.""" - - self._add_parameters() - - # Check that these values are split between the qubits. - amp_values = self.cals.parameters_table(parameters=["amp"], qubit_list=[(0,)])["data"] - self.assertEqual(len(amp_values), 1) - - # Check that we have one value for sigma. - sigma_values = self.cals.parameters_table(parameters=["σ"])["data"] - self.assertEqual(len(sigma_values), 1) - - # Check that we have two values for amp. - amp_values = self.cals.parameters_table(parameters=["amp"])["data"] - self.assertEqual(len(amp_values), 2) - - amp_values = self.cals.parameters_table(parameters=["amp"], qubit_list=[(3,)])["data"] - self.assertEqual(len(amp_values), 1) - - # Check to see if we get back the two qubits when explicitly specifying them. - amp_values = self.cals.parameters_table(parameters=["amp"], qubit_list=[(3,), (0,)])["data"] - self.assertEqual(len(amp_values), 2) - - -class TestConcurrentParameters(QiskitExperimentsTestCase): - """Test a particular edge case with the time in the parameter values.""" - - def test_concurrent_values(self): - """ - Ensure that if the max time has multiple entries we take the most recent appended one. - """ - - cals = Calibrations(coupling_map=[]) - - amp = Parameter("amp") - ch0 = Parameter("ch0") - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(160, amp, 40), DriveChannel(ch0)) - - cals.add_schedule(xp, num_qubits=1) - - date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - - cals.add_parameter_value(ParameterValue(0.25, date_time), "amp", (3,), "xp") - cals.add_parameter_value(ParameterValue(0.35, date_time), "amp", (3,), "xp") - cals.add_parameter_value(ParameterValue(0.45, date_time), "amp", (3,), "xp") - - self.assertEqual(cals.get_parameter_value("amp", 3, "xp"), 0.45) - - -class TestMeasurements(QiskitExperimentsTestCase): - """Test that schedules on measure channels are handled properly.""" - - def setUp(self): - """Create the setting to test.""" - super().setUp() - - self.amp = Parameter("amp") - self.amp_xp = Parameter("amp") - self.sigma = Parameter("σ") - self.sigma_xp = Parameter("σ") - self.width = Parameter("w") - self.duration = Parameter("dur") - self.duration_xp = Parameter("dur") - ch0 = Parameter("ch0") - self.m0_ = MeasureChannel(ch0) - self.d0_ = DriveChannel(ch0) - self.delay = Parameter("delay") - - with pulse.build(name="meas") as meas: - pulse.play(GaussianSquare(self.duration, self.amp, self.sigma, self.width), self.m0_) - - with pulse.build(name="meas_acquire") as meas_acq: - pulse.play(GaussianSquare(self.duration, self.amp, self.sigma, self.width), self.m0_) - pulse.delay(self.delay, pulse.AcquireChannel(ch0)) - pulse.acquire(self.duration, pulse.AcquireChannel(ch0), pulse.RegisterSlot(ch0)) - - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(self.duration_xp, self.amp_xp, self.sigma_xp), self.d0_) - - with pulse.build(name="xp_meas") as xp_meas: - pulse.reference(xp.name, "q0") - pulse.reference(meas.name, "q0") - - with pulse.build(name="xt_meas") as xt_meas: - with pulse.align_sequential(): - pulse.reference(xp.name, "q0") - pulse.reference(meas.name, "q0") - with pulse.align_sequential(): - pulse.reference(xp.name, "q1") - pulse.reference(meas.name, "q1") - - self.cals = Calibrations(coupling_map=[]) - self.cals.add_schedule(meas, num_qubits=1) - self.cals.add_schedule(xp, num_qubits=1) - self.cals.add_schedule(xp_meas, num_qubits=1) - self.cals.add_schedule(xt_meas, num_qubits=2) - self.cals.add_schedule(meas_acq, num_qubits=1) - - # self.cals.add_parameter_value(8000, self.duration, schedule="meas") - self.cals.add_parameter_value(0.5, self.amp, (0,), "meas") - self.cals.add_parameter_value(0.56, self.amp, (123,), "meas") - self.cals.add_parameter_value(0.3, self.amp, (2,), "meas") - self.cals.add_parameter_value(160, self.sigma, schedule="meas") - self.cals.add_parameter_value(7000, self.width, schedule="meas") - self.cals.add_parameter_value(8000, self.duration, schedule="meas") - self.cals.add_parameter_value(100, self.delay, schedule="meas_acquire") - - self.cals.add_parameter_value(0.9, self.amp_xp, (0,), "xp") - self.cals.add_parameter_value(0.7, self.amp_xp, (2,), "xp") - self.cals.add_parameter_value(40, self.sigma_xp, schedule="xp") - self.cals.add_parameter_value(160, self.duration_xp, schedule="xp") - - def test_calibration_save_json(self): - """Test that the calibration under test can be serialized through JSON.""" - filename = self.__class__.__name__ - - try: - self.cals.save(file_type="json", file_prefix=filename) - loaded = self.cals.load(file_path=f"{filename}.json") - self.assertEqual(self.cals, loaded) - finally: - if os.path.exists(f"{filename}.json"): - os.remove(f"{filename}.json") - - def test_meas_schedule(self): - """Test that we get a properly assigned measure schedule without drive channels.""" - sched = self.cals.get_schedule("meas", (0,)) - meas = Play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) - self.assertTrue(sched.instructions[0][1], meas) - - sched = self.cals.get_schedule("meas", (2,)) - meas = Play(GaussianSquare(8000, 0.3, 160, 7000), MeasureChannel(0)) - self.assertTrue(sched.instructions[0][1], meas) - - def test_call_meas(self): - """Test that we can call a measurement pulse.""" - sched = self.cals.get_schedule("xp_meas", (0,)) - xp = Play(Gaussian(160, 0.9, 40), DriveChannel(0)) - meas = Play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) - - self.assertTrue(sched.instructions[0][1], xp) - self.assertTrue(sched.instructions[1][1], meas) - - def test_xt_meas(self): - """Test that creating multi-qubit schedules out of calls works.""" - - sched = self.cals.get_schedule("xt_meas", (0, 2)) - - xp0 = Play(Gaussian(160, 0.9, 40), DriveChannel(0)) - xp2 = Play(Gaussian(160, 0.7, 40), DriveChannel(2)) - - meas0 = Play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) - meas2 = Play(GaussianSquare(8000, 0.3, 160, 7000), MeasureChannel(2)) - - self.assertEqual(sched.instructions[0][1], xp0) - self.assertEqual(sched.instructions[1][1], xp2) - self.assertEqual(sched.instructions[2][1], meas0) - self.assertEqual(sched.instructions[3][1], meas2) - - def test_free_parameters(self): - """Test that we can get a schedule with free parameters.""" - - # Test coupling breaking - my_amp = Parameter("my_amp") - schedule = self.cals.get_schedule( - "xt_meas", - (0, 2), - assign_params={("amp", (0,), "xp"): my_amp}, - ) - - schedule = block_to_schedule(schedule) - - with pulse.build(name="xt_meas") as expected: - with pulse.align_sequential(): - pulse.play(Gaussian(160, my_amp, 40), DriveChannel(0)) - pulse.play(GaussianSquare(8000, 0.5, 160, 7000), MeasureChannel(0)) - with pulse.align_sequential(): - pulse.play(Gaussian(160, 0.7, 40), DriveChannel(2)) - pulse.play(GaussianSquare(8000, 0.3, 160, 7000), MeasureChannel(2)) - - expected = block_to_schedule(expected) - - self.assertEqual(schedule.parameters, {my_amp}) - self.assertEqual(schedule, expected) - - def test_free_parameters_check(self): - """ - Test that get_schedule raises an error if the number of parameters does not match. - This test ensures that we forbid ambiguity in free parameters in schedules with - calls that share parameters. - """ - - amp1 = Parameter("amp1") - amp2 = Parameter("amp2") - assign_dict = {("amp", (0,), "xp"): amp1, ("amp", (2,), "xp"): amp2} - - sched = self.cals.get_schedule("xt_meas", (0, 2), assign_params=assign_dict) - - self.assertEqual(set(sched.parameters), {amp1, amp2}) - - sched = block_to_schedule(sched) - - self.assertEqual(sched.instructions[0][1].parameters, {amp1}) - self.assertEqual(sched.instructions[1][1].parameters, {amp2}) - - def test_measure_and_acquire(self): - """Test that we can get a measurement schedule with an acquire instruction.""" - - sched = self.cals.get_schedule("meas_acquire", (123,)) - - with pulse.build(name="meas_acquire") as expected: - pulse.play(GaussianSquare(8000, 0.56, 160, 7000), MeasureChannel(123)) - pulse.delay(100, AcquireChannel(123)) - pulse.acquire(8000, AcquireChannel(123), RegisterSlot(123)) - - self.assertEqual(sched, expected) - - -class TestInstructions(QiskitExperimentsTestCase): - """Class to test that instructions like Shift and Set Phase/Frequency are properly managed.""" - - def setUp(self): - """Create the setting to test.""" - super().setUp() - - self.phase = Parameter("φ") - self.freq = Parameter("ν") - self.d0_ = DriveChannel(Parameter("ch0")) - - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(160, 0.5, 40), self.d0_) - - with pulse.build(name="xp12") as xp12: - pulse.shift_phase(self.phase, self.d0_) - pulse.set_frequency(self.freq, self.d0_) - pulse.play(Gaussian(160, 0.5, 40), self.d0_) - - # To make things more interesting we will use a call. - with pulse.build(name="xp02") as xp02: - pulse.reference(xp.name, "q0") - pulse.reference(xp12.name, "q0") - - self.cals = Calibrations(coupling_map=[]) - self.cals.add_schedule(xp, num_qubits=1) - self.cals.add_schedule(xp12, num_qubits=1) - self.cals.add_schedule(xp02, num_qubits=1) - - self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - - self.cals.add_parameter_value(ParameterValue(1.57, self.date_time), "φ", (3,), "xp12") - self.cals.add_parameter_value(ParameterValue(200, self.date_time), "ν", (3,), "xp12") - - def test_calibration_save_json(self): - """Test that the calibration under test can be serialized through JSON.""" - filename = self.__class__.__name__ - - try: - self.cals.save(file_type="json", file_prefix=filename) - loaded = self.cals.load(file_path=f"{filename}.json") - self.assertEqual(self.cals, loaded) - finally: - if os.path.exists(f"{filename}.json"): - os.remove(f"{filename}.json") - - def test_call_registration(self): - """Check that by registering the call we registered three schedules.""" - - self.assertEqual(len(self.cals.schedules()), 3) - - def test_instructions(self): - """Check that we get a properly assigned schedule.""" - - sched = self.cals.get_schedule("xp02", (3,)) - - self.assertEqual(set(sched.parameters), set()) - - sched = inline_subroutines(sched) # inline makes the check more transparent. - - self.assertTrue(isinstance(sched.instructions[0][1], pulse.Play)) - self.assertEqual(sched.instructions[1][1].phase, 1.57) - self.assertEqual(sched.instructions[2][1].frequency, 200) - - -class TestRegistering(QiskitExperimentsTestCase): - """Class to test registering of subroutines with calls.""" - - def setUp(self): - """Create the setting to test.""" - super().setUp() - - self.cals = Calibrations(coupling_map=[]) - self.d0_ = DriveChannel(Parameter("ch0")) - - def test_call_registering(self): - """Test registering of schedules with call.""" - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(160, 0.5, 40), self.d0_) - - with pulse.build(name="call_xp") as call_xp: - pulse.reference(xp.name, "q0") - - with self.assertRaises(CalibrationError): - self.cals.add_schedule(call_xp, num_qubits=1) - - self.cals.add_schedule(xp, num_qubits=1) - self.cals.add_schedule(call_xp, num_qubits=1) - - self.assertTrue(isinstance(self.cals.get_schedule("call_xp", 2), pulse.ScheduleBlock)) - - def test_get_template(self): - """Test that we can get a registered template and use it.""" - amp = Parameter("amp") - - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(160, amp, 40), self.d0_) - - self.cals.add_schedule(xp, num_qubits=1) - - registered_xp = self.cals.get_template("xp", (1,)) - - self.assertEqual(registered_xp, xp) - - with pulse.build(name="dxp") as dxp: - pulse.reference(registered_xp.name, "q0") - pulse.play(Gaussian(160, amp, 40), self.d0_) - - self.cals.add_schedule(dxp, num_qubits=1) - self.cals.add_parameter_value(0.5, "amp", 3, "xp") - - sched = block_to_schedule(self.cals.get_schedule("dxp", 3)) - - self.assertEqual(sched.instructions[0][1], Play(Gaussian(160, 0.5, 40), DriveChannel(3))) - self.assertEqual(sched.instructions[1][1], Play(Gaussian(160, 0.5, 40), DriveChannel(3))) - - with self.assertRaises(CalibrationError): - self.cals.get_template("not registered", (1,)) - - self.cals.get_template("xp", (3,)) - - -class CrossResonanceTest(QiskitExperimentsTestCase): - """Setup class for an echoed cross-resonance calibration.""" - - def setUp(self): - """Create the setting to test.""" - super().setUp() - - controls = { - (3, 2): [ControlChannel(10), ControlChannel(123)], - (2, 3): [ControlChannel(15), ControlChannel(23)], - } - coupling_map = [[0, 1], [1, 0], [1, 2], [2, 1], [2, 3], [3, 2]] - self.cals = Calibrations(coupling_map=coupling_map, control_channel_map=controls) - - self.amp_cr = Parameter("amp") - self.amp_rot = Parameter("amp_rot") - self.amp = Parameter("amp") - self.amp_tcp = Parameter("amp") - self.d0_ = DriveChannel(Parameter("ch0")) - self.d1_ = DriveChannel(Parameter("ch1")) - self.c1_ = ControlChannel(Parameter("ch0.1")) - self.sigma = Parameter("σ") - self.width = Parameter("w") - self.date_time = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - - cr_tone_p = GaussianSquare(640, self.amp_cr, self.sigma, self.width) - rotary_p = GaussianSquare(640, self.amp_rot, self.sigma, self.width) - - cr_tone_m = GaussianSquare(640, -self.amp_cr, self.sigma, self.width) - rotary_m = GaussianSquare(640, -self.amp_rot, self.sigma, self.width) - - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(160, self.amp, self.sigma), self.d0_) - - with pulse.build(name="cr") as cr: - with pulse.align_sequential(): - with pulse.align_left(): - pulse.play(rotary_p, self.d1_) # Rotary tone - pulse.play(cr_tone_p, self.c1_) # CR tone. - pulse.reference("xp", "q0") - with pulse.align_left(): - pulse.play(rotary_m, self.d1_) - pulse.play(cr_tone_m, self.c1_) - pulse.reference("xp", "q0") - - # Mimic a tunable coupler pulse that is just a pulse on a control channel. - with pulse.build(name="tcp") as tcp: - pulse.play(GaussianSquare(640, self.amp_tcp, self.sigma, self.width), self.c1_) - - self.cals.add_schedule(xp, num_qubits=1) - self.cals.add_schedule(cr, num_qubits=2) - self.cals.add_schedule(tcp, num_qubits=2) - - self.cals.add_parameter_value(ParameterValue(40, self.date_time), "σ", schedule="xp") - self.cals.add_parameter_value(ParameterValue(0.1, self.date_time), "amp", (3,), "xp") - self.cals.add_parameter_value(ParameterValue(0.3, self.date_time), "amp", (3, 2), "cr") - self.cals.add_parameter_value(ParameterValue(0.2, self.date_time), "amp_rot", (3, 2), "cr") - self.cals.add_parameter_value(ParameterValue(0.8, self.date_time), "amp", (3, 2), "tcp") - self.cals.add_parameter_value(ParameterValue(20, self.date_time), "w", (3, 2), "cr") - - # Reverse gate parameters - self.cals.add_parameter_value(ParameterValue(0.15, self.date_time), "amp", (2,), "xp") - self.cals.add_parameter_value(ParameterValue(0.5, self.date_time), "amp", (2, 3), "cr") - self.cals.add_parameter_value(ParameterValue(0.4, self.date_time), "amp_rot", (2, 3), "cr") - self.cals.add_parameter_value(ParameterValue(30, self.date_time), "w", (2, 3), "cr") - - -class TestControlChannels(CrossResonanceTest): - """ - Test the echoed cross-resonance schedule which is more complex than single-qubit - schedules. The example also shows that a schedule with call instructions can - support parameters with the same names. - """ - - def test_calibration_save_json(self): - """Test that the calibration under test can be serialized through JSON.""" - filename = self.__class__.__name__ - - try: - self.cals.save(file_type="json", file_prefix=filename) - loaded = self.cals.load(file_path=f"{filename}.json") - self.assertEqual(self.cals, loaded) - finally: - if os.path.exists(f"{filename}.json"): - os.remove(f"{filename}.json") - - def test_get_schedule(self): - """Check that we can get a CR schedule with a built in Call.""" - - with pulse.build(name="cr") as cr_32: - with pulse.align_sequential(): - with pulse.align_left(): - pulse.play(GaussianSquare(640, 0.2, 40, 20), DriveChannel(2)) # Rotary tone - pulse.play(GaussianSquare(640, 0.3, 40, 20), ControlChannel(10)) # CR tone. - pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) - with pulse.align_left(): - pulse.play(GaussianSquare(640, -0.2, 40, 20), DriveChannel(2)) # Rotary tone - pulse.play(GaussianSquare(640, -0.3, 40, 20), ControlChannel(10)) # CR tone. - pulse.play(Gaussian(160, 0.1, 40), DriveChannel(3)) - - # We inline to make the schedules comparable with the construction directly above. - schedule = self.cals.get_schedule("cr", (3, 2)) - inline_schedule = inline_subroutines(schedule) - for idx, inst in enumerate(inline_schedule.instructions): - self.assertTrue(inst == cr_32.instructions[idx]) - - self.assertEqual(set(schedule.parameters), set()) - - # Do the CR in the other direction - with pulse.build(name="cr") as cr_23: - with pulse.align_sequential(): - with pulse.align_left(): - pulse.play(GaussianSquare(640, 0.4, 40, 30), DriveChannel(3)) # Rotary tone - pulse.play(GaussianSquare(640, 0.5, 40, 30), ControlChannel(15)) # CR tone. - pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) - with pulse.align_left(): - pulse.play(GaussianSquare(640, -0.4, 40, 30), DriveChannel(3)) # Rotary tone - pulse.play(GaussianSquare(640, -0.5, 40, 30), ControlChannel(15)) # CR tone. - pulse.play(Gaussian(160, 0.15, 40), DriveChannel(2)) - - schedule = self.cals.get_schedule("cr", (2, 3)) - inline_schedule = inline_subroutines(schedule) - for idx, inst in enumerate(inline_schedule.instructions): - self.assertTrue(inst == cr_23.instructions[idx]) - - self.assertEqual(set(schedule.parameters), set()) - - def test_free_parameters(self): - """Test that we can get a schedule with free parameters.""" - - assign_params = {("amp", (3, 2), "cr"): self.amp_cr} - schedule = self.cals.get_schedule("cr", (3, 2), assign_params=assign_params) - - self.assertEqual(set(schedule.parameters), {self.amp_cr}) - - def test_single_control_channel(self): - """Test that getting a correct pulse on a control channel only works.""" - - with pulse.build(name="tcp") as expected: - pulse.play(GaussianSquare(640, 0.8, 40, 20), ControlChannel(10)) - - self.assertEqual(self.cals.get_schedule("tcp", (3, 2)), expected) - - def test_inst_map_stays_consistent(self): - """Check that get schedule and inst map are in sync in a complex ECR case. - - Test that when a parameter value is updated for a parameter that is used in a - schedule nested inside a call instruction of an outer schedule that that outer - schedule is also updated in the instruction schedule map. For example, this test - will fail if the coupling_map and the control_channel_map are not consistent - with each other. This is because the coupling_map is used to build the - _operated_qubits variable which determines the qubits of the instruction to - which a schedule is associated. - """ - - # Check that the ECR schedules from get_schedule and the instmap are the same - sched_inst = self.cals.default_inst_map.get("cr", (2, 3)) - self.assertEqual(sched_inst, self.cals.get_schedule("cr", (2, 3))) - - # Ensure that amp is 0.15 - insts = block_to_schedule(sched_inst).filter(channels=[DriveChannel(2)]).instructions - self.assertEqual(insts[0][1].pulse.amp, 0.15) - - # Update amp to 0.25 and check that change is propagated through. - date_time2 = datetime.strptime("15/09/19 10:22:35", "%d/%m/%y %H:%M:%S") - self.cals.add_parameter_value(ParameterValue(0.25, date_time2), "amp", (2,), schedule="xp") - - sched_inst = self.cals.default_inst_map.get("cr", (2, 3)) - self.assertEqual(sched_inst, self.cals.get_schedule("cr", (2, 3))) - insts = block_to_schedule(sched_inst).filter(channels=[DriveChannel(2)]).instructions - self.assertEqual(insts[0][1].pulse.amp, 0.25) - - # Test linked parameters. - self.cals.add_parameter_value(ParameterValue(2, date_time2), "σ", (2,), schedule="xp") - - sched_inst = self.cals.default_inst_map.get("cr", (2, 3)) - self.assertEqual(sched_inst, self.cals.get_schedule("cr", (2, 3))) - insts = block_to_schedule(sched_inst).filter(channels=[DriveChannel(2)]).instructions - self.assertEqual(insts[0][1].pulse.sigma, 2) - - -class TestAssignment(QiskitExperimentsTestCase): - """Test simple assignment""" - - def setUp(self): - """Create the setting to test.""" - super().setUp() - - controls = {(3, 2): [ControlChannel(10)]} - coupling_map = [[2, 3], [3, 2]] - self.cals = Calibrations(coupling_map=coupling_map, control_channel_map=controls) - - self.amp_xp = Parameter("amp") - self.ch0 = Parameter("ch0") - self.d0_ = DriveChannel(self.ch0) - self.ch1 = Parameter("ch1") - self.d1_ = DriveChannel(self.ch1) - self.sigma = Parameter("σ") - self.width = Parameter("w") - self.dur = Parameter("duration") - - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) - - with pulse.build(name="xpxp") as xpxp: - with pulse.align_left(): - pulse.reference("xp", "q0") - pulse.reference("xp", "q1") - - self.xp_ = xp - self.cals.add_schedule(xp, num_qubits=1) - self.cals.add_schedule(xpxp, num_qubits=2) - - self.cals.add_parameter_value(0.2, "amp", (2,), "xp") - self.cals.add_parameter_value(0.3, "amp", (3,), "xp") - self.cals.add_parameter_value(40, "σ", (), "xp") - - def test_calibration_save_json(self): - """Test that the calibration under test can be serialized through JSON.""" - filename = self.__class__.__name__ - - try: - self.cals.save(file_type="json", file_prefix=filename) - loaded = self.cals.load(file_path=f"{filename}.json") - self.assertEqual(self.cals, loaded) - finally: - if os.path.exists(f"{filename}.json"): - os.remove(f"{filename}.json") - - def test_short_key(self): - """Test simple value assignment""" - sched = self.cals.get_schedule("xp", (2,), assign_params={"amp": 0.1}) - - with pulse.build(name="xp") as expected: - pulse.play(Gaussian(160, 0.1, 40), DriveChannel(2)) - - self.assertEqual(sched, expected) - - def test_assign_to_parameter(self): - """Test assigning to a Parameter instance""" - my_amp = Parameter("my_amp") - sched = self.cals.get_schedule("xp", (2,), assign_params={"amp": my_amp}) - - with pulse.build(name="xp") as expected: - pulse.play(Gaussian(160, my_amp, 40), DriveChannel(2)) - - self.assertEqual(sched, expected) - - def test_assign_to_parameter_in_reference(self): - """Test assigning to a Parameter instance in a reference.""" - with pulse.build(name="call_xp") as call_xp: - pulse.reference(self.xp_.name, "q0") - self.cals.add_schedule(call_xp, num_qubits=1) - - my_amp = Parameter("my_amp") - sched = self.cals.get_schedule("call_xp", (2,), assign_params={("amp", (2,), "xp"): my_amp}) - sched = block_to_schedule(sched) - - with pulse.build(name="xp") as expected: - pulse.play(Gaussian(160, my_amp, 40), DriveChannel(2)) - expected = block_to_schedule(expected) - - self.assertEqual(sched, expected) - - def test_assign_to_parameter_in_reference_and_to_value_in_referencer(self): - """Test assigning to a Parameter instances in a reference and referencer.""" - with pulse.build(name="call_xp_xp") as call_xp_xp: - pulse.reference(self.xp_.name, "q0") - pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) - self.cals.add_schedule(call_xp_xp, num_qubits=1) - - my_amp = Parameter("amp") - sched = self.cals.get_schedule( - "call_xp_xp", - (2,), - assign_params={ - ("amp", (2,), "xp"): my_amp, - ("amp", (2,), "call_xp_xp"): 0.2, - }, - ) - sched = block_to_schedule(sched) - - with pulse.build(name="xp") as expected: - pulse.play(Gaussian(160, my_amp, 40), DriveChannel(2)) - pulse.play(Gaussian(160, 0.2, 40), DriveChannel(2)) - expected = block_to_schedule(expected) - - self.assertEqual(sched, expected) - - def test_assign_to_same_parameter_in_call_and_caller(self): - """ - Test assigning to a Parameter in a call and reassigning in caller raises - - Check that it is not allowed to leave a parameter in a subschedule free - by assigning it to a Parameter that is also used in the calling - schedule as that will re-bind the Parameter in the subschedule as well. - """ - with pulse.build(name="call_xp_xp") as call_xp_xp: - pulse.reference(self.xp_.name, "q0") - pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) - self.cals.add_schedule(call_xp_xp, num_qubits=1) - - my_amp = Parameter("amp") - with self.assertRaises(CalibrationError): - self.cals.get_schedule( - "call_xp_xp", - (2,), - assign_params={ - ("amp", (2,), "xp"): self.amp_xp, - ("amp", (2,), "call_xp_xp"): my_amp, - }, - ) - - def test_full_key(self): - """Test value assignment with full key""" - sched = self.cals.get_schedule("xp", (2,), assign_params={("amp", (2,), "xp"): 0.1}) - - with pulse.build(name="xp") as expected: - pulse.play(Gaussian(160, 0.1, 40), DriveChannel(2)) - - self.assertEqual(sched, expected) - - def test_default_qubit(self): - """Test value assignment with default qubit""" - sched = self.cals.get_schedule("xp", (2,), assign_params={("amp", (), "xp"): 0.1}) - - with pulse.build(name="xp") as expected: - pulse.play(Gaussian(160, 0.1, 40), DriveChannel(2)) - - self.assertEqual(sched, expected) - - def test_default_across_qubits(self): - """Test assigning to multiple schedules through default parameter""" - sched = self.cals.get_schedule("xpxp", (2, 3), assign_params={("amp", (), "xp"): 0.4}) - sched = block_to_schedule(sched) - - with pulse.build(name="xpxp") as expected: - with pulse.align_left(): - pulse.play(Gaussian(160, 0.4, 40), DriveChannel(2)) - pulse.play(Gaussian(160, 0.4, 40), DriveChannel(3)) - - expected = block_to_schedule(expected) - - self.assertEqual(sched, expected) - - -class TestReplaceScheduleAndCall(QiskitExperimentsTestCase): - """A test to ensure that inconsistencies are picked up when a schedule is reassigned.""" - - def setUp(self): - """Create the setting to test.""" - super().setUp() - - self.cals = Calibrations(coupling_map=[]) - - self.amp = Parameter("amp") - self.dur = Parameter("duration") - self.sigma = Parameter("σ") - self.beta = Parameter("β") - self.ch0 = Parameter("ch0") - - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(self.dur, self.amp, self.sigma), DriveChannel(self.ch0)) - - with pulse.build(name="call_xp") as call_xp: - pulse.reference(xp.name, "q0") - - self.cals.add_schedule(xp, num_qubits=1) - self.cals.add_schedule(call_xp, num_qubits=1) - - self.cals.add_parameter_value(0.2, "amp", (4,), "xp") - self.cals.add_parameter_value(160, "duration", (4,), "xp") - self.cals.add_parameter_value(40, "σ", (), "xp") - - def test_calibration_save_json(self): - """Test that the calibration under test can be serialized through JSON.""" - filename = self.__class__.__name__ - - try: - self.cals.save(file_type="json", file_prefix=filename) - loaded = self.cals.load(file_path=f"{filename}.json") - self.assertEqual(self.cals, loaded) - finally: - if os.path.exists(f"{filename}.json"): - os.remove(f"{filename}.json") - - def test_reference_replaced(self): - """Test that we get an error when there is an inconsistency in subroutines.""" - - sched = self.cals.get_schedule("call_xp", (4,)) - - with pulse.build(name="xp") as expected: - pulse.play(Gaussian(160, 0.2, 40), DriveChannel(4)) - - self.assertEqual(block_to_schedule(sched), block_to_schedule(expected)) - - # Now update the xp pulse without updating the call_xp schedule and ensure consistency. - with pulse.build(name="xp") as drag: - pulse.play(Drag(self.dur, self.amp, self.sigma, self.beta), DriveChannel(self.ch0)) - - self.cals.add_schedule(drag, num_qubits=1) - self.cals.add_parameter_value(10.0, "β", (4,), "xp") - - sched = self.cals.get_schedule("call_xp", (4,)) - - with pulse.build(name="xp") as expected: - pulse.play(Drag(160, 0.2, 40, 10.0), DriveChannel(4)) - - self.assertEqual(block_to_schedule(sched), block_to_schedule(expected)) - - -class TestCoupledAssigning(QiskitExperimentsTestCase): - """Test that assigning parameters works when they are coupled in calls.""" - - def setUp(self): - """Create the setting to test.""" - super().setUp() - - controls = {(3, 2): [ControlChannel(10)]} - coupling_map = [[2, 3], [3, 2]] - self.cals = Calibrations(coupling_map=coupling_map, control_channel_map=controls) - - self.amp_cr = Parameter("amp") - self.amp_xp = Parameter("amp") - self.ch0 = Parameter("ch0") - self.d0_ = DriveChannel(self.ch0) - self.ch1 = Parameter("ch1") - self.d1_ = DriveChannel(self.ch1) - self.c1_ = ControlChannel(Parameter("ch0.1")) - self.sigma = Parameter("σ") - self.width = Parameter("w") - self.dur = Parameter("duration") - - with pulse.build(name="cr_p") as cr_p: - pulse.play(GaussianSquare(self.dur, self.amp_cr, self.sigma, self.width), self.c1_) - - with pulse.build(name="cr_m") as cr_m: - pulse.play(GaussianSquare(self.dur, -self.amp_cr, self.sigma, self.width), self.c1_) - - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(160, self.amp_xp, self.sigma), self.d0_) - - with pulse.build(name="ecr") as ecr: - with pulse.align_sequential(): - pulse.reference(cr_p.name, "q0", "q1") - pulse.reference(xp.name, "q0") - pulse.reference(cr_m.name, "q0", "q1") - - with pulse.build(name="cr_echo_both") as cr_echo_both: - with pulse.align_sequential(): - pulse.reference(cr_p.name, "q0", "q1") - with pulse.align_left(): - pulse.reference(xp.name, "q0") - pulse.reference(xp.name, "q1") - pulse.reference(cr_m.name, "q0", "q1") - - self.cals.add_schedule(cr_p, num_qubits=2) - self.cals.add_schedule(cr_m, num_qubits=2) - self.cals.add_schedule(xp, num_qubits=1) - self.cals.add_schedule(ecr, num_qubits=2) - self.cals.add_schedule(cr_echo_both, num_qubits=2) - - self.cals.add_parameter_value(0.3, "amp", (3, 2), "cr_p") - self.cals.add_parameter_value(0.2, "amp", (3,), "xp") - self.cals.add_parameter_value(0.4, "amp", (2,), "xp") - self.cals.add_parameter_value(40, "σ", (), "xp") - self.cals.add_parameter_value(640, "w", (3, 2), "cr_p") - self.cals.add_parameter_value(800, "duration", (3, 2), "cr_p") - - def test_calibration_save_json(self): - """Test that the calibration under test can be serialized through JSON.""" - filename = self.__class__.__name__ - - try: - self.cals.save(file_type="json", file_prefix=filename) - loaded = self.cals.load(file_path=f"{filename}.json") - self.assertEqual(self.cals, loaded) - finally: - if os.path.exists(f"{filename}.json"): - os.remove(f"{filename}.json") - - def test_assign_coupled_explicitly(self): - """Test that we get the proper schedules when they are coupled.""" - - # Test that we can preserve the coupling - my_amp = Parameter("my_amp") - assign_params = {("amp", (3, 2), "cr_p"): my_amp, ("amp", (3, 2), "cr_m"): my_amp} - sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) - sched = block_to_schedule(sched) - - with pulse.build(name="ecr") as expected: - with pulse.align_sequential(): - pulse.play(GaussianSquare(800, my_amp, 40, 640), ControlChannel(10)) - pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) - pulse.play(GaussianSquare(800, -my_amp, 40, 640), ControlChannel(10)) - - expected = block_to_schedule(expected) - - self.assertEqual(sched, expected) - - def test_assign_coupled_implicitly_float(self): - """Test that we get the proper schedules when they are coupled.""" - assign_params = {("amp", (3, 2), "cr_m"): 0.8} - sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) - sched = block_to_schedule(sched) - - with pulse.build(name="ecr") as expected: - with pulse.align_sequential(): - pulse.play(GaussianSquare(800, 0.8, 40, 640), ControlChannel(10)) - pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) - pulse.play(GaussianSquare(800, -0.8, 40, 640), ControlChannel(10)) - - expected = block_to_schedule(expected) - - self.assertEqual(sched, expected) - - def test_assign_coupled_implicitly(self): - """Test that we get the proper schedules when they are coupled.""" - my_amp = Parameter("my_amp") - assign_params = {("amp", (3, 2), "cr_p"): my_amp} - sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) - sched = block_to_schedule(sched) - - with pulse.build(name="ecr") as expected: - with pulse.align_sequential(): - pulse.play(GaussianSquare(800, my_amp, 40, 640), ControlChannel(10)) - pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) - pulse.play(GaussianSquare(800, -my_amp, 40, 640), ControlChannel(10)) - - expected = block_to_schedule(expected) - - self.assertEqual(sched, expected) - - def test_break_coupled(self): - """Test that we get the proper schedules when they are coupled.""" - my_amp = Parameter("my_amp") - my_amp2 = Parameter("my_amp2") - assign_params = {("amp", (3, 2), "cr_p"): my_amp, ("amp", (3, 2), "cr_m"): my_amp2} - sched = self.cals.get_schedule("ecr", (3, 2), assign_params=assign_params) - sched = block_to_schedule(sched) - - with pulse.build(name="ecr") as expected: - with pulse.align_sequential(): - pulse.play(GaussianSquare(800, my_amp, 40, 640), ControlChannel(10)) - pulse.play(Gaussian(160, 0.2, 40), DriveChannel(3)) - pulse.play(GaussianSquare(800, -my_amp2, 40, 640), ControlChannel(10)) - - expected = block_to_schedule(expected) - - self.assertEqual(sched, expected) - - def test_assign_coupled_explicitly_two_channel(self): - """Test that we get the proper schedules when they are coupled.""" - - # Test that we can preserve the coupling - my_amp = Parameter("my_amp") - my_amp2 = Parameter("my_amp2") - assign_params = {("amp", (3,), "xp"): my_amp, ("amp", (2,), "xp"): my_amp2} - sched = self.cals.get_schedule("cr_echo_both", (3, 2), assign_params=assign_params) - sched = block_to_schedule(sched) - - with pulse.build(name="cr_echo_both") as expected: - with pulse.align_sequential(): - pulse.play(GaussianSquare(800, 0.3, 40, 640), ControlChannel(10)) - with pulse.align_left(): - pulse.play(Gaussian(160, my_amp, 40), DriveChannel(3)) - pulse.play(Gaussian(160, my_amp2, 40), DriveChannel(2)) - pulse.play(GaussianSquare(800, -0.3, 40, 640), ControlChannel(10)) - - expected = block_to_schedule(expected) - - self.assertEqual(sched, expected) - - -class TestFiltering(QiskitExperimentsTestCase): - """Test that the filtering works as expected.""" - - def setUp(self): - """Setup a calibration.""" - super().setUp() - - self.cals = Calibrations(coupling_map=[]) - - self.sigma = Parameter("σ") - self.amp = Parameter("amp") - self.drive = DriveChannel(Parameter("ch0")) - - # Define and add template schedules. - with pulse.build(name="xp") as xp: - pulse.play(Gaussian(160, self.amp, self.sigma), self.drive) - - self.cals.add_schedule(xp, num_qubits=1) - - self.date_time1 = datetime.strptime("15/09/19 10:21:35", "%d/%m/%y %H:%M:%S") - self.date_time2 = datetime.strptime("15/09/19 11:21:35", "%d/%m/%y %H:%M:%S") - - self.cals.add_parameter_value(ParameterValue(40, self.date_time1), "σ", schedule="xp") - self.cals.add_parameter_value( - ParameterValue(45, self.date_time2, False), "σ", schedule="xp" - ) - self.cals.add_parameter_value(ParameterValue(0.1, self.date_time1), "amp", (0,), "xp") - self.cals.add_parameter_value(ParameterValue(0.2, self.date_time2), "amp", (0,), "xp") - self.cals.add_parameter_value( - ParameterValue(0.4, self.date_time2, group="super_cal"), "amp", (0,), "xp" - ) - - def test_calibration_save_json(self): - """Test that the calibration under test can be serialized through JSON.""" - filename = self.__class__.__name__ - - try: - self.cals.save(file_type="json", file_prefix=filename) - loaded = self.cals.load(file_path=f"{filename}.json") - self.assertEqual(self.cals, loaded) - finally: - if os.path.exists(f"{filename}.json"): - os.remove(f"{filename}.json") - - def test_parameter_table_most_recent(self): - """Test the most_recent argument to the parameter_table method.""" - - table = self.cals.parameters_table(parameters=["amp"], most_recent_only=False) - self.assertTrue(len(table["data"]), 2) - - table = self.cals.parameters_table(parameters=["amp"], most_recent_only=True) - self.assertTrue(len(table["data"]), 1) - self.assertTrue(table["data"][0]["value"], 0.2) - - def test_get_parameter_value(self): - """Test that getting parameter values functions properly.""" - - amp = self.cals.get_parameter_value(self.amp, (0,), "xp") - self.assertEqual(amp, 0.2) - - amp = self.cals.get_parameter_value(self.amp, (0,), "xp", group="super_cal") - self.assertEqual(amp, 0.4) - - cutoff_date = datetime.strptime("15/09/19 11:21:34", "%d/%m/%y %H:%M:%S") - amp = self.cals.get_parameter_value(self.amp, (0,), "xp", cutoff_date=cutoff_date) - self.assertEqual(amp, 0.1) - - sigma = self.cals.get_parameter_value(self.sigma, (0,), "xp") - self.assertEqual(sigma, 40) - - sigma = self.cals.get_parameter_value(self.sigma, (0,), "xp", valid_only=False) - self.assertEqual(sigma, 45) - - -class TestSavingAndLoading(CrossResonanceTest): - """Test that calibrations can be saved and loaded to and from files.""" - - def setUp(self): - """Setup the test.""" - self._prefix = str(uuid.uuid4()) - super().setUp() - - def tearDown(self): - """Clean-up after the test.""" - super().tearDown() - - for file in ["parameter_values.csv", "parameter_config.csv", "schedules.csv", ".json"]: - if os.path.exists(self._prefix + file): - os.remove(self._prefix + file) - - def test_save_load_parameter_values_csv(self): - """Test that we can save and load parameter values.""" - # NOTE: This is a legacy test that can be removed when csv support is - # removed from Calibrations.save - - # Expect user warning about schedules, deprecation warning about csv - with self.assertWarns((UserWarning, DeprecationWarning)): - self.cals.save("csv", overwrite=True, file_prefix=self._prefix) - self.assertEqual(self.cals.get_parameter_value("amp", (3,), "xp"), 0.1) - - self.cals._params = defaultdict(list) - - with self.assertRaises(CalibrationError): - self.cals.get_parameter_value("amp", (3,), "xp") - - # Load the parameters, check value and type. - with self.assertWarns(DeprecationWarning): - self.cals.load_parameter_values(self._prefix + "parameter_values.csv") - - val = self.cals.get_parameter_value("amp", (3,), "xp") - self.assertEqual(val, 0.1) - - val = self.cals.get_parameter_value("σ", (3,), "xp") - self.assertEqual(val, 40) - self.assertTrue(isinstance(val, int)) - - val = self.cals.get_parameter_value("amp", (3, 2), "cr") - self.assertEqual(val, 0.3) - self.assertTrue(isinstance(val, float)) - - # Check that we cannot rewrite files as they already exist. - with self.assertRaises(CalibrationError): - with self.assertWarns((UserWarning, DeprecationWarning)): - self.cals.save("csv", file_prefix=self._prefix) - - with self.assertWarns((UserWarning, DeprecationWarning)): - self.cals.save("csv", overwrite=True, file_prefix=self._prefix) - - def test_alternate_date_formats(self): - """Test that we can reload dates with or without time-zone.""" - - new_date = datetime.strptime("16/09/20 10:21:35.012+0200", "%d/%m/%y %H:%M:%S.%f%z") - value = ParameterValue(0.222, date_time=new_date) - self.cals.add_parameter_value(value, "amp", (3,), "xp") - - self.cals.save("json", overwrite=True, file_prefix=self._prefix) - self.cals.load(self._prefix + ".json") - - def test_save_load_library_csv(self): - """Test that we can load and save a library. - - These libraries contain both parameters with schedules and parameters without - any schedules (e.g. frequencies for qubits and readout). - """ - - library = FixedFrequencyTransmon() - backend = FakeArmonkV2() - cals = Calibrations.from_backend(backend, libraries=[library]) - - cals.parameters_table() - - with self.assertWarns((UserWarning, DeprecationWarning)): - cals.save(file_type="csv", overwrite=True, file_prefix=self._prefix) - - with self.assertWarns(DeprecationWarning): - cals.load_parameter_values(self._prefix + "parameter_values.csv") - - # Test the value of a few loaded params. - self.assertEqual(cals.get_parameter_value("amp", (0,), "x"), 0.5) - self.assertEqual( - cals.get_parameter_value("drive_freq", (0,)), - BackendData(backend).drive_freqs[0], - ) - - def test_save_load_library(self): - """Test that we can load and save a library. - - These libraries contain both parameters with schedules and parameters without - any schedules (e.g. frequencies for qubits and readout). - """ - - library = FixedFrequencyTransmon() - backend = FakeArmonkV2() - cals = Calibrations.from_backend(backend, libraries=[library]) - - cals.parameters_table() - - cals.save(file_type="json", overwrite=True, file_prefix=self._prefix) - - loaded = Calibrations.load(self._prefix + ".json") - - # Test the value of a few loaded params. - self.assertEqual(loaded.get_parameter_value("amp", (0,), "x"), 0.5) - self.assertEqual( - loaded.get_parameter_value("drive_freq", (0,)), - BackendData(backend).drive_freqs[0], - ) - - def test_json_round_trip(self): - """Test round trip test for JSON file format. - - This method guarantees full equality including parameterized template schedules - and we can still generate schedules with loaded calibration instance, - even though calibrations is instantiated outside built-in library. - """ - self.cals.save(file_type="json", overwrite=True, file_prefix=self._prefix) - loaded = self.cals.load(file_path=self._prefix + ".json") - self.assertEqual(self.cals, loaded) - - original_sched = self.cals.get_schedule("cr", (3, 2)) - roundtrip_sched = loaded.get_schedule("cr", (3, 2)) - self.assertEqual(original_sched, roundtrip_sched) - - def test_overwrite(self): - """Test that overwriting errors unless overwrite flag is used""" - self.cals.save(file_type="json", overwrite=True, file_prefix=self._prefix) - with self.assertRaises(CalibrationError): - self.cals.save(file_type="json", overwrite=False, file_prefix=self._prefix) - - # Add a value to make sure data is really overwritten and not carried - # over from first write - self.cals.add_parameter_value(0.45, "amp", (3,), "xp") - self.cals.save(file_type="json", overwrite=True, file_prefix=self._prefix) - loaded = Calibrations.load(file_path=self._prefix + ".json") - self.assertEqual(self.cals, loaded) - - -class TestInstructionScheduleMap(QiskitExperimentsTestCase): - """Class to test the functionality of a Calibrations""" - - def test_setup_withLibrary(self): - """Test that we can setup with a library.""" - - cals = Calibrations.from_backend( - FakeArmonkV2(), - libraries=[ - FixedFrequencyTransmon(basis_gates=["x", "sx"], default_values={"duration": 320}) - ], - ) - - # Check the x gate - with pulse.build(name="x") as expected: - pulse.play(pulse.Drag(duration=320, amp=0.5, sigma=80, beta=0), pulse.DriveChannel(0)) - - self.assertEqual(cals.get_schedule("x", (0,)), expected) - - # Check the sx gate - with pulse.build(name="sx") as expected: - pulse.play(pulse.Drag(duration=320, amp=0.25, sigma=80, beta=0), pulse.DriveChannel(0)) - - self.assertEqual(cals.get_schedule("sx", (0,)), expected) - - def test_instruction_schedule_map_export(self): - """Test that exporting the inst map works as planned.""" - - backend = FakeBelemV2() - - cals = Calibrations.from_backend( - backend, - libraries=[FixedFrequencyTransmon(basis_gates=["sx"])], - ) - - u_chan = pulse.ControlChannel(Parameter("ch0.1")) - with pulse.build(name="cr") as cr: - pulse.play(pulse.GaussianSquare(640, 0.5, 64, 384), u_chan) - - cals.add_schedule(cr, num_qubits=2) - cals.update_inst_map({"cr"}) - - for qubit in range(BackendData(backend).num_qubits): - self.assertTrue(cals.default_inst_map.has("sx", (qubit,))) - - # based on coupling map of Belem to keep the test robust. - expected_pairs = [(0, 1), (1, 0), (1, 2), (2, 1), (1, 3), (3, 1), (3, 4), (4, 3)] - coupling_map = set(tuple(pair) for pair in BackendData(backend).coupling_map) - - for pair in expected_pairs: - self.assertTrue(pair in coupling_map) - self.assertTrue(cals.default_inst_map.has("cr", pair), pair) - - def test_inst_map_transpilation(self): - """Test that we can use the inst_map to inject the cals into the circuit.""" - - cals = Calibrations.from_backend( - FakeArmonkV2(), - libraries=[FixedFrequencyTransmon(basis_gates=["x"])], - ) - - param = Parameter("amp") - cals.inst_map_add("Rabi", (0,), "x", assign_params={"amp": param}) - - circ = QuantumCircuit(1) - circ.x(0) - circ.append(Gate("Rabi", num_qubits=1, params=[param]), (0,)) - - circs, amps = [], [0.12, 0.25] - - for amp in amps: - new_circ = circ.assign_parameters({param: amp}, inplace=False) - circs.append(new_circ) - - # Check that calibrations are absent - for circ in circs: - self.assertEqual(len(circ.calibrations), 0) - - # Transpile to inject the cals. - circs = transpile(circs, inst_map=cals.default_inst_map) - - # Check that we have the expected schedules. - with pulse.build() as x_expected: - pulse.play(pulse.Drag(160, 0.5, 40, 0), pulse.DriveChannel(0)) - - for idx, circ in enumerate(circs): - amp = amps[idx] - - with pulse.build() as rabi_expected: - pulse.play(pulse.Drag(160, amp, 40, 0), pulse.DriveChannel(0)) - - self.assertEqual(circ.calibrations["x"][((0,), ())], x_expected) - - circ_rabi = next(iter(circ.calibrations["Rabi"].values())) - self.assertEqual(circ_rabi, rabi_expected) - - # Test the removal of the Rabi instruction - self.assertTrue(cals.default_inst_map.has("Rabi", (0,))) - - cals.default_inst_map.remove("Rabi", (0,)) - - self.assertFalse(cals.default_inst_map.has("Rabi", (0,))) - - def test_inst_map_updates(self): - """Test that updating a parameter will force an inst map update.""" - - cals = Calibrations.from_backend( - FakeBelemV2(), - libraries=[FixedFrequencyTransmon(basis_gates=["sx", "x"])], - ) - - # Test the schedules before the update. - for qubit in range(5): - for gate, amp in [("x", 0.5), ("sx", 0.25)]: - with pulse.build() as expected: - pulse.play(pulse.Drag(160, amp, 40, 0), pulse.DriveChannel(qubit)) - - self.assertEqual(cals.default_inst_map.get(gate, qubit), expected) - - # Update the duration, this should impact all gates. - cals.add_parameter_value(200, "duration", schedule="sx") - - # Test that all schedules now have an updated duration in the inst_map - for qubit in range(5): - for gate, amp in [("x", 0.5), ("sx", 0.25)]: - with pulse.build() as expected: - pulse.play(pulse.Drag(200, amp, 40, 0), pulse.DriveChannel(qubit)) - - self.assertEqual(cals.default_inst_map.get(gate, qubit), expected) - - # Update the amp on a single qubit, this should only update one gate in the inst_map - cals.add_parameter_value(0.8, "amp", qubits=(4,), schedule="sx") - - # Test that all schedules now have an updated duration in the inst_map - for qubit in range(5): - for gate, amp in [("x", 0.5), ("sx", 0.25)]: - - if gate == "sx" and qubit == 4: - amp = 0.8 - - with pulse.build() as expected: - pulse.play(pulse.Drag(200, amp, 40, 0), pulse.DriveChannel(qubit)) - - self.assertEqual(cals.default_inst_map.get(gate, qubit), expected) - - def test_cx_cz_case(self): - """Test the case where the coupling map has CX and CZ on different qubits. - - We use FakeBelem which has a linear coupling map and will restrict ourselves to - qubits 0, 1, and 2. The Cals will define a template schedule for CX and CZ. We will - mock this with GaussianSquare and Gaussian pulses since the nature of the schedules - is irrelevant here. The parameters for CX will only have values for qubits 0 and 1 while - the parameters for CZ will only have values for qubits 1 and 2. We therefore will have - a CX on qubits 0, 1 in the inst. map and a CZ on qubits 1, 2. - """ - - cals = Calibrations.from_backend(FakeBelemV2()) - - sig = Parameter("σ") - dur = Parameter("duration") - width = Parameter("width") - amp_cx = Parameter("amp") - amp_cz = Parameter("amp") - uchan = Parameter("ch1.0") - - with pulse.build(name="cx") as cx: - pulse.play( - pulse.GaussianSquare(duration=dur, amp=amp_cx, sigma=sig, width=width), - pulse.ControlChannel(uchan), - ) - - with pulse.build(name="cz") as cz: - pulse.play( - pulse.Gaussian(duration=dur, amp=amp_cz, sigma=sig), pulse.ControlChannel(uchan) - ) - - cals.add_schedule(cx, num_qubits=2) - cals.add_schedule(cz, num_qubits=2) - - cals.add_parameter_value(640, "duration", schedule="cx") - cals.add_parameter_value(64, "σ", schedule="cx") - cals.add_parameter_value(320, "width", qubits=(0, 1), schedule="cx") - cals.add_parameter_value(320, "width", qubits=(1, 0), schedule="cx") - cals.add_parameter_value(0.1, "amp", qubits=(0, 1), schedule="cx") - cals.add_parameter_value(0.8, "amp", qubits=(1, 0), schedule="cx") - cals.add_parameter_value(0.1, "amp", qubits=(2, 1), schedule="cz") - cals.add_parameter_value(0.8, "amp", qubits=(1, 2), schedule="cz") - - # CX only defined for qubits (0, 1) and (1,0)? - self.assertTrue(cals.default_inst_map.has("cx", (0, 1))) - self.assertTrue(cals.default_inst_map.has("cx", (1, 0))) - self.assertFalse(cals.default_inst_map.has("cx", (2, 1))) - self.assertFalse(cals.default_inst_map.has("cx", (1, 2))) - - # CZ only defined for qubits (2, 1) and (1,2)? - self.assertTrue(cals.default_inst_map.has("cz", (2, 1))) - self.assertTrue(cals.default_inst_map.has("cz", (1, 2))) - self.assertFalse(cals.default_inst_map.has("cz", (0, 1))) - self.assertFalse(cals.default_inst_map.has("cz", (1, 0))) - - def test_alternate_initialization(self): - """Test that we can initialize without a backend object.""" - - backend = FakeBelemV2() - library = FixedFrequencyTransmon(basis_gates=["sx", "x"]) - - backend_data = BackendData(backend) - control_channel_map = {} - for qargs in backend_data.coupling_map: - control_channel_map[tuple(qargs)] = backend_data.control_channel(qargs) - - cals1 = Calibrations.from_backend(backend, libraries=[library]) - cals2 = Calibrations( - libraries=[library], - control_channel_map=control_channel_map, - coupling_map=backend_data.coupling_map, - ) - - self.assertEqual(str(cals1.get_schedule("x", 1)), str(cals2.get_schedule("x", 1))) - - -class TestSerialization(QiskitExperimentsTestCase): - """Test the serialization of the Calibrations.""" - - def test_serialization(self): - """Test the serialization.""" - - backend = FakeBelemV2() - library = FixedFrequencyTransmon(basis_gates=["sx", "x"]) - - cals = Calibrations.from_backend(backend, libraries=[library]) - cals.add_parameter_value(0.12345, "amp", 3, "x") - - self.assertRoundTripSerializable(cals) diff --git a/test/calibration/test_setup_library.py b/test/calibration/test_setup_library.py deleted file mode 100644 index 12900dbf38..0000000000 --- a/test/calibration/test_setup_library.py +++ /dev/null @@ -1,238 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Class to test the calibrations setup methods.""" - -from typing import Dict, Set -import json - -from test.base import QiskitExperimentsTestCase -from qiskit import pulse -from numpy import pi - -from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon -from qiskit_experiments.calibration_management.calibration_key_types import DefaultCalValue -from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.framework.json import ExperimentEncoder, ExperimentDecoder - - -class MutableTestLibrary(FixedFrequencyTransmon): - """A subclass designed for test_hash_warn. - - This class ensures that FixedFrequencyTransmon is preserved if anything goes wrong - with the serialization :meth:`in test_hash_warn`. - """ - - def _build_schedules(self, basis_gates: Set[str]) -> Dict[str, pulse.ScheduleBlock]: - """Dummy schedule building.""" - with pulse.build(name="x") as schedule: - pulse.play(pulse.Drag(160, 0.1, 40, 0), pulse.DriveChannel(0)) - - schedules = {} - if "x" in basis_gates: - schedules["x"] = schedule - - return schedules - - -class TestFixedFrequencyTransmon(QiskitExperimentsTestCase): - """Test the various setup methods.""" - - def test_standard_single_qubit_gates(self): - """Test the setup of single-qubit gates.""" - - library = FixedFrequencyTransmon(default_values={"duration": 320}) - - for gate in ["x", "sx"]: - sched = library[gate] - self.assertTrue(isinstance(sched, pulse.ScheduleBlock)) - self.assertEqual(len(sched.parameters), 6) - - sched_x = library["x"] - sched_y = library["y"] - sched_sx = library["sx"] - sched_sy = library["sy"] - - self.assertEqual(sched_x.blocks[0].pulse.duration, sched_sx.blocks[0].pulse.duration) - self.assertEqual(sched_x.blocks[0].pulse.sigma, sched_sx.blocks[0].pulse.sigma) - - self.assertEqual(len(set(sched_x.parameters) & set(sched_y.parameters)), 5) - self.assertEqual(len(set(sched_sx.parameters) & set(sched_sy.parameters)), 5) - - expected = [ - DefaultCalValue(0.5, "amp", (), "x"), - DefaultCalValue(0.0, "β", (), "x"), - DefaultCalValue(320, "duration", (), "x"), - DefaultCalValue(80, "σ", (), "x"), - DefaultCalValue(0.0, "angle", (), "x"), - DefaultCalValue(320, "duration", (), "sx"), - DefaultCalValue(0.0, "β", (), "sx"), - DefaultCalValue(0.25, "amp", (), "sx"), - DefaultCalValue(80, "σ", (), "sx"), - DefaultCalValue(0.0, "angle", (), "sx"), - ] - - for param_conf in library.default_values(): - self.assertTrue(param_conf in expected) - - # Check that an error gets raise if the gate is not in the library. - with self.assertRaises(CalibrationError): - print(library["bswap"]) - - # Test the basis gates of the library. - self.assertListEqual(library.basis_gates, ["x", "y", "sx", "sy"]) - - def test_unlinked_parameters(self): - """Test that we get schedules with unlinked parameters.""" - - library = FixedFrequencyTransmon(link_parameters=False) - - sched_x = library["x"] - sched_y = library["y"] - sched_sx = library["sx"] - sched_sy = library["sy"] - - # Test the number of parameters. - self.assertEqual(len(set(sched_x.parameters) & set(sched_y.parameters)), 2) - self.assertEqual(len(set(sched_sx.parameters) & set(sched_sy.parameters)), 2) - - expected = [ - DefaultCalValue(0.5, "amp", (), "x"), - DefaultCalValue(0.0, "β", (), "x"), - DefaultCalValue(160, "duration", (), "x"), - DefaultCalValue(40, "σ", (), "x"), - DefaultCalValue(0.0, "angle", (), "x"), - DefaultCalValue(160, "duration", (), "sx"), - DefaultCalValue(0.0, "β", (), "sx"), - DefaultCalValue(0.25, "amp", (), "sx"), - DefaultCalValue(40, "σ", (), "sx"), - DefaultCalValue(0.0, "angle", (), "sx"), - DefaultCalValue(0.5, "amp", (), "y"), - DefaultCalValue(0.0, "β", (), "y"), - DefaultCalValue(160, "duration", (), "y"), - DefaultCalValue(40, "σ", (), "y"), - DefaultCalValue(pi / 2, "angle", (), "y"), - DefaultCalValue(160, "duration", (), "sy"), - DefaultCalValue(0.0, "β", (), "sy"), - DefaultCalValue(0.25, "amp", (), "sy"), - DefaultCalValue(40, "σ", (), "sy"), - DefaultCalValue(pi / 2, "angle", (), "sy"), - ] - - self.assertSetEqual(set(library.default_values()), set(expected)) - - def test_setup_partial_gates(self): - """Check that we do not setup all gates if not required.""" - - library = FixedFrequencyTransmon(basis_gates=["x", "sy"]) - - self.assertTrue("x" in library) - self.assertTrue("sy" in library) - self.assertTrue("y" not in library) - self.assertTrue("sx" not in library) - - with self.assertRaises(CalibrationError): - FixedFrequencyTransmon(basis_gates=["x", "bswap"]) - - def test_serialization(self): - """Test the serialization of the object.""" - - lib1 = FixedFrequencyTransmon( - basis_gates=["x", "sy"], - default_values={"duration": 320}, - link_parameters=False, - ) - - lib2 = FixedFrequencyTransmon.from_config(lib1.config()) - - self.assertEqual(lib2.basis_gates, lib1.basis_gates) - - # Note: we convert to string since the parameters prevent a direct comparison. - self.assertTrue(self._test_library_equivalence(lib1, lib2)) - - # Test that the extra args are properly accounted for. - lib3 = FixedFrequencyTransmon( - basis_gates=["x", "sy"], - default_values={"duration": 320}, - link_parameters=True, - ) - - self.assertFalse(self._test_library_equivalence(lib1, lib3)) - - def test_json_serialization(self): - """Test that the library can be serialized using JSon.""" - - lib1 = FixedFrequencyTransmon( - basis_gates=["x", "sy"], - default_values={"duration": 320}, - link_parameters=False, - ) - - # Test that serialization fails without the right encoder - with self.assertRaises(TypeError): - json.dumps(lib1) - - # Test that serialization works with the proper library - lib_data = json.dumps(lib1, cls=ExperimentEncoder) - lib2 = json.loads(lib_data, cls=ExperimentDecoder) - - self.assertTrue(self._test_library_equivalence(lib1, lib2)) - - def test_hash_warn(self): - """Test that a warning is raised when the hash of the library is different. - - This test mimics the behaviour of the following workflow: - 1. A user serializes a library. - 2. Changes to the class of the library are made. - 3. The user deserializes the library with the changed class. - 4. A warning is raised since the class definition has changed. - """ - - lib1 = MutableTestLibrary() - lib_data = json.dumps(lib1, cls=ExperimentEncoder) - lib2 = json.loads(lib_data, cls=ExperimentDecoder) - - self.assertTrue(self._test_library_equivalence(lib1, lib2)) - - # stash method build schedules to avoid other tests from failing - build_schedules = MutableTestLibrary._build_schedules - - def _my_build_schedules(): - """A dummy function to change the class behaviour.""" - pass - - # Change the schedule behaviour - MutableTestLibrary._build_schedules = _my_build_schedules - - with self.assertWarns(UserWarning): - try: - json.loads(lib_data, cls=ExperimentDecoder) - finally: - MutableTestLibrary._build_schedules = build_schedules - - def _test_library_equivalence(self, lib1, lib2) -> bool: - """Test if libraries are equivalent. - - Two libraries are equivalent if they have the same basis gates and - if the strings of the schedules are equal. We cannot directly compare - the schedules because the parameter objects in them will be different - instances. - """ - - if len(set(lib1.basis_gates)) != len(set(lib2.basis_gates)): - return False - - for gate in lib1.basis_gates: - if str(lib1[gate]) != str(lib2[gate]): - return False - - return True diff --git a/test/calibration/test_update_library.py b/test/calibration/test_update_library.py deleted file mode 100644 index 2edd851c22..0000000000 --- a/test/calibration/test_update_library.py +++ /dev/null @@ -1,62 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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 the calibration update library.""" -from test.base import QiskitExperimentsTestCase -import numpy as np - -from qiskit.qobj.utils import MeasLevel -from qiskit_ibm_runtime.fake_provider import FakeAthensV2 - -from qiskit_experiments.framework import BackendData -from qiskit_experiments.library import QubitSpectroscopy -from qiskit_experiments.calibration_management.calibrations import Calibrations -from qiskit_experiments.calibration_management.update_library import Frequency -from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon -from qiskit_experiments.test.mock_iq_backend import MockIQBackend -from qiskit_experiments.test.mock_iq_helpers import MockIQSpectroscopyHelper as SpectroscopyHelper - - -class TestFrequencyUpdate(QiskitExperimentsTestCase): - """Test the frequency update function in the update library.""" - - def test_frequency(self): - """Test calibrations update from spectroscopy.""" - - qubit = 1 - peak_offset = 5.0e6 - backend = MockIQBackend( - experiment_helper=SpectroscopyHelper( - freq_offset=peak_offset, - iq_cluster_centers=[((-1.0, -1.0), (1.0, 1.0))], - iq_cluster_width=[0.2], - ), - ) - - freq01 = BackendData(backend).drive_freqs[qubit] - frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) - - spec = QubitSpectroscopy([qubit], frequencies) - spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) - exp_data = spec.run(backend) - self.assertExperimentDone(exp_data) - result = exp_data.analysis_results("f01") - value = result.value.n - - self.assertTrue(freq01 + peak_offset - 2e6 < value < freq01 + peak_offset + 2e6) - self.assertEqual(result.quality, "good") - - # Test the integration with the Calibrations - cals = Calibrations.from_backend(FakeAthensV2(), libraries=[FixedFrequencyTransmon()]) - self.assertNotEqual(cals.get_parameter_value("drive_freq", qubit), value) - Frequency.update(cals, exp_data) - self.assertEqual(cals.get_parameter_value("drive_freq", qubit), value) diff --git a/test/calibration/utils.py b/test/calibration/utils.py deleted file mode 100644 index 2f9dd7b96b..0000000000 --- a/test/calibration/utils.py +++ /dev/null @@ -1,127 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Utility to test calibration module.""" - -import datetime -from typing import Optional, Sequence - -from qiskit import QuantumCircuit -from qiskit.providers import Backend - -from qiskit_experiments.calibration_management import ( - BaseCalibrationExperiment, - ParameterValue, - Calibrations, -) -from qiskit_experiments.framework import BaseExperiment, BaseAnalysis, AnalysisResultData - - -class DoNothingAnalysis(BaseAnalysis): - """Analysis just returns return_value set in the options.""" - - @classmethod - def _default_options(cls): - options = super()._default_options() - options.return_value = None - return options - - def _run_analysis( - self, - experiment_data, - ): - ret = AnalysisResultData( - name="return_value", - value=self.options.return_value, - ) - return [ret], [] - - -class DoNothingExperiment(BaseExperiment): - """Experiment doesn't provide any circuit to run.""" - - def __init__( - self, - physical_qubits: Sequence[int], - return_value: float, - circuits: Optional[Sequence[QuantumCircuit]] = None, - backend: Optional[Backend] = None, - ): - super().__init__( - physical_qubits=physical_qubits, analysis=DoNothingAnalysis(), backend=backend - ) - self.analysis.set_options(return_value=return_value) - - if circuits is not None: - self._circuits = circuits - else: - self._circuits = [] - - def circuits(self): - return self._circuits - - -class MockCalExperiment(BaseCalibrationExperiment, DoNothingExperiment): - """Mock calibration experiment. - - This experiment only invokes update_calibrations method and - adds new entry to the Calibrations instance. - Added entry can be directly managed by the constructor arguments. - """ - - def __init__( - self, - physical_qubits: Sequence[int], - calibrations: Calibrations, - new_value: float, - param_name: str, - sched_name: str, - circuits: Optional[Sequence[QuantumCircuit]] = None, - backend: Optional[Backend] = None, - ): - """Create mock calibration experiment. - - Args: - qubits: Qubit to update calibration. - calibrations: Calibrations instance to update. - new_value: New parameter value obtained by the calibration experiment. - param_name: Name of parameter to update. - sched_name: Name of schedule to update. - circuits: List of QuantumCircuits for the circuits() method - backend: Backend to set on experiment - """ - super().__init__( - physical_qubits=physical_qubits, - calibrations=calibrations, - return_value=new_value, - circuits=circuits, - backend=backend, - ) - self.to_update = { - "param": param_name, - "qubits": physical_qubits, - "schedule": sched_name, - } - - def update_calibrations(self, experiment_data): - new_value = experiment_data.analysis_results("return_value", block=False).value - - param_value = ParameterValue( - value=new_value, - date_time=datetime.datetime.now(), - group="default", - exp_id="0123456789", - ) - self.calibrations.add_parameter_value(value=param_value, **self.to_update) - - def _attach_calibrations(self, circuit): - pass diff --git a/test/curve_analysis/test_oscillation_analysis.py b/test/curve_analysis/test_oscillation_analysis.py new file mode 100644 index 0000000000..8ef979530e --- /dev/null +++ b/test/curve_analysis/test_oscillation_analysis.py @@ -0,0 +1,119 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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 Rabi amplitude Experiment class.""" +from test.base import QiskitExperimentsTestCase +import numpy as np + +from qiskit import QuantumCircuit, transpile +from qiskit.qobj.utils import MeasLevel +from qiskit_aer import AerSimulator + +from qiskit_experiments.framework import ExperimentData + +from qiskit_experiments.curve_analysis.standard_analysis.oscillation import OscillationAnalysis +from qiskit_experiments.data_processing.data_processor import DataProcessor +from qiskit_experiments.data_processing.nodes import Probability +from qiskit_experiments.curve_analysis import ParameterRepr + + +class TestOscillationAnalysis(QiskitExperimentsTestCase): + """Class to test the fitting.""" + + def simulate_experiment_data(self, thetas, amplitudes, shots=1024): + """Generate experiment data for Rx rotations with an arbitrary amplitude calibration.""" + circuits = [] + for theta in thetas: + qc = QuantumCircuit(1) + qc.rx(theta, 0) + qc.measure_all() + circuits.append(qc) + + sim = AerSimulator() + circuits = transpile(circuits, sim) + job = sim.run(circuits, shots=shots, seed_simulator=10) + result = job.result() + data = [ + { + "counts": self._add_uncertainty(result.get_counts(i)), + "metadata": { + "xval": amplitudes[i], + "meas_level": MeasLevel.CLASSIFIED, + "meas_return": "avg", + }, + } + for i, theta in enumerate(thetas) + ] + return data + + @staticmethod + def _add_uncertainty(counts): + """Ensure that we always have a non-zero sigma in the test.""" + for label in ["0", "1"]: + if label not in counts: + counts[next(iter(counts))] -= 1 + counts[label] = 1 + + return counts + + def test_good_analysis(self): + """Test the Rabi analysis.""" + experiment_data = ExperimentData() + + thetas = np.linspace(-np.pi, np.pi, 31) + amplitudes = np.linspace(-0.25, 0.25, 31) + expected_rate, test_tol = 2.0, 0.2 + + experiment_data.add_data(self.simulate_experiment_data(thetas, amplitudes, shots=400)) + + data_processor = DataProcessor("counts", [Probability(outcome="1")]) + + analysis = OscillationAnalysis() + analysis.set_options( + result_parameters=[ParameterRepr("freq", "rabi_rate")], + ) + + experiment_data = analysis.run( + experiment_data, data_processor=data_processor, plot=False + ).block_for_results() + + result = experiment_data.analysis_results("rabi_rate") + self.assertEqual(result.quality, "good") + self.assertAlmostEqual(result.value, expected_rate, delta=test_tol) + + def test_bad_analysis(self): + """Test the Rabi analysis.""" + experiment_data = ExperimentData() + + # Change rotation angle with square root of amplitude so that + # population versus amplitude will not be sinusoidal and the fit will + # be bad. + thetas = np.sqrt(np.linspace(0.0, 4 * np.pi**2, 31)) + amplitudes = np.linspace(0.0, 0.95, 31) + + experiment_data.add_data(self.simulate_experiment_data(thetas, amplitudes, shots=200)) + + data_processor = DataProcessor("counts", [Probability(outcome="1")]) + + analysis = OscillationAnalysis() + analysis.set_options( + result_parameters=[ParameterRepr("freq", "rabi_rate")], + ) + experiment_data = analysis.run( + experiment_data, + data_processor=data_processor, + plot=False, + ).block_for_results() + + result = experiment_data.analysis_results("rabi_rate") + + self.assertEqual(result.quality, "bad") diff --git a/test/framework/test_backend_timing.py b/test/framework/test_backend_timing.py index c2384b405d..483d3d11bf 100644 --- a/test/framework/test_backend_timing.py +++ b/test/framework/test_backend_timing.py @@ -28,11 +28,8 @@ class TestBackendTiming(QiskitExperimentsTestCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.acquire_alignment = 16 cls.dt = 1 / 4.5e9 cls.granularity = 16 - cls.min_length = 64 - cls.pulse_alignment = 1 def setUp(self): super().setUp() @@ -41,9 +38,6 @@ def setUp(self): # with the values assumed for the unit tests. self.backend = FakeNairobiV2() self.backend.target.dt = self.dt - self.backend.target.acquire_alignment = self.acquire_alignment - self.backend.target.pulse_alignment = self.pulse_alignment - self.backend.target.min_length = self.min_length self.backend.target.granularity = self.granularity @data((True, "s"), (False, "dt")) @@ -63,14 +57,6 @@ def test_round_delay_args(self): with self.assertRaises(QiskitError): timing.round_delay() - def test_round_pulse_args(self): - """Test argument checking in round_pulse""" - timing = BackendTiming(self.backend) - with self.assertRaises(QiskitError): - timing.round_pulse(time=self.dt * 64, samples=64) - with self.assertRaises(QiskitError): - timing.round_pulse() - @data([14, 16], [16, 16], [18, 16], [64.5, 64]) @unpack def test_round_delay(self, samples_in, samples_out): @@ -95,22 +81,6 @@ def test_round_delay_samples_in(self, samples_in, samples_out): timing = BackendTiming(self.backend) self.assertEqual(timing.round_delay(samples=samples_in), samples_out) - @data([12, 64], [65, 64], [79, 80], [83, 80]) - @unpack - def test_round_pulse(self, samples_in, samples_out): - """Test round pulse calculation with time input""" - time = self.dt * samples_in - - timing = BackendTiming(self.backend) - self.assertEqual(timing.round_pulse(time=time), samples_out) - - @data([12, 64], [65, 64], [79, 80], [83, 80], [80.5, 80]) - @unpack - def test_round_pulse_samples_in(self, samples_in, samples_out): - """Test round pulse calculation with samples input""" - timing = BackendTiming(self.backend) - self.assertEqual(timing.round_pulse(samples=samples_in), samples_out) - def test_delay_time(self): """Test delay_time calculation""" time_in = self.dt * 16.1 @@ -137,45 +107,3 @@ def test_delay_time_no_dt(self): self.backend.target.dt = None timing = BackendTiming(self.backend) self.assertAlmostEqual(timing.delay_time(time=time_in), time_out, delta=1e-6 * self.dt) - - def test_pulse_time(self): - """Test pulse_time calculation""" - time_in = self.dt * 85.1 - time_out = self.dt * 80 - - timing = BackendTiming(self.backend) - self.assertAlmostEqual(timing.pulse_time(time=time_in), time_out, delta=1e-6 * self.dt) - - def test_pulse_time_samples_in(self): - """Test pulse_time calculation""" - samples_in = 85.1 - time_out = self.dt * 80 - - timing = BackendTiming(self.backend) - self.assertAlmostEqual( - timing.pulse_time(samples=samples_in), time_out, delta=1e-6 * self.dt - ) - - def test_round_pulse_no_dt_error(self): - """Test methods that don't work when dt is None raise exceptions""" - self.backend.target.dt = None - timing = BackendTiming(self.backend) - - time = self.dt * 81 - - with self.assertRaises(QiskitError): - timing.round_pulse(time=time) - - def test_unexpected_pulse_alignment(self): - """Test that a weird pulse_alignment parameter is caught""" - self.backend.target.pulse_alignment = 33 - timing = BackendTiming(self.backend) - with self.assertRaises(QiskitError): - timing.round_pulse(samples=81) - - def test_unexpected_acquire_alignment(self): - """Test that a weird acquire_alignment parameter is caught""" - self.backend.target.acquire_alignment = 33 - timing = BackendTiming(self.backend) - with self.assertRaises(QiskitError): - timing.round_pulse(samples=81) diff --git a/test/library/calibration/__init__.py b/test/library/calibration/__init__.py deleted file mode 100644 index b9080a3c89..0000000000 --- a/test/library/calibration/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. -"""Tests for library calibration experiments""" diff --git a/test/library/calibration/test_drag.py b/test/library/calibration/test_drag.py deleted file mode 100644 index 1c1f651028..0000000000 --- a/test/library/calibration/test_drag.py +++ /dev/null @@ -1,252 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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 drag calibration experiment.""" - -from test.base import QiskitExperimentsTestCase -import unittest -from ddt import ddt, data, unpack -import numpy as np - -from qiskit import pulse -from qiskit.circuit import Parameter -from qiskit.exceptions import QiskitError -from qiskit.pulse import DriveChannel, Drag -from qiskit.qobj.utils import MeasLevel -from qiskit_ibm_runtime.fake_provider import FakeWashingtonV2 - -from qiskit_experiments.library import RoughDrag, RoughDragCal -from qiskit_experiments.library.characterization.analysis import DragCalAnalysis -from qiskit_experiments.test.mock_iq_backend import MockIQBackend -from qiskit_experiments.test.mock_iq_helpers import MockIQDragHelper as DragHelper -from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon -from qiskit_experiments.calibration_management import Calibrations - - -@ddt -class TestDragEndToEnd(QiskitExperimentsTestCase): - """Test the drag experiment.""" - - def setUp(self): - """Setup some schedules.""" - super().setUp() - - beta = Parameter("β") - - with pulse.build(name="xp") as xp: - pulse.play(Drag(duration=160, amp=0.208519, sigma=40, beta=beta), DriveChannel(0)) - - self.x_plus = xp - self.test_tol = 0.1 - - @data( - (None, None, None), - (0.0044, None, None), - (0.04, np.linspace(-4, 4, 31), {"beta": 1.8, "freq": 0.08}), - ) - @unpack - def test_end_to_end(self, freq, betas, p0_opt): - """Test the drag experiment end to end.""" - - drag_experiment_helper = DragHelper(gate_name="Drag(xp)") - if freq: - drag_experiment_helper.frequency = freq - backend = MockIQBackend(drag_experiment_helper) - - drag = RoughDrag([1], self.x_plus) - drag.set_run_options(shots=200) - - if betas is not None: - drag.set_experiment_options(betas=betas) - if p0_opt: - drag.analysis.set_options(p0=p0_opt) - - expdata = drag.run(backend) - self.assertExperimentDone(expdata) - result = expdata.analysis_results("beta") - - # pylint: disable=no-member - self.assertTrue(abs(result.value.n - backend.experiment_helper.ideal_beta) < self.test_tol) - self.assertEqual(result.quality, "good") - self.assertEqual(expdata.metadata["meas_level"], MeasLevel.CLASSIFIED) - - @data( - (0.0040, 1.0, 0.00, [1, 3, 5], None, 0.2), # partial oscillation. - (0.0020, 0.5, 0.00, [1, 3, 5], None, 1.0), # even slower oscillation with amp < 1 - (0.0040, 0.8, 0.05, [3, 5, 7], None, 0.2), # constant offset, i.e. lower SNR. - (0.0800, 0.9, 0.05, [1, 3, 5], np.linspace(-1, 1, 51), 0.2), # Beta not in range - (0.2000, 0.5, 0.10, [1, 3, 5], np.linspace(-2.5, 2.5, 51), 0.2), # Max closer to zero - ) - @unpack - def test_nasty_data(self, freq, amp, offset, reps, betas, tol): - """A set of tests for non-ideal data.""" - - backend = MockIQBackend( - DragHelper( - gate_name="Drag(xp)", frequency=freq, max_probability=amp, offset_probability=offset - ) - ) - - drag = RoughDrag([0], self.x_plus, betas=betas) - drag.set_experiment_options(reps=reps) - drag.set_run_options(shots=500) - - exp_data = drag.run(backend) - self.assertExperimentDone(exp_data) - result = exp_data.analysis_results("beta") - # pylint: disable=no-member - self.assertTrue(abs(result.value.n - backend.experiment_helper.ideal_beta) < tol) - self.assertEqual(result.quality, "good") - - def test_drag_reanalysis(self): - """Test edge case for re-analyzing DRAG result multiple times.""" - drag_experiment_helper = DragHelper(gate_name="Drag(xp)") - backend = MockIQBackend(drag_experiment_helper) - - drag = RoughDrag([1], self.x_plus) - drag.set_experiment_options(reps=[2, 4, 6]) - - expdata = drag.run(backend, analysis=None) - self.assertExperimentDone(expdata) - - # Assume the situation we reloaded experiment data from server - # and prepared analysis class in the client machine. - # DRAG reps numbers might be different from the default value, - # but the client doesn't know the original setting. - analysis = DragCalAnalysis() - expdata1 = analysis.run(expdata.copy(), replace_results=True) - self.assertExperimentDone(expdata1) - # Check mapping of model name to circuit metadata. - self.assertDictEqual( - analysis.options.data_subfit_map, - { - "nrep=2": {"nrep": 2}, - "nrep=4": {"nrep": 4}, - "nrep=6": {"nrep": 6}, - }, - ) - - # Running experiment twice. - # Reported by https://github.com/Qiskit/qiskit-experiments/issues/1086. - expdata2 = analysis.run(expdata.copy(), replace_results=True) - self.assertExperimentDone(expdata2) - self.assertEqual(len(analysis.models), 3) - - self.assertAlmostEqual( - expdata1.analysis_results("beta").value.n, - expdata2.analysis_results("beta").value.n, - ) - - -class TestDragCircuits(QiskitExperimentsTestCase): - """Test the circuits of the drag calibration.""" - - def setUp(self): - """Setup some schedules.""" - super().setUp() - - beta = Parameter("β") - - with pulse.build(name="xp") as xp: - pulse.play(Drag(duration=160, amp=0.208519, sigma=40, beta=beta), DriveChannel(0)) - - self.x_plus = xp - - def test_default_circuits(self): - """Test the default circuit.""" - - drag = RoughDrag([0], self.x_plus) - drag.set_experiment_options(reps=[2, 4, 8]) - drag.backend = MockIQBackend(DragHelper(gate_name="Drag(xp)")) - circuits = drag._transpiled_circuits() - - for idx, expected in enumerate([4, 8, 16]): - ops = circuits[idx * 51].count_ops() - self.assertEqual(ops["Drag(xp)"], expected) - - def test_circuit_roundtrip_serializable(self): - """Test circuit serializations for drag experiment.""" - drag = RoughDrag([0], self.x_plus) - drag.set_experiment_options(reps=[2, 4], betas=[-5, 5]) - drag.backend = FakeWashingtonV2() - self.assertRoundTripSerializable(drag._transpiled_circuits()) - - def test_raise_multiple_parameter(self): - """Check that the experiment raises with unassigned parameters.""" - - beta = Parameter("β") - amp = Parameter("amp") - - with pulse.build(name="xp") as xp: - pulse.play(Drag(duration=160, amp=amp, sigma=40, beta=beta), DriveChannel(0)) - - with self.assertRaises(QiskitError): - RoughDrag([1], xp, betas=np.linspace(-3, 3, 21)) - - -class TestRoughDragCalUpdate(QiskitExperimentsTestCase): - """Test that a Drag calibration experiment properly updates the calibrations.""" - - def setUp(self): - """Setup the tests""" - super().setUp() - - library = FixedFrequencyTransmon() - - self.backend = MockIQBackend(DragHelper(gate_name="Drag(x)")) - self.cals = Calibrations.from_backend(self.backend, libraries=[library]) - self.test_tol = 0.05 - - # pylint: disable=no-member - def test_update(self): - """Test that running RoughDragCal updates the calibrations.""" - - qubit = 0 - prev_beta = self.cals.get_parameter_value("β", (0,), "x") - self.assertEqual(prev_beta, 0) - - expdata = RoughDragCal([qubit], self.cals, backend=self.backend).run() - self.assertExperimentDone(expdata) - - new_beta = self.cals.get_parameter_value("β", (0,), "x") - self.assertTrue(abs(new_beta - self.backend.experiment_helper.ideal_beta) < self.test_tol) - self.assertTrue(abs(new_beta) > self.test_tol) - - def test_dragcal_experiment_config(self): - """Test RoughDragCal config can round trip""" - exp = RoughDragCal([0], self.cals, backend=self.backend) - loaded_exp = RoughDragCal.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertEqualExtended(exp, loaded_exp) - - @unittest.skip("Calibration experiments are not yet JSON serializable") - def test_dragcal_roundtrip_serializable(self): - """Test round trip JSON serialization""" - exp = RoughDragCal([0], self.cals) - self.assertRoundTripSerializable(exp) - - def test_drag_experiment_config(self): - """Test RoughDrag config can roundtrip""" - with pulse.build(name="xp") as sched: - pulse.play(pulse.Drag(160, 0.5, 40, Parameter("β")), pulse.DriveChannel(0)) - exp = RoughDrag([0], backend=self.backend, schedule=sched) - loaded_exp = RoughDrag.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertEqualExtended(exp, loaded_exp) - - @unittest.skip("Schedules are not yet JSON serializable") - def test_drag_roundtrip_serializable(self): - """Test round trip JSON serialization""" - with pulse.build(name="xp") as sched: - pulse.play(pulse.Drag(160, 0.5, 40, Parameter("β")), pulse.DriveChannel(0)) - exp = RoughDrag([0], backend=self.backend, schedule=sched) - self.assertRoundTripSerializable(exp) diff --git a/test/library/calibration/test_fine_drag.py b/test/library/calibration/test_fine_drag.py deleted file mode 100644 index 10a97e8bf0..0000000000 --- a/test/library/calibration/test_fine_drag.py +++ /dev/null @@ -1,137 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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 fine drag calibration experiment.""" - -from test.base import QiskitExperimentsTestCase -import numpy as np - -from qiskit import pulse -from qiskit.circuit import Gate -from qiskit_ibm_runtime.fake_provider import FakeArmonkV2 - -from qiskit_experiments.library import FineDrag, FineXDrag, FineDragCal -from qiskit_experiments.test.mock_iq_backend import MockIQBackend -from qiskit_experiments.test.mock_iq_helpers import MockIQFineDragHelper as FineDragHelper -from qiskit_experiments.calibration_management import Calibrations -from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon - - -class TestFineDrag(QiskitExperimentsTestCase): - """Tests of the fine DRAG experiment.""" - - def setUp(self): - """Setup test variables.""" - super().setUp() - - with pulse.build(name="Drag") as schedule: - pulse.play(pulse.Drag(160, 0.5, 40, 0.3), pulse.DriveChannel(0)) - - self.schedule = schedule - - def test_circuits(self): - """Test the circuits of the experiment.""" - - drag = FineDrag([0], Gate("Drag", num_qubits=1, params=[])) - drag.set_experiment_options(schedule=self.schedule) - drag.backend = FakeArmonkV2() - for circuit in drag.circuits()[1:]: - for idx, name in enumerate(["Drag", "rz", "Drag", "rz"]): - self.assertEqual(circuit.data[idx].operation.name, name) - - def test_end_to_end(self): - """A simple test to check if the experiment will run and fit data.""" - - drag = FineDrag([0], Gate("Drag", num_qubits=1, params=[])) - drag.set_experiment_options(schedule=self.schedule) - drag.set_transpile_options(basis_gates=["rz", "Drag", "sx"]) - exp_data = drag.run(MockIQBackend(FineDragHelper())) - self.assertExperimentDone(exp_data) - - self.assertEqual(exp_data.analysis_results("d_theta").quality, "good") - - def test_end_to_end_no_schedule(self): - """Test that we can run without a schedule.""" - exp_data = FineXDrag([0]).run(MockIQBackend(FineDragHelper())) - self.assertExperimentDone(exp_data) - - self.assertEqual(exp_data.analysis_results("d_theta").quality, "good") - - def test_circuits_roundtrip_serializable(self): - """Test circuits serialization of the experiment.""" - drag = FineDrag([0], Gate("Drag", num_qubits=1, params=[])) - drag.set_experiment_options(schedule=self.schedule) - drag.backend = FakeArmonkV2() - self.assertRoundTripSerializable(drag._transpiled_circuits()) - - def test_experiment_config(self): - """Test converting to and from config works""" - exp = FineDrag([0], Gate("Drag", num_qubits=1, params=[])) - config = exp.config() - loaded_exp = FineDrag.from_config(config) - self.assertNotEqual(exp, loaded_exp) - self.assertEqual(config, loaded_exp.config()) - - -class TestFineDragCal(QiskitExperimentsTestCase): - """Test the calibration version of the fine drag experiment.""" - - def setUp(self): - """Setup the test.""" - super().setUp() - - library = FixedFrequencyTransmon() - self.backend = MockIQBackend(FineDragHelper()) - self.cals = Calibrations.from_backend(self.backend, libraries=[library]) - - def test_experiment_config(self): - """Test converting to and from config works""" - exp = FineDragCal([0], self.cals, schedule_name="x") - config = exp.config() - loaded_exp = FineDragCal.from_config(config) - self.assertNotEqual(exp, loaded_exp) - self.assertEqual(config, loaded_exp.config()) - - def test_update_cals(self): - """Test that the calibrations are updated.""" - - init_beta = 0.0 - - drag_cal = FineDragCal([0], self.cals, "x", self.backend) - - circs = drag_cal._transpiled_circuits() - - with pulse.build(name="x") as expected_x: - pulse.play(pulse.Drag(160, 0.5, 40, 0), pulse.DriveChannel(0)) - - with pulse.build(name="sx") as expected_sx: - pulse.play(pulse.Drag(160, 0.25, 40, 0), pulse.DriveChannel(0)) - - self.assertEqual(circs[5].calibrations["x"][((0,), ())], expected_x) - self.assertEqual(circs[5].calibrations["sx"][((0,), ())], expected_sx) - - # run the calibration experiment. This should update the beta parameter of x which we test. - exp_data = drag_cal.run(self.backend) - self.assertExperimentDone(exp_data) - d_theta = exp_data.analysis_results("d_theta").value.n - sigma = 40 - target_angle = np.pi - new_beta = -np.sqrt(np.pi) * d_theta * sigma / target_angle**2 - - circs = drag_cal._transpiled_circuits() - - x_cal = circs[5].calibrations["x"][((0,), ())] - - # Requires allclose due to numerical precision. - self.assertTrue(np.allclose(x_cal.blocks[0].pulse.beta, new_beta)) - self.assertFalse(np.allclose(x_cal.blocks[0].pulse.beta, init_beta)) - self.assertEqual(circs[5].calibrations["sx"][((0,), ())], expected_sx) diff --git a/test/library/calibration/test_half_angle.py b/test/library/calibration/test_half_angle.py deleted file mode 100644 index 7e6d704636..0000000000 --- a/test/library/calibration/test_half_angle.py +++ /dev/null @@ -1,62 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test rough amplitude calibration experiment classes.""" - -from test.base import QiskitExperimentsTestCase - -from qiskit import pulse -from qiskit.circuit import Parameter - -from qiskit_experiments.exceptions import CalibrationError -from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon -from qiskit_experiments.calibration_management import Calibrations -from qiskit_experiments.library import HalfAngleCal -from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - - -class TestHalfAngleCal(QiskitExperimentsTestCase): - """A class to test the half angle calibration experiments.""" - - def setUp(self): - """Setup the tests.""" - super().setUp() - library = FixedFrequencyTransmon() - - self.backend = SingleTransmonTestBackend(noise=False, atol=1e-3) - self.cals = Calibrations.from_backend(self.backend, libraries=[library]) - - def test_amp_parameter_error(self): - """Test that setting cal_parameter_name to amp raises an error""" - with self.assertRaises(CalibrationError): - HalfAngleCal([0], self.cals, cal_parameter_name="amp") - - def test_angle_parameter_missing_error(self): - """Test that default cal_parameter_name with no matching parameter raises an error""" - cals_no_angle = Calibrations() - dur = Parameter("dur") - amp = Parameter("amp") - sigma = Parameter("σ") - beta = Parameter("β") - drive = pulse.DriveChannel(Parameter("ch0")) - - with pulse.build(name="sx") as sx: - pulse.play(pulse.Drag(dur, amp, sigma, beta), drive) - - cals_no_angle.add_schedule(sx, num_qubits=1) - with self.assertRaises(CalibrationError): - HalfAngleCal([0], cals_no_angle) - - def test_circuits_roundtrip_serializable(self): - """Test circuits serialization of the experiment.""" - exp = HalfAngleCal([0], self.cals, backend=self.backend) - self.assertRoundTripSerializable(exp._transpiled_circuits()) diff --git a/test/library/calibration/test_rabi.py b/test/library/calibration/test_rabi.py deleted file mode 100644 index 9c8d035111..0000000000 --- a/test/library/calibration/test_rabi.py +++ /dev/null @@ -1,337 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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 Rabi amplitude Experiment class.""" -from test.base import QiskitExperimentsTestCase -import unittest -import numpy as np - -from qiskit import QuantumCircuit, pulse, transpile -from qiskit.exceptions import QiskitError -from qiskit.circuit import Parameter -from qiskit.qobj.utils import MeasLevel -from qiskit_aer import AerSimulator - -from qiskit_experiments.framework import ExperimentData, ParallelExperiment -from qiskit_experiments.library import Rabi, EFRabi - -from qiskit_experiments.curve_analysis.standard_analysis.oscillation import OscillationAnalysis -from qiskit_experiments.data_processing.data_processor import DataProcessor -from qiskit_experiments.data_processing.nodes import Probability -from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend -from qiskit_experiments.framework.experiment_data import ExperimentStatus -from qiskit_experiments.curve_analysis import ParameterRepr - - -class TestRabiEndToEnd(QiskitExperimentsTestCase): - """Test the rabi experiment.""" - - @classmethod - def setUpClass(cls): - """Setup the tests.""" - super().setUpClass() - - cls.qubit = 0 - - with pulse.build(name="x") as sched: - pulse.play(pulse.Drag(160, Parameter("amp"), 40, 0.4), pulse.DriveChannel(cls.qubit)) - - cls.sched = sched - cls.backend = SingleTransmonTestBackend(noise=False, atol=1e-3) - - # pylint: disable=no-member - def test_rabi_end_to_end(self): - """Test the Rabi experiment end to end.""" - - test_tol = 0.15 - - rabi = Rabi([self.qubit], self.sched, backend=self.backend) - rabi.set_run_options(shots=200) - rabi.set_experiment_options(amplitudes=np.linspace(-0.1, 0.1, 21)) - expdata = rabi.run() - self.assertExperimentDone(expdata) - result = expdata.analysis_results("rabi_rate") - - self.assertEqual(result.quality, "good") - # The comparison is made against the object that exists in the backend for accurate testing - self.assertAlmostEqual( - expdata.artifacts("fit_summary").data.params["freq"], - self.backend.rabi_rate_01, - delta=test_tol, - ) - - def test_wrong_processor(self): - """Test that we can override the data processing by giving a faulty data processor.""" - rabi = Rabi([self.qubit], self.sched, backend=self.backend) - fail_key = "fail_key" - - rabi.analysis.set_options(data_processor=DataProcessor(fail_key, [])) - # pylint: disable=no-member - rabi.set_run_options(shots=2) - data = rabi.run() - result = data.analysis_results() - - self.assertEqual(data.status(), ExperimentStatus.ERROR) - self.assertEqual(len(result), 0) - - def test_experiment_config(self): - """Test converting to and from config works""" - exp = Rabi([self.qubit], self.sched) - loaded_exp = Rabi.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertEqualExtended(exp, loaded_exp) - - @unittest.skip("Schedules are not yet JSON serializable") - def test_roundtrip_serializable(self): - """Test round trip JSON serialization""" - exp = Rabi([self.qubit], self.sched) - self.assertRoundTripSerializable(exp) - - -class TestEFRabi(QiskitExperimentsTestCase): - """Test the ef_rabi experiment.""" - - def setUp(self): - """Setup the tests.""" - super().setUp() - - self.qubit = 0 - self.backend = SingleTransmonTestBackend(noise=False, atol=1e-4) - self.anharmonicity = self.backend.anharmonicity - with pulse.build(name="x") as sched: - with pulse.frequency_offset(self.anharmonicity, pulse.DriveChannel(self.qubit)): - pulse.play( - pulse.Drag(160, Parameter("amp"), 40, 0.4), pulse.DriveChannel(self.qubit) - ) - - self.sched = sched - - # pylint: disable=no-member - def test_ef_rabi_end_to_end(self): - """Test the EFRabi experiment end to end.""" - - test_tol = 0.05 - - # Note that the backend is not sophisticated enough to simulate an e-f - # transition so we run the test with a tiny frequency shift, still driving the e-g transition. - rabi = EFRabi([self.qubit], self.sched, backend=self.backend) - rabi.set_experiment_options(amplitudes=np.linspace(-0.1, 0.1, 11)) - expdata = rabi.run() - self.assertExperimentDone(expdata) - result = expdata.analysis_results("rabi_rate_12") - - self.assertEqual(result.quality, "good") - self.assertTrue(abs(result.value.n - self.backend.rabi_rate_12) < test_tol) - - def test_ef_rabi_circuit(self): - """Test the EFRabi experiment end to end.""" - anharm = self.anharmonicity - - with pulse.build() as sched: - pulse.shift_frequency(anharm, pulse.DriveChannel(2)) - pulse.play(pulse.Gaussian(160, Parameter("amp"), 40), pulse.DriveChannel(2)) - pulse.shift_frequency(-anharm, pulse.DriveChannel(2)) - - rabi12 = EFRabi([2], sched) - rabi12.set_experiment_options(amplitudes=[0.5]) - circ = rabi12.circuits()[0] - - with pulse.build() as expected: - pulse.shift_frequency(anharm, pulse.DriveChannel(2)) - pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(2)) - pulse.shift_frequency(-anharm, pulse.DriveChannel(2)) - - self.assertEqual(circ.calibrations["Rabi"][((2,), (0.5,))], expected) - self.assertEqual(circ.data[0].operation.name, "x") - self.assertEqual(circ.data[1].operation.name, "Rabi") - - def test_experiment_config(self): - """Test converting to and from config works""" - exp = EFRabi([0], self.sched) - loaded_exp = EFRabi.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertEqualExtended(exp, loaded_exp) - - @unittest.skip("Schedules are not yet JSON serializable") - def test_roundtrip_serializable(self): - """Test round trip JSON serialization""" - exp = EFRabi([0], self.sched) - self.assertRoundTripSerializable(exp) - - -class TestRabiCircuits(QiskitExperimentsTestCase): - """Test the circuits generated by the experiment and the options.""" - - def setUp(self): - """Setup tests.""" - super().setUp() - - with pulse.build() as sched: - pulse.play(pulse.Gaussian(160, Parameter("amp"), 40), pulse.DriveChannel(2)) - - self.sched = sched - - def test_default_schedule(self): - """Test the default schedule.""" - rabi = Rabi([2], self.sched) - rabi.set_experiment_options(amplitudes=[0.5]) - circs = rabi.circuits() - - with pulse.build() as expected: - pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(2)) - - self.assertEqual(circs[0].calibrations["Rabi"][((2,), (0.5,))], expected) - self.assertEqual(len(circs), 1) - - def test_user_schedule(self): - """Test the user given schedule.""" - - amp = Parameter("my_double_amp") - with pulse.build() as my_schedule: - pulse.play(pulse.Drag(160, amp, 40, 10), pulse.DriveChannel(2)) - pulse.play(pulse.Drag(160, amp, 40, 10), pulse.DriveChannel(2)) - - rabi = Rabi([2], self.sched) - rabi.set_experiment_options(schedule=my_schedule, amplitudes=[0.5]) - circs = rabi.circuits() - - assigned_sched = my_schedule.assign_parameters({amp: 0.5}, inplace=False) - self.assertEqual(circs[0].calibrations["Rabi"][((2,), (0.5,))], assigned_sched) - - def test_circuits_roundtrip_serializable(self): - """Test circuits serialization of the experiment.""" - rabi = Rabi([2], self.sched) - rabi.set_experiment_options(amplitudes=[0.5]) - self.assertRoundTripSerializable(rabi._transpiled_circuits()) - - -class TestOscillationAnalysis(QiskitExperimentsTestCase): - """Class to test the fitting.""" - - def simulate_experiment_data(self, thetas, amplitudes, shots=1024): - """Generate experiment data for Rx rotations with an arbitrary amplitude calibration.""" - circuits = [] - for theta in thetas: - qc = QuantumCircuit(1) - qc.rx(theta, 0) - qc.measure_all() - circuits.append(qc) - - sim = AerSimulator() - circuits = transpile(circuits, sim) - job = sim.run(circuits, shots=shots, seed_simulator=10) - result = job.result() - data = [ - { - "counts": self._add_uncertainty(result.get_counts(i)), - "metadata": { - "xval": amplitudes[i], - "meas_level": MeasLevel.CLASSIFIED, - "meas_return": "avg", - }, - } - for i, theta in enumerate(thetas) - ] - return data - - @staticmethod - def _add_uncertainty(counts): - """Ensure that we always have a non-zero sigma in the test.""" - for label in ["0", "1"]: - if label not in counts: - counts[label] = 1 - - return counts - - def test_good_analysis(self): - """Test the Rabi analysis.""" - experiment_data = ExperimentData() - - thetas = np.linspace(-np.pi, np.pi, 31) - amplitudes = np.linspace(-0.25, 0.25, 31) - expected_rate, test_tol = 2.0, 0.2 - - experiment_data.add_data(self.simulate_experiment_data(thetas, amplitudes, shots=400)) - - data_processor = DataProcessor("counts", [Probability(outcome="1")]) - - analysis = OscillationAnalysis() - analysis.set_options( - result_parameters=[ParameterRepr("freq", "rabi_rate")], - ) - - experiment_data = analysis.run( - experiment_data, data_processor=data_processor, plot=False - ).block_for_results() - - result = experiment_data.analysis_results("rabi_rate") - self.assertEqual(result.quality, "good") - self.assertAlmostEqual(result.value, expected_rate, delta=test_tol) - - def test_bad_analysis(self): - """Test the Rabi analysis.""" - experiment_data = ExperimentData() - - # Change rotation angle with square root of amplitude so that - # population versus amplitude will not be sinusoidal and the fit will - # be bad. - thetas = np.sqrt(np.linspace(0.0, 4 * np.pi**2, 31)) - amplitudes = np.linspace(0.0, 0.95, 31) - - experiment_data.add_data(self.simulate_experiment_data(thetas, amplitudes, shots=200)) - - data_processor = DataProcessor("counts", [Probability(outcome="1")]) - - analysis = OscillationAnalysis() - analysis.set_options( - result_parameters=[ParameterRepr("freq", "rabi_rate")], - ) - experiment_data = analysis.run( - experiment_data, - data_processor=data_processor, - plot=False, - ).block_for_results() - - result = experiment_data.analysis_results("rabi_rate") - - self.assertEqual(result.quality, "bad") - - -class TestCompositeExperiment(QiskitExperimentsTestCase): - """Test composite Rabi experiment.""" - - def test_calibrations(self): - """Test that the calibrations are preserved and that the circuit transpiles.""" - - experiments = [] - for qubit in range(3): - with pulse.build() as sched: - pulse.play(pulse.Gaussian(160, Parameter("amp"), 40), pulse.DriveChannel(qubit)) - - experiments.append(Rabi([qubit], sched, amplitudes=[0.5])) - - par_exp = ParallelExperiment(experiments, flatten_results=False) - par_circ = par_exp.circuits()[0] - - # If the calibrations are not there we will not be able to transpile - try: - transpile(par_circ, basis_gates=["rz", "sx", "x", "cx"]) - except QiskitError as error: - self.fail("Failed to transpile with error: " + str(error)) - - # Assert that the calibration keys are in the calibrations of the composite circuit. - for qubit in range(3): - rabi_circuit = experiments[qubit].circuits()[0] - cal_key = next(iter(rabi_circuit.calibrations["Rabi"].keys())) - - self.assertEqual(cal_key[0], (qubit,)) - self.assertTrue(cal_key in par_circ.calibrations["Rabi"]) diff --git a/test/library/calibration/test_rough_amplitude.py b/test/library/calibration/test_rough_amplitude.py deleted file mode 100644 index 7618757f5c..0000000000 --- a/test/library/calibration/test_rough_amplitude.py +++ /dev/null @@ -1,186 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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 rough amplitude calibration experiment classes.""" - -from test.base import QiskitExperimentsTestCase -import numpy as np - -from qiskit import pulse -from qiskit.circuit import Parameter - -from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon -from qiskit_experiments.calibration_management import Calibrations -from qiskit_experiments.library import EFRoughXSXAmplitudeCal, RoughXSXAmplitudeCal -from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - - -class TestRoughAmpCal(QiskitExperimentsTestCase): - """A class to test the rough amplitude calibration experiments.""" - - def setUp(self): - """Setup the tests.""" - super().setUp() - library = FixedFrequencyTransmon() - - self.backend = SingleTransmonTestBackend(noise=False, atol=1e-3) - self.cals = Calibrations.from_backend(self.backend, libraries=[library]) - - def test_circuits(self): - """Test the quantum circuits.""" - test_amps = [-0.5, 0, 0.5] - rabi = RoughXSXAmplitudeCal([0], self.cals, amplitudes=test_amps, backend=self.backend) - - circs = rabi._transpiled_circuits() - - for circ, amp in zip(circs, test_amps): - self.assertEqual(circ.count_ops()["Rabi"], 1) - - d0 = pulse.DriveChannel(0) - with pulse.build(name="x") as expected_x: - pulse.play(pulse.Drag(160, amp, 40, 0), d0) - - self.assertEqual(circ.calibrations["Rabi"][((0,), (amp,))], expected_x) - - def test_update(self): - """Test that the calibrations update properly.""" - - tol = 0.01 - default_amp = 0.5 / self.backend.rabi_rate_01 - - rabi = RoughXSXAmplitudeCal( - [0], self.cals, amplitudes=np.linspace(-0.1, 0.1, 11), backend=self.backend - ) - expdata = rabi.run() - self.assertExperimentDone(expdata) - - self.assertAlmostEqual(self.cals.get_parameter_value("amp", 0, "x"), default_amp, delta=tol) - self.assertAlmostEqual( - self.cals.get_parameter_value("amp", 0, "sx"), default_amp / 2, delta=tol - ) - - self.cals.add_parameter_value(int(4 * 160 / 5), "duration", (), schedule="x") - rabi = RoughXSXAmplitudeCal( - [0], self.cals, amplitudes=np.linspace(-0.1, 0.1, 11), backend=self.backend - ) - expdata = rabi.run() - self.assertExperimentDone(expdata) - - self.assertTrue( - abs(self.cals.get_parameter_value("amp", 0, "x") * (4 / 5) - default_amp) < tol - ) - self.assertTrue( - abs(self.cals.get_parameter_value("amp", 0, "sx") * (4 / 5) - default_amp / 2) < tol - ) - - def test_circuit_roundtrip_serializable(self): - """Test round trip JSON serialization""" - test_amps = [-0.5, 0] - rabi = RoughXSXAmplitudeCal([0], self.cals, amplitudes=test_amps, backend=self.backend) - self.assertRoundTripSerializable(rabi._transpiled_circuits()) - - def test_experiment_config(self): - """Test converting to and from config works""" - exp = RoughXSXAmplitudeCal([0], self.cals) - config = exp.config() - loaded_exp = RoughXSXAmplitudeCal.from_config(config) - self.assertNotEqual(exp, loaded_exp) - self.assertEqual(config, loaded_exp.config()) - - -class TestSpecializations(QiskitExperimentsTestCase): - """Test the specialized versions of the calibration.""" - - @classmethod - def setUpClass(cls): - """Setup the tests""" - super().setUpClass() - - library = FixedFrequencyTransmon() - - cls.backend = SingleTransmonTestBackend(noise=False, atol=1e-3) - cls.cals = Calibrations.from_backend(cls.backend, libraries=[library]) - - # Add some pulses on the 1-2 transition. - d0 = pulse.DriveChannel(0) - with pulse.build(name="x12") as x12: - with pulse.frequency_offset(-300e6, d0): - pulse.play(pulse.Drag(Parameter("duration"), Parameter("amp"), 40, 0.0), d0) - - with pulse.build(name="sx12") as sx12: - with pulse.frequency_offset(-300e6, d0): - pulse.play(pulse.Drag(Parameter("duration"), Parameter("amp"), 40, 0.0), d0) - - cls.cals.add_schedule(x12, 0) - cls.cals.add_schedule(sx12, 0) - cls.cals.add_parameter_value(0.4, "amp", 0, "x12") - cls.cals.add_parameter_value(0.2, "amp", 0, "sx12") - cls.cals.add_parameter_value(160, "duration", 0, "x12") - cls.cals.add_parameter_value(160, "duration", 0, "sx12") - - def test_ef_circuits(self): - """Test that we get the expected circuits with calibrations for the EF experiment.""" - - test_amps = [-0.5, 0, 0.5] - rabi_ef = EFRoughXSXAmplitudeCal([0], self.cals, amplitudes=test_amps, backend=self.backend) - - circs = rabi_ef._transpiled_circuits() - - for circ, amp in zip(circs, test_amps): - - self.assertEqual(circ.count_ops()["x"], 1) - self.assertEqual(circ.count_ops()["Rabi"], 1) - - d0 = pulse.DriveChannel(0) - with pulse.build(name="x") as expected_x: - pulse.play(pulse.Drag(160, 0.5, 40, 0), d0) - - with pulse.build(name="x12") as expected_x12: - with pulse.frequency_offset(-300e6, d0): - pulse.play(pulse.Drag(160, amp, 40, 0), d0) - - self.assertEqual(circ.calibrations["x"][((0,), ())], expected_x) - self.assertEqual(circ.calibrations["Rabi"][((0,), (amp,))], expected_x12) - - def test_ef_update(self): - """Test that we properly update the pulses on the 1<->2 transition.""" - - tol = 0.05 - default_amp = 0.5 / self.backend.rabi_rate_12 - - rabi_ef = EFRoughXSXAmplitudeCal( - [0], self.cals, amplitudes=np.linspace(-0.1, 0.1, 11), backend=self.backend - ) - rabi_ef.set_run_options(shots=200) - expdata = rabi_ef.run() - self.assertExperimentDone(expdata) - - self.assertAlmostEqual( - self.cals.get_parameter_value("amp", 0, "x12"), default_amp, delta=tol - ) - self.assertAlmostEqual( - self.cals.get_parameter_value("amp", 0, "sx12"), default_amp / 2, delta=tol - ) - - self.cals.add_parameter_value(int(4 * 160 / 5), "duration", 0, "x12") - self.cals.add_parameter_value(int(4 * 160 / 5), "duration", 0, "sx12") - rabi_ef = EFRoughXSXAmplitudeCal([0], self.cals, amplitudes=np.linspace(-0.1, 0.1, 11)) - rabi_ef.set_run_options(shots=200) - expdata = rabi_ef.run(self.backend) - self.assertExperimentDone(expdata) - - self.assertTrue( - abs(self.cals.get_parameter_value("amp", 0, "x12") * (4 / 5) - default_amp) < tol - ) - self.assertTrue( - abs(self.cals.get_parameter_value("amp", 0, "sx12") * (4 / 5) - default_amp / 2) < tol - ) diff --git a/test/library/calibration/test_rough_frequency.py b/test/library/calibration/test_rough_frequency.py deleted file mode 100644 index 930cce1814..0000000000 --- a/test/library/calibration/test_rough_frequency.py +++ /dev/null @@ -1,84 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Rough frequency calibration tests.""" -from test.base import QiskitExperimentsTestCase - -import numpy as np - -from qiskit_experiments.framework import BackendData -from qiskit_experiments.library import RoughFrequencyCal -from qiskit_experiments.calibration_management import Calibrations -from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon -from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend - - -class TestRoughFrequency(QiskitExperimentsTestCase): - """Tests for the rough frequency calibration experiment.""" - - def setUp(self): - """Setup the tests.""" - super().setUp() - self.backend = SingleTransmonTestBackend(noise=False, atol=1e-3) - - def test_init(self): - """Test that initialization.""" - - qubit = 0 - cals = Calibrations.from_backend(self.backend) - frequencies = [1000, 2000, 3000] - auto_update = False - absolute = False - - freq = RoughFrequencyCal( - [qubit], cals, frequencies, auto_update=auto_update, absolute=absolute - ) - - self.assertEqual(freq.physical_qubits, (qubit,)) - self.assertEqual(freq._frequencies, frequencies) - self.assertEqual(freq._absolute, False) - self.assertEqual(freq.auto_update, False) - - def test_update_calibrations(self): - """Test that we can properly update an instance of Calibrations.""" - - freq01 = BackendData(self.backend).drive_freqs[0] - - backend_5mhz = SingleTransmonTestBackend( - qubit_frequency=freq01 + 5e6, noise=False, atol=1e-3 - ) - - library = FixedFrequencyTransmon() - cals = Calibrations.from_backend(self.backend, libraries=[library]) - - prev_freq = cals.get_parameter_value("drive_freq", (0,)) - self.assertEqual(prev_freq, freq01) - - frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 11) - - spec = RoughFrequencyCal([0], cals, frequencies, backend=backend_5mhz) - spec.set_experiment_options(amp=0.005) - expdata = spec.run() - self.assertExperimentDone(expdata) - - # Check the updated frequency which should be shifted by 5MHz. - post_freq = cals.get_parameter_value("drive_freq", (0,)) - self.assertTrue(abs(post_freq - freq01 - 5e6) < 1e6) - - def test_experiment_config(self): - """Test converting to and from config works""" - cals = Calibrations.from_backend(self.backend) - frequencies = [1, 2, 3] - exp = RoughFrequencyCal([0], cals, frequencies) - loaded_exp = RoughFrequencyCal.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertEqualExtended(exp, loaded_exp) diff --git a/test/library/characterization/test_cross_resonance_hamiltonian.py b/test/library/characterization/test_cross_resonance_hamiltonian.py deleted file mode 100644 index da23fb6320..0000000000 --- a/test/library/characterization/test_cross_resonance_hamiltonian.py +++ /dev/null @@ -1,318 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -# pylint: disable=invalid-name - -"""Spectroscopy tests.""" -from test.base import QiskitExperimentsTestCase -import functools -import io -from unittest.mock import patch - -import numpy as np -from ddt import ddt, data, unpack - -from qiskit import QuantumCircuit, pulse, qpy, quantum_info as qi -from qiskit.circuit import Gate - -# TODO: remove old path after we stop supporting the relevant version of Qiskit -try: - from qiskit.circuit.library.hamiltonian_gate import HamiltonianGate -except ModuleNotFoundError: - from qiskit.extensions.hamiltonian_gate import HamiltonianGate - -from qiskit_aer import AerSimulator -from qiskit_ibm_runtime.fake_provider import FakeBogotaV2 - -from qiskit_experiments.library.characterization import cr_hamiltonian - - -class SimulatableCRGate(HamiltonianGate): - """Hamiltonian Gate for simulation.""" - - def __init__(self, width, hamiltonian, sigma, dt): - super().__init__(data=hamiltonian, time=np.sqrt(2 * np.pi) * sigma * dt + width) - - -@ddt -class TestCrossResonanceHamiltonian(QiskitExperimentsTestCase): - """Test for cross resonance Hamiltonian tomography.""" - - def test_circuit_generation(self): - """Test generated circuits.""" - backend = FakeBogotaV2() - - expr = cr_hamiltonian.CrossResonanceHamiltonian( - physical_qubits=(0, 1), - amp=0.1, - sigma=64, - risefall=2, - durations=[backend.dt * (1000 + 4 * 64)], - ) - expr.backend = backend - - with pulse.build(default_alignment="left", name="cr") as ref_cr_sched: - pulse.play( - pulse.GaussianSquare( - duration=1256, - amp=0.1, - sigma=64, - width=1000, - ), - pulse.ControlChannel(0), - ) - pulse.delay(1256, pulse.DriveChannel(0)) - pulse.delay(1256, pulse.DriveChannel(1)) - - width_sec = 1000 * backend.dt - cr_gate = cr_hamiltonian.CrossResonanceHamiltonian.CRPulseGate(width=width_sec) - expr_circs = expr.circuits() - - x0_circ = QuantumCircuit(2, 1) - x0_circ.append(cr_gate, [0, 1]) - x0_circ.rz(np.pi / 2, 1) - x0_circ.sx(1) - x0_circ.measure(1, 0) - - x1_circ = QuantumCircuit(2, 1) - x1_circ.x(0) - x1_circ.append(cr_gate, [0, 1]) - x1_circ.rz(np.pi / 2, 1) - x1_circ.sx(1) - x1_circ.measure(1, 0) - - y0_circ = QuantumCircuit(2, 1) - y0_circ.append(cr_gate, [0, 1]) - y0_circ.sx(1) - y0_circ.measure(1, 0) - - y1_circ = QuantumCircuit(2, 1) - y1_circ.x(0) - y1_circ.append(cr_gate, [0, 1]) - y1_circ.sx(1) - y1_circ.measure(1, 0) - - z0_circ = QuantumCircuit(2, 1) - z0_circ.append(cr_gate, [0, 1]) - z0_circ.measure(1, 0) - - z1_circ = QuantumCircuit(2, 1) - z1_circ.x(0) - z1_circ.append(cr_gate, [0, 1]) - z1_circ.measure(1, 0) - - ref_circs = [x0_circ, y0_circ, z0_circ, x1_circ, y1_circ, z1_circ] - for c in ref_circs: - c.add_calibration(cr_gate, (0, 1), ref_cr_sched) - - self.assertListEqual(expr_circs, ref_circs) - - def test_circuit_generation_no_backend(self): - """User can check experiment circuit without setting backend.""" - - class FakeCRGate(HamiltonianGate): - """Hamiltonian Gate for simulation.""" - - def __init__(self, width): - super().__init__(data=np.eye(4), time=width) - - expr = cr_hamiltonian.CrossResonanceHamiltonian( - physical_qubits=(0, 1), - cr_gate=FakeCRGate, - amp=0.1, - sigma=64, - risefall=2, - durations=[1256], - ) - - # Not raise an error - expr.circuits() - - @data( - [1e6, 2e6, 1e3, -3e6, -2e6, 1e4], - [-1e6, -2e6, 1e3, 3e6, 2e6, 1e4], - [1e4, 2e4, 1e3, 5e6, 1e6, 2e3], - [1e4, -1e3, 1e3, 5e5, 1e3, -1e3], # low frequency test case 1 - [-1.0e5, 1.2e5, 1.0e3, 1.5e5, -1.1e5, -1.0e3], # low frequency test case 2 - ) - @unpack - def test_integration(self, ix, iy, iz, zx, zy, zz): - """Integration test for Hamiltonian tomography.""" - delta = 3e4 - - dt = 0.222e-9 - sigma = 64 - shots = 2000 - - backend = AerSimulator(seed_simulator=123, shots=shots) - backend._configuration.dt = dt - - # Note that Qiskit is Little endian, i.e. [q1, q0] - hamiltonian = ( - 2 - * np.pi - * ( - ix * qi.Operator.from_label("XI") / 2 - + iy * qi.Operator.from_label("YI") / 2 - + iz * qi.Operator.from_label("ZI") / 2 - + zx * qi.Operator.from_label("XZ") / 2 - + zy * qi.Operator.from_label("YZ") / 2 - + zz * qi.Operator.from_label("ZZ") / 2 - ) - ) - - expr = cr_hamiltonian.CrossResonanceHamiltonian( - physical_qubits=(0, 1), - sigma=sigma, - # A hack to avoid local function in pickle, i.e. in transpile. - cr_gate=functools.partial( - SimulatableCRGate, hamiltonian=hamiltonian, sigma=sigma, dt=dt - ), - ) - expr.backend = backend - - exp_data = expr.run(shots=shots) - self.assertExperimentDone(exp_data, timeout=1000) - - self.assertEqual(exp_data.analysis_results("omega_ix").quality, "good") - - # These values are computed from other analysis results in post hook. - # Thus at least one of these values should be round-trip tested. - res_ix = exp_data.analysis_results("omega_ix") - self.assertAlmostEqual(res_ix.value.n, ix, delta=delta) - self.assertRoundTripSerializable(res_ix.value) - self.assertEqual(res_ix.extra["unit"], "Hz") - - self.assertAlmostEqual(exp_data.analysis_results("omega_iy").value.n, iy, delta=delta) - self.assertAlmostEqual(exp_data.analysis_results("omega_iz").value.n, iz, delta=delta) - self.assertAlmostEqual(exp_data.analysis_results("omega_zx").value.n, zx, delta=delta) - self.assertAlmostEqual(exp_data.analysis_results("omega_zy").value.n, zy, delta=delta) - self.assertAlmostEqual(exp_data.analysis_results("omega_zz").value.n, zz, delta=delta) - - def test_integration_backward_compat(self): - """Integration test for Hamiltonian tomography.""" - ix, iy, iz, zx, zy, zz = 1e6, 2e6, 1e3, -3e6, -2e6, 1e4 - - delta = 3e4 - - dt = 0.222e-9 - sigma = 64 - - backend = AerSimulator(seed_simulator=123, shots=2000) - backend._configuration.dt = dt - - # Note that Qiskit is Little endian, i.e. [q1, q0] - hamiltonian = ( - 2 - * np.pi - * ( - ix * qi.Operator.from_label("XI") / 2 - + iy * qi.Operator.from_label("YI") / 2 - + iz * qi.Operator.from_label("ZI") / 2 - + zx * qi.Operator.from_label("XZ") / 2 - + zy * qi.Operator.from_label("YZ") / 2 - + zz * qi.Operator.from_label("ZZ") / 2 - ) - ) - - expr = cr_hamiltonian.CrossResonanceHamiltonian( - (0, 1), - np.linspace(0, 700, 50), - sigma=sigma, - # A hack to avoild local function in pickle, i.e. in transpile. - cr_gate=functools.partial( - SimulatableCRGate, hamiltonian=hamiltonian, sigma=sigma, dt=dt - ), - ) - expr.backend = backend - - exp_data = expr.run() - self.assertExperimentDone(exp_data, timeout=1000) - - self.assertEqual(exp_data.analysis_results("omega_ix").quality, "good") - - self.assertAlmostEqual(exp_data.analysis_results("omega_ix").value.n, ix, delta=delta) - self.assertAlmostEqual(exp_data.analysis_results("omega_iy").value.n, iy, delta=delta) - self.assertAlmostEqual(exp_data.analysis_results("omega_iz").value.n, iz, delta=delta) - self.assertAlmostEqual(exp_data.analysis_results("omega_zx").value.n, zx, delta=delta) - self.assertAlmostEqual(exp_data.analysis_results("omega_zy").value.n, zy, delta=delta) - self.assertAlmostEqual(exp_data.analysis_results("omega_zz").value.n, zz, delta=delta) - - def test_experiment_config(self): - """Test converting to and from config works""" - exp = cr_hamiltonian.CrossResonanceHamiltonian( - physical_qubits=[0, 1], - durations=[1000], - amp=0.1, - sigma=64, - risefall=2, - ) - loaded_exp = cr_hamiltonian.CrossResonanceHamiltonian.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertEqualExtended(exp, loaded_exp) - - def test_roundtrip_serializable(self): - """Test round trip JSON serialization""" - exp = cr_hamiltonian.CrossResonanceHamiltonian( - physical_qubits=[0, 1], - durations=[1000], - amp=0.1, - sigma=64, - risefall=2, - ) - self.assertRoundTripSerializable(exp) - - def test_circuit_serialization(self): - """Test generated circuits.""" - backend = FakeBogotaV2() - - with patch.object( - cr_hamiltonian.CrossResonanceHamiltonian.CRPulseGate, - "base_class", - Gate, - ): - # Monkey patching the Instruction.base_class property of the CRPulseGate. - # QPY loader is not aware of Gate subclasses defined outside Qiskit core, - # and a Gate subclass instance is reconstructed as a Gate class instance. - # This results in the failure in comparison of structurally same circuits. - # In this context, CRPulseGate looks like a Gate class. - expr = cr_hamiltonian.CrossResonanceHamiltonian( - physical_qubits=(0, 1), - amp=0.1, - sigma=64, - risefall=2, - ) - expr.backend = backend - - width_sec = 1000 * backend.dt - cr_gate = cr_hamiltonian.CrossResonanceHamiltonian.CRPulseGate(width=width_sec) - circuits = expr._transpiled_circuits() - - x0_circ = QuantumCircuit(2, 1) - x0_circ.append(cr_gate, [0, 1]) - x0_circ.rz(np.pi / 2, 1) - x0_circ.sx(1) - x0_circ.measure(1, 0) - - circuits.append(x0_circ) - - with io.BytesIO() as buff: - qpy.dump(circuits, buff) - buff.seek(0) - serialized_data = buff.read() - - with io.BytesIO() as buff: - buff.write(serialized_data) - buff.seek(0) - decoded = qpy.load(buff) - - self.assertListEqual(circuits, decoded) diff --git a/test/library/calibration/test_fine_amplitude.py b/test/library/characterization/test_fine_amplitude.py similarity index 52% rename from test/library/calibration/test_fine_amplitude.py rename to test/library/characterization/test_fine_amplitude.py index 0973072f3c..d8f866a783 100644 --- a/test/library/calibration/test_fine_amplitude.py +++ b/test/library/characterization/test_fine_amplitude.py @@ -11,25 +11,21 @@ # that they have been altered from the originals. """Test the fine amplitude characterization and calibration experiments.""" +import warnings from test.base import QiskitExperimentsTestCase + import numpy as np from ddt import ddt, data -from qiskit import pulse from qiskit.circuit import Gate from qiskit.circuit.library import XGate, SXGate -from qiskit.pulse import DriveChannel, Drag from qiskit_ibm_runtime.fake_provider import FakeArmonkV2 from qiskit_experiments.library import ( FineXAmplitude, FineSXAmplitude, FineZXAmplitude, - FineXAmplitudeCal, - FineSXAmplitudeCal, ) -from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon -from qiskit_experiments.calibration_management import Calibrations from qiskit_experiments.test.mock_iq_backend import MockIQBackend from qiskit_experiments.test.mock_iq_helpers import MockIQFineAmpHelper as FineAmpHelper @@ -95,7 +91,9 @@ def test_end_to_end(self, pi_ratio): """Test the experiment end to end.""" error = -np.pi * pi_ratio - amp_exp = FineZXAmplitude((0, 1)) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=r".*Qiskit 2\.0.*") + amp_exp = FineZXAmplitude((0, 1)) backend = MockIQBackend(FineAmpHelper(error, np.pi / 2, "szx")) backend.target.add_instruction(Gate("szx", 2, []), properties={(0, 1): None}) @@ -111,8 +109,10 @@ def test_end_to_end(self, pi_ratio): def test_experiment_config(self): """Test converting to and from config works""" - exp = FineZXAmplitude((0, 1)) - loaded_exp = FineZXAmplitude.from_config(exp.config()) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=r".*Qiskit 2\.0.*") + exp = FineZXAmplitude((0, 1)) + loaded_exp = FineZXAmplitude.from_config(exp.config()) self.assertNotEqual(exp, loaded_exp) self.assertEqualExtended(exp, loaded_exp) @@ -120,19 +120,6 @@ def test_experiment_config(self): class TestFineAmplitudeCircuits(QiskitExperimentsTestCase): """Test the circuits.""" - def setUp(self): - """Setup some schedules.""" - super().setUp() - - with pulse.build(name="xp") as xp: - pulse.play(Drag(duration=160, amp=0.208519, sigma=40, beta=0.0), DriveChannel(0)) - - with pulse.build(name="x90p") as x90p: - pulse.play(Drag(duration=160, amp=0.208519, sigma=40, beta=0.0), DriveChannel(0)) - - self.x_plus = xp - self.x_90_plus = x90p - def test_xp(self): """Test a circuit with the x gate.""" @@ -201,134 +188,10 @@ def test_sx_roundtrip_serializable(self): def test_measure_qubits(self, qubits): """Test that the measurement is on the logical qubits.""" - fine_amp = FineZXAmplitude(qubits) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=r".*Qiskit 2\.0.*") + fine_amp = FineZXAmplitude(qubits) for circuit in fine_amp.circuits(): self.assertEqual(circuit.num_qubits, 2) self.assertEqual(circuit.data[-1].operation.name, "measure") self.assertEqual(circuit.data[-1].qubits[0], circuit.qregs[0][1]) - - -class TestFineAmplitudeCal(QiskitExperimentsTestCase): - """A class to test the fine amplitude calibration experiments.""" - - def setUp(self): - """Setup the tests""" - super().setUp() - - library = FixedFrequencyTransmon() - - self.backend = MockIQBackend(FineAmpHelper(-np.pi * 0.07, np.pi, "xp")) - self.backend.target.add_instruction(SXGate(), properties={(0,): None}) - self.backend.target.add_instruction(XGate(), properties={(0,): None}) - self.cals = Calibrations.from_backend(self.backend, libraries=[library]) - - def test_cal_options(self): - """Test that the options are properly propagated.""" - - # Test the X gate cal - amp_cal = FineXAmplitudeCal([0], self.cals, "x") - - exp_opt = amp_cal.experiment_options - - self.assertEqual(exp_opt.gate.name, "x") - self.assertTrue(exp_opt.add_cal_circuits) - self.assertEqual(exp_opt.result_index, -1) - self.assertEqual(exp_opt.group, "default") - self.assertTrue(np.allclose(exp_opt.target_angle, np.pi)) - - # Test the SX gate cal - amp_cal = FineSXAmplitudeCal([0], self.cals, "sx") - - exp_opt = amp_cal.experiment_options - - self.assertEqual(exp_opt.gate.name, "sx") - self.assertFalse(exp_opt.add_cal_circuits) - self.assertEqual(exp_opt.result_index, -1) - self.assertEqual(exp_opt.group, "default") - self.assertTrue(np.allclose(exp_opt.target_angle, np.pi / 2)) - - def test_run_x_cal(self): - """Test that we can transpile in the calibrations before and after update. - - If this test passes then we were successful in running a calibration experiment, - updating a pulse parameter, having this parameter propagated to the schedules - for use the next time the experiment is run. - """ - - # Initial pulse amplitude - init_amp = 0.5 - - amp_cal = FineXAmplitudeCal([0], self.cals, "x", backend=self.backend) - - circs = amp_cal._transpiled_circuits() - - with pulse.build(name="x") as expected_x: - pulse.play(pulse.Drag(160, 0.5, 40, 0), pulse.DriveChannel(0)) - - with pulse.build(name="sx") as expected_sx: - pulse.play(pulse.Drag(160, 0.25, 40, 0), pulse.DriveChannel(0)) - - self.assertEqual(circs[5].calibrations["x"][((0,), ())], expected_x) - self.assertEqual(circs[5].calibrations["sx"][((0,), ())], expected_sx) - - # run the calibration experiment. This should update the amp parameter of x which we test. - exp_data = amp_cal.run() - self.assertExperimentDone(exp_data) - d_theta = exp_data.analysis_results("d_theta").value.n - new_amp = init_amp * np.pi / (np.pi + d_theta) - - circs = amp_cal._transpiled_circuits() - - x_cal = circs[5].calibrations["x"][((0,), ())] - - # Requires allclose due to numerical precision. - self.assertTrue(np.allclose(x_cal.blocks[0].pulse.amp, new_amp)) - self.assertFalse(np.allclose(x_cal.blocks[0].pulse.amp, init_amp)) - self.assertEqual(circs[5].calibrations["sx"][((0,), ())], expected_sx) - - def test_run_sx_cal(self): - """Test that we can transpile in the calibrations before and after update. - - If this test passes then we were successful in running a calibration experiment, - updating a pulse parameter, having this parameter propagated to the schedules - for use the next time the experiment is run. - """ - - # Initial pulse amplitude - init_amp = 0.25 - - backend = MockIQBackend(FineAmpHelper(-np.pi * 0.07, np.pi / 2, "sx")) - amp_cal = FineSXAmplitudeCal([0], self.cals, "sx", backend=backend) - - circs = amp_cal._transpiled_circuits() - - with pulse.build(name="sx") as expected_sx: - pulse.play(pulse.Drag(160, 0.25, 40, 0), pulse.DriveChannel(0)) - - self.assertEqual(circs[5].calibrations["sx"][((0,), ())], expected_sx) - - # run the calibration experiment. This should update the amp parameter of x which we test. - exp_data = amp_cal.run() - self.assertExperimentDone(exp_data) - d_theta = exp_data.analysis_results("d_theta").value.n - new_amp = init_amp * (np.pi / 2) / (np.pi / 2 + d_theta) - - circs = amp_cal._transpiled_circuits() - - sx_cal = circs[5].calibrations["sx"][((0,), ())] - - # Requires allclose due to numerical precision. - self.assertTrue(np.allclose(sx_cal.blocks[0].pulse.amp, new_amp)) - self.assertFalse(np.allclose(sx_cal.blocks[0].pulse.amp, init_amp)) - - def test_experiment_config(self): - """Test converting to and from config works""" - exp = FineSXAmplitudeCal([0], self.cals, "sx") - loaded_exp = FineSXAmplitudeCal.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertEqualExtended(exp, loaded_exp) - - def test_roundtrip_serializable(self): - """Test round trip JSON serialization""" - exp = FineSXAmplitudeCal([0], self.cals, "sx") - self.assertRoundTripSerializable(exp) diff --git a/test/library/characterization/test_fine_drag.py b/test/library/characterization/test_fine_drag.py new file mode 100644 index 0000000000..f692c5c7fe --- /dev/null +++ b/test/library/characterization/test_fine_drag.py @@ -0,0 +1,56 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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 fine drag calibration experiment.""" + +from test.base import QiskitExperimentsTestCase + +from qiskit.circuit import Gate +from qiskit_ibm_runtime.fake_provider import FakeArmonkV2 + +from qiskit_experiments.library import FineDrag, FineXDrag +from qiskit_experiments.test.mock_iq_backend import MockIQBackend +from qiskit_experiments.test.mock_iq_helpers import MockIQFineDragHelper as FineDragHelper + + +class TestFineDrag(QiskitExperimentsTestCase): + """Tests of the fine DRAG experiment.""" + + def test_circuits(self): + """Test the circuits of the experiment.""" + + drag = FineDrag([0], Gate("Drag", num_qubits=1, params=[])) + drag.backend = FakeArmonkV2() + for circuit in drag.circuits()[1:]: + for idx, name in enumerate(["Drag", "rz", "Drag", "rz"]): + self.assertEqual(circuit.data[idx].operation.name, name) + + def test_end_to_end(self): + """A simple test to check if the experiment will run and fit data.""" + exp_data = FineXDrag([0]).run(MockIQBackend(FineDragHelper())) + self.assertExperimentDone(exp_data) + + self.assertEqual(exp_data.analysis_results("d_theta").quality, "good") + + def test_circuits_roundtrip_serializable(self): + """Test circuits serialization of the experiment.""" + drag = FineXDrag([0]) + drag.backend = FakeArmonkV2() + self.assertRoundTripSerializable(drag._transpiled_circuits()) + + def test_experiment_config(self): + """Test converting to and from config works""" + exp = FineDrag([0], Gate("Drag", num_qubits=1, params=[])) + config = exp.config() + loaded_exp = FineDrag.from_config(config) + self.assertNotEqual(exp, loaded_exp) + self.assertEqual(config, loaded_exp.config()) diff --git a/test/library/calibration/test_fine_frequency.py b/test/library/characterization/test_fine_frequency.py similarity index 61% rename from test/library/calibration/test_fine_frequency.py rename to test/library/characterization/test_fine_frequency.py index fb8a21cad6..9ddc9f00ac 100644 --- a/test/library/calibration/test_fine_frequency.py +++ b/test/library/characterization/test_fine_frequency.py @@ -16,15 +16,7 @@ import numpy as np from ddt import ddt, data -from qiskit import pulse -from qiskit_ibm_runtime.fake_provider import FakeArmonkV2 - -from qiskit_experiments.library import ( - FineFrequency, - FineFrequencyCal, -) -from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon -from qiskit_experiments.calibration_management import Calibrations +from qiskit_experiments.library import FineFrequency from qiskit_experiments.framework import BackendData from qiskit_experiments.test.mock_iq_backend import MockIQBackend from qiskit_experiments.test.mock_iq_helpers import MockIQFineFreqHelper as FineFreqHelper @@ -37,17 +29,9 @@ class TestFineFreqEndToEnd(QiskitExperimentsTestCase): def setUp(self): """Setup for the test.""" super().setUp() - self.inst_map = pulse.InstructionScheduleMap() self.sx_duration = 160 - with pulse.build(name="sx") as sx_sched: - pulse.play(pulse.Gaussian(self.sx_duration, 0.5, 40), pulse.DriveChannel(0)) - - self.inst_map.add("sx", 0, sx_sched) - - self.cals = Calibrations.from_backend(FakeArmonkV2(), libraries=[FixedFrequencyTransmon()]) - @data(-0.5e6, -0.1e6, 0.1e6, 0.5e6) def test_end_to_end(self, freq_shift): """Test the experiment end to end.""" @@ -56,7 +40,6 @@ def test_end_to_end(self, freq_shift): exp_helper.dt = BackendData(backend).dt freq_exp = FineFrequency([0], 160, backend) - freq_exp.set_transpile_options(inst_map=self.inst_map) expdata = freq_exp.run(shots=100) self.assertExperimentDone(expdata) @@ -70,28 +53,6 @@ def test_end_to_end(self, freq_shift): self.assertAlmostEqual(d_freq, freq_shift, delta=tol) self.assertEqual(result.quality, "good") - def test_calibration_version(self): - """Test the calibration version of the experiment.""" - - exp_helper = FineFreqHelper(sx_duration=self.sx_duration, freq_shift=0.1e6) - backend = MockIQBackend(exp_helper) - exp_helper.dt = BackendData(backend).dt - - fine_freq = FineFrequencyCal([0], self.cals, backend) - armonk_freq = BackendData(FakeArmonkV2()).drive_freqs[0] - - freq_before = self.cals.get_parameter_value("drive_freq", 0) - - self.assertAlmostEqual(freq_before, armonk_freq) - - expdata = fine_freq.run() - self.assertExperimentDone(expdata) - - freq_after = self.cals.get_parameter_value("drive_freq", 0) - - # Test equality up to 10kHz on a 100 kHz shift - self.assertAlmostEqual(freq_after, armonk_freq + exp_helper.freq_shift, delta=1e4) - def test_experiment_config(self): """Test converting to and from config works""" exp = FineFrequency([0], 160) diff --git a/test/library/characterization/test_half_angle.py b/test/library/characterization/test_half_angle.py index 9d0ebfd58f..15877ce010 100644 --- a/test/library/characterization/test_half_angle.py +++ b/test/library/characterization/test_half_angle.py @@ -13,11 +13,6 @@ """Test the half angle experiment.""" from test.base import QiskitExperimentsTestCase -import copy - -from qiskit import pulse, transpile -from qiskit.pulse import InstructionScheduleMap -from qiskit_ibm_runtime.fake_provider import FakeAthensV2 from qiskit_experiments.test.mock_iq_backend import MockIQBackend from qiskit_experiments.test.mock_iq_helpers import MockIQHalfAngleHelper as HalfAngleHelper @@ -48,24 +43,12 @@ def test_circuits(self): qubit = 1 - inst_map = InstructionScheduleMap() - for inst in ["sx", "x"]: - inst_map.add(inst, (qubit,), pulse.Schedule(name=inst)) - hac = HalfAngle([qubit]) - hac.set_transpile_options(inst_map=inst_map) - - # mimic what will happen in the experiment. - transpile_opts = copy.copy(hac.transpile_options.__dict__) - transpile_opts["initial_layout"] = list(hac._physical_qubits) - circuits = transpile(hac.circuits(), FakeAthensV2(), **transpile_opts) - for idx, circ in enumerate(circuits): + for idx, circ in enumerate(hac.circuits()): self.assertEqual(circ.count_ops()["sx"], idx * 2 + 2) - self.assertEqual(circ.calibrations["sx"][((qubit,), ())], pulse.Schedule(name="sx")) if idx > 0: self.assertEqual(circ.count_ops()["x"], idx) - self.assertEqual(circ.calibrations["x"][((qubit,), ())], pulse.Schedule(name="x")) def test_experiment_config(self): """Test converting to and from config works""" diff --git a/test/library/characterization/test_multi_state_discrimination.py b/test/library/characterization/test_multi_state_discrimination.py index c2294f8f01..a3cb163b8f 100644 --- a/test/library/characterization/test_multi_state_discrimination.py +++ b/test/library/characterization/test_multi_state_discrimination.py @@ -17,11 +17,10 @@ from ddt import ddt, data -from qiskit import pulse from qiskit.exceptions import MissingOptionalLibraryError from qiskit_experiments.library import MultiStateDiscrimination -from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend +from qiskit_experiments.test.mock_iq_backend import MockMultiStateBackend from qiskit_experiments.framework.package_deps import HAS_SKLEARN @@ -49,34 +48,16 @@ def setUp(self): """Setup test variables.""" super().setUp() - self.backend = SingleTransmonTestBackend(noise=False, atol=1e-3) + self.backend = MockMultiStateBackend(iq_centers=[1, 1j, -1], rng_seed=1234) # Build x12 schedule self.qubit = 0 - anharm = self.backend.anharmonicity - - d0 = pulse.DriveChannel(self.qubit) - - sch_map = self.backend.defaults().instruction_schedule_map - pulse_x = sch_map.get("x", (self.qubit,)).instructions[0][1].pulse - amp_x = pulse_x.amp - dur_x = pulse_x.duration - sigma_x = pulse_x.sigma - with pulse.build(name="x12") as x12: - pulse.shift_frequency(anharm, d0) - pulse.play(pulse.Gaussian(dur_x, amp_x * self.backend.rabi_rate_12, sigma_x), d0) - pulse.shift_frequency(-anharm, d0) - - self.schedules = {"x12": x12} - @data(2, 3) @requires_sklearn def test_circuit_generation(self, n_states): """Test the experiment circuit generation""" - exp = MultiStateDiscrimination( - [self.qubit], n_states=n_states, backend=self.backend, schedules=self.schedules - ) + exp = MultiStateDiscrimination([self.qubit], n_states=n_states, backend=self.backend) self.assertEqual(len(exp.circuits()), n_states) # check the metadata @@ -86,9 +67,7 @@ def test_circuit_generation(self, n_states): @requires_sklearn def test_discrimination_analysis(self, n_states): """Test the discrimination analysis""" - exp = MultiStateDiscrimination( - [self.qubit], n_states=n_states, backend=self.backend, schedules=self.schedules - ) + exp = MultiStateDiscrimination([self.qubit], n_states=n_states, backend=self.backend) exp_data = exp.run() @@ -104,7 +83,5 @@ def test_discrimination_analysis(self, n_states): def test_circuit_roundtrip_serializable(self): """Test round trip JSON serialization for the experiment circuits.""" - exp = MultiStateDiscrimination( - [self.qubit], n_states=3, backend=self.backend, schedules=self.schedules - ) + exp = MultiStateDiscrimination([self.qubit], n_states=3, backend=self.backend) self.assertRoundTripSerializable(exp._transpiled_circuits()) diff --git a/test/library/characterization/test_qubit_spectroscopy.py b/test/library/characterization/test_qubit_spectroscopy.py deleted file mode 100644 index e49d567e3f..0000000000 --- a/test/library/characterization/test_qubit_spectroscopy.py +++ /dev/null @@ -1,296 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Spectroscopy tests.""" -from test.base import QiskitExperimentsTestCase -import numpy as np - -from qiskit.qobj.utils import MeasLevel -from qiskit.circuit.library import XGate -from qiskit_ibm_runtime.fake_provider import FakeWashingtonV2 -from qiskit_experiments.framework import ParallelExperiment - -from qiskit_experiments.framework import BackendData -from qiskit_experiments.library import QubitSpectroscopy, EFSpectroscopy -from qiskit_experiments.test.mock_iq_backend import MockIQBackend, MockIQParallelBackend -from qiskit_experiments.test.mock_iq_helpers import MockIQSpectroscopyHelper as SpectroscopyHelper -from qiskit_experiments.test.mock_iq_helpers import ( - MockIQParallelExperimentHelper as ParallelExperimentHelper, -) - - -class TestQubitSpectroscopy(QiskitExperimentsTestCase): - """Test spectroscopy experiment.""" - - def test_spectroscopy_end2end_classified(self): - """End to end test of the spectroscopy experiment.""" - - exp_helper = SpectroscopyHelper( - line_width=2e6, - iq_cluster_centers=[((-1.0, -1.0), (1.0, 1.0))], - iq_cluster_width=[0.2], - ) - backend = MockIQBackend( - experiment_helper=exp_helper, - ) - backend.target.add_instruction(XGate(), properties={(0,): None}) - - qubit = 1 - freq01 = BackendData(backend).drive_freqs[qubit] - frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) - - spec = QubitSpectroscopy([qubit], frequencies) - spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) - expdata = spec.run(backend) - self.assertExperimentDone(expdata) - result = expdata.analysis_results("f01") - self.assertRoundTripSerializable(result.value) - - self.assertAlmostEqual(result.value.n, freq01, delta=1e6) - self.assertEqual(result.quality, "good") - self.assertEqual(str(result.device_components[0]), f"Q{qubit}") - - # Test if we find still find the peak when it is shifted by 5 MHz. - exp_helper.freq_offset = 5.0e6 - spec = QubitSpectroscopy([qubit], frequencies) - spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) - expdata = spec.run(backend) - self.assertExperimentDone(expdata) - result = expdata.analysis_results("f01") - self.assertRoundTripSerializable(result.value) - - self.assertAlmostEqual(result.value.n, freq01 + 5e6, delta=1e6) - self.assertEqual(result.quality, "good") - - def test_spectroscopy_end2end_kerneled(self): - """End to end test of the spectroscopy experiment on IQ data.""" - - exp_helper = SpectroscopyHelper( - line_width=2e6, - iq_cluster_centers=[((-1.0, -1.0), (1.0, 1.0))], - iq_cluster_width=[0.2], - ) - backend = MockIQBackend( - experiment_helper=exp_helper, - ) - backend.target.add_instruction(XGate(), properties={(0,): None}) - - qubit = 0 - freq01 = BackendData(backend).drive_freqs[qubit] - frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) - - spec = QubitSpectroscopy([qubit], frequencies) - expdata = spec.run(backend) - self.assertExperimentDone(expdata) - result = expdata.analysis_results("f01") - self.assertRoundTripSerializable(result.value) - - self.assertTrue(freq01 - 2e6 < result.value.n < freq01 + 2e6) - self.assertEqual(result.quality, "good") - - exp_helper.freq_offset = 5.0e6 - exp_helper.iq_cluster_centers = [((1.0, 1.0), (-1.0, -1.0))] - - spec = QubitSpectroscopy([qubit], frequencies) - expdata = spec.run(backend) - self.assertExperimentDone(expdata) - result = expdata.analysis_results("f01") - self.assertRoundTripSerializable(result.value) - - self.assertTrue(freq01 + 3e6 < result.value.n < freq01 + 8e6) - self.assertEqual(result.quality, "good") - - spec.set_run_options(meas_return="avg") - expdata = spec.run(backend) - self.assertExperimentDone(expdata) - result = expdata.analysis_results("f01") - self.assertRoundTripSerializable(result.value) - - self.assertTrue(freq01 + 3e6 < result.value.n < freq01 + 8e6) - self.assertEqual(result.quality, "good") - - def test_spectroscopy12_end2end_classified(self): - """End to end test of the spectroscopy experiment with an x pulse.""" - - backend = MockIQBackend( - experiment_helper=SpectroscopyHelper( - line_width=2e6, - iq_cluster_centers=[((-1.0, -1.0), (1.0, 1.0))], - iq_cluster_width=[0.2], - ), - ) - backend.target.add_instruction(XGate(), properties={(0,): None}) - qubit = 0 - freq01 = BackendData(backend).drive_freqs[qubit] - frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) - - # Note that the backend is not sophisticated enough to simulate an e-f - # transition so we run the test with g-e. - spec = EFSpectroscopy([qubit], frequencies) - spec.backend = backend - spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) - expdata = spec.run(backend) - self.assertExperimentDone(expdata) - result = expdata.analysis_results("f12") - self.assertRoundTripSerializable(result.value) - - self.assertTrue(freq01 - 2e6 < result.value.n < freq01 + 2e6) - self.assertEqual(result.quality, "good") - - # Test the circuits - circ = spec.circuits()[0] - self.assertEqual(circ.data[0].operation.name, "x") - self.assertEqual(circ.data[1].operation.name, "Spec") - - def test_experiment_config(self): - """Test converting to and from config works""" - exp = QubitSpectroscopy([1], np.linspace(100, 150, 20) * 1e6) - loaded_exp = QubitSpectroscopy.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertEqualExtended(exp, loaded_exp) - - def test_roundtrip_serializable(self): - """Test round trip JSON serialization""" - exp = QubitSpectroscopy([1], np.linspace(int(100e6), int(150e6), 4)) - # Checking serialization of the experiment - self.assertRoundTripSerializable(exp) - - def test_expdata_serialization(self): - """Test experiment data and analysis data JSON serialization""" - exp_helper = SpectroscopyHelper( - line_width=2e6, - iq_cluster_centers=[((-1.0, -1.0), (1.0, 1.0))], - iq_cluster_width=[0.2], - ) - backend = MockIQBackend( - experiment_helper=exp_helper, - ) - backend.target.add_instruction(XGate(), properties={(0,): None}) - - qubit = 1 - freq01 = BackendData(backend).drive_freqs[qubit] - frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) - exp = QubitSpectroscopy([qubit], frequencies) - - exp.set_run_options(meas_level=MeasLevel.CLASSIFIED, shots=1024) - expdata = exp.run(backend) - self.assertExperimentDone(expdata) - - # Checking serialization of the experiment data obj - self.assertRoundTripSerializable(expdata) - - # Checking serialization of the analysis - self.assertRoundTripSerializable(expdata.analysis_results("f01")) - - def test_kerneled_expdata_serialization(self): - """Test experiment data and analysis data JSON serialization""" - exp_helper = SpectroscopyHelper( - line_width=2e6, - iq_cluster_centers=[((-1.0, -1.0), (1.0, 1.0))], - iq_cluster_width=[0.2], - ) - backend = MockIQBackend( - experiment_helper=exp_helper, - ) - backend.target.add_instruction(XGate(), properties={(0,): None}) - - qubit = 1 - freq01 = BackendData(backend).drive_freqs[qubit] - frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 21) - exp = QubitSpectroscopy([qubit], frequencies) - - exp.set_run_options(meas_level=MeasLevel.KERNELED, shots=1024) - expdata = exp.run(backend) - self.assertExperimentDone(expdata) - - # Checking serialization of the experiment data - self.assertRoundTripSerializable(expdata) - - # Checking serialization of the analysis - self.assertRoundTripSerializable(expdata.analysis_results("f01")) - - def test_parallel_experiment(self): - """Test for parallel experiment""" - # backend initialization - iq_cluster_centers = [ - ((-1.0, 0.0), (1.0, 0.0)), - ((0.0, -1.0), (0.0, 1.0)), - ((3.0, 0.0), (5.0, 0.0)), - ] - - parallel_backend = MockIQParallelBackend( - experiment_helper=None, - rng_seed=0, - ) - parallel_backend.target.add_instruction( - XGate(), - properties={(0,): None, (1,): None}, - ) - - # experiment hyper parameters - qubit1 = 0 - qubit2 = 1 - backend_data = BackendData(parallel_backend) - freq01 = backend_data.drive_freqs[qubit1] - freq02 = backend_data.drive_freqs[qubit2] - - # experiments initialization - frequencies1 = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 23) - frequencies2 = np.linspace(freq02 - 10.0e6, freq02 + 10.0e6, 21) - - exp_list = [ - QubitSpectroscopy( - [qubit1], - frequencies1, - ), - QubitSpectroscopy( - [qubit2], - frequencies2, - ), - ] - - exp_helper_list = [ - SpectroscopyHelper(iq_cluster_centers=iq_cluster_centers), - SpectroscopyHelper(iq_cluster_centers=iq_cluster_centers), - ] - parallel_helper = ParallelExperimentHelper(exp_list, exp_helper_list) - - parallel_backend.experiment_helper = parallel_helper - - # initializing parallel experiment - par_experiment = ParallelExperiment( - exp_list, flatten_results=False, backend=parallel_backend - ) - par_experiment.set_run_options( - meas_level=MeasLevel.KERNELED, meas_return="single", shots=20 - ) - - par_data = par_experiment.run() - self.assertExperimentDone(par_data) - - # since under _experiment in kwargs there is an argument of the backend which isn't serializable. - par_data._experiment = None - # Checking serialization of the experiment data - self.assertRoundTripSerializable(par_data) - - for child_data in par_data.child_data(): - self.assertRoundTripSerializable(child_data) - - def test_circuit_roundtrip_serializable(self): - """Test circuits round trip JSON serialization""" - backend = FakeWashingtonV2() - qubit = 1 - freq01 = BackendData(backend).drive_freqs[qubit] - frequencies = np.linspace(freq01 - 10.0e6, freq01 + 10.0e6, 3) - exp = QubitSpectroscopy([1], frequencies, backend=backend) - # Checking serialization of the experiment - self.assertRoundTripSerializable(exp._transpiled_circuits()) diff --git a/test/library/calibration/test_ramsey_xy.py b/test/library/characterization/test_ramsey_xy.py similarity index 54% rename from test/library/calibration/test_ramsey_xy.py rename to test/library/characterization/test_ramsey_xy.py index 693e8b48ca..68b55531ec 100644 --- a/test/library/calibration/test_ramsey_xy.py +++ b/test/library/characterization/test_ramsey_xy.py @@ -12,17 +12,13 @@ """Test Ramsey XY experiments.""" -import unittest from test.base import QiskitExperimentsTestCase from ddt import ddt, data, named_data from qiskit_aer import AerSimulator from qiskit_ibm_runtime.fake_provider import FakeArmonkV2 -from qiskit_experiments.calibration_management.calibrations import Calibrations -from qiskit_experiments.calibration_management.basis_gate_library import FixedFrequencyTransmon -from qiskit_experiments.framework import AnalysisStatus, BackendData, BaseAnalysis -from qiskit_experiments.library import RamseyXY, FrequencyCal +from qiskit_experiments.library import RamseyXY from qiskit_experiments.test.mock_iq_backend import MockIQBackend from qiskit_experiments.test.mock_iq_helpers import MockIQRamseyXYHelper as RamseyXYHelper @@ -31,13 +27,6 @@ class TestRamseyXY(QiskitExperimentsTestCase): """Tests for the Ramsey XY experiment.""" - def setUp(self): - """Initialize some cals.""" - super().setUp() - - library = FixedFrequencyTransmon() - self.cals = Calibrations.from_backend(FakeArmonkV2(), libraries=[library]) - @named_data( ["no_backend", None], ["fake_backend", FakeArmonkV2()], ["aer_backend", AerSimulator()] ) @@ -71,51 +60,6 @@ def test_end_to_end(self, freq_shift: float): self.assertAlmostEqual(freq_est_data.value.n, freq_shift, delta=abs_tol) self.assertLess(freq_est_data.chisq, 3.0) - def test_update_calibrations(self): - """Test that the calibration version of the experiment updates the cals.""" - - tol = 1e4 # 10 kHz resolution - - freq_name = "drive_freq" - - # Check qubit frequency before running the cal - f01 = self.cals.get_parameter_value(freq_name, 0) - self.assertTrue(len(self.cals.parameters_table(parameters=[freq_name])["data"]), 1) - self.assertEqual(f01, BackendData(FakeArmonkV2()).drive_freqs[0]) - - freq_shift = 4e6 - osc_shift = 2e6 - - # oscillation with 6 MHz - backend = MockIQBackend(RamseyXYHelper(freq_shift=freq_shift + osc_shift)) - expdata = FrequencyCal([0], self.cals, backend, osc_freq=osc_shift).run() - self.assertExperimentDone(expdata) - - # Check that qubit frequency after running the cal is shifted by freq_shift, i.e. 4 MHz. - f01 = self.cals.get_parameter_value(freq_name, 0) - self.assertTrue(len(self.cals.parameters_table(parameters=[freq_name])["data"]), 2) - self.assertLess(abs(f01 - (freq_shift + BackendData(FakeArmonkV2()).drive_freqs[0])), tol) - - def test_update_with_failed_analysis(self): - """Test that calibration update handles analysis producing no results - - Here we test that the experiment does not raise an unexpected exception - or hang indefinitely. Since there are no analysis results, we expect - that the calibration update will result in an ERROR status. - """ - backend = MockIQBackend(RamseyXYHelper(freq_shift=0)) - - class NoResults(BaseAnalysis): - """Simple analysis class that generates no results""" - - def _run_analysis(self, experiment_data): - return ([], []) - - expt = FrequencyCal([0], self.cals, backend, auto_update=True) - expt.analysis = NoResults() - expdata = expt.run().block_for_results(timeout=3) - self.assertEqual(expdata.analysis_status(), AnalysisStatus.ERROR) - def test_ramseyxy_experiment_config(self): """Test RamseyXY config roundtrips""" exp = RamseyXY([0]) @@ -134,13 +78,6 @@ def test_circuit_roundtrip_serializable(self): exp = RamseyXY([0], backend=backend) self.assertRoundTripSerializable(exp._transpiled_circuits()) - def test_cal_experiment_config(self): - """Test FrequencyCal config roundtrips""" - exp = FrequencyCal([0], self.cals) - loaded_exp = FrequencyCal.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertEqualExtended(exp, loaded_exp) - def test_residual_plot(self): """Test if plot is changing due to residual plotting.""" freq_shift = 1e3 @@ -159,9 +96,3 @@ def test_residual_plot(self): test_data2_figure_bounds = test_data2.figure(0).figure.figbbox.bounds self.assertNotEqual(test_data_figure_bounds[3], test_data2_figure_bounds[3]) - - @unittest.skip("Cal experiments are not yet JSON serializable") - def test_freqcal_roundtrip_serializable(self): - """Test round trip JSON serialization""" - exp = FrequencyCal([0], self.cals) - self.assertRoundTripSerializable(exp) diff --git a/test/library/characterization/test_resonator_spectroscopy.py b/test/library/characterization/test_resonator_spectroscopy.py deleted file mode 100644 index 38b53c82da..0000000000 --- a/test/library/characterization/test_resonator_spectroscopy.py +++ /dev/null @@ -1,373 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Spectroscopy tests for resonator spectroscopy experiment.""" - -from test.base import QiskitExperimentsTestCase -from typing import Any, List, Tuple - -import numpy as np -from ddt import data, ddt -from qiskit import QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.circuit.library import CXGate, Measure, XGate -from qiskit.qobj.utils import MeasLevel - -from qiskit_experiments.framework import BackendData, ParallelExperiment -from qiskit_experiments.library import ResonatorSpectroscopy -from qiskit_experiments.database_service import Resonator -from qiskit_experiments.test.mock_iq_backend import MockIQBackend, MockIQParallelBackend -from qiskit_experiments.test.mock_iq_helpers import ( - MockIQParallelExperimentHelper as ParallelExperimentHelper, -) -from qiskit_experiments.test.mock_iq_helpers import ( - MockIQSpectroscopyHelper as ResonatorSpectroscopyHelper, -) - - -class MockIQBackendDefaults(MockIQBackend): - """MockIQBackend with defaults() method""" - - def defaults(self): - """Pulse defaults - - NOTE: ResonatorSpectroscopy still relies on defaults() so we add here. - Because defaults() is not in the BackendV2 base class, we do not add it - to Backend classes outside of this test module so that we do not - introduce new dependencies on it. - """ - return self._defaults - - -class MockIQParallelBackendDefaults(MockIQParallelBackend): - """MockIQParallelBackend with defaults() method""" - - def defaults(self): - """Pulse defaults - - NOTE: ResonatorSpectroscopy still relies on defaults() so we add here. - Because defaults() is not in the BackendV2 base class, we do not add it - to Backend classes outside of this test module so that we do not - introduce new dependencies on it. - """ - return self._defaults - - -def data_valid_initial_circuits() -> List[Tuple[Any, str]]: - """Returns a list of parameters for ``test_valid_initial_circuits``. - - Returns: - list: List of tuples containing valid circuits and labels identifying what they represent. - """ - - good_circuit = QuantumCircuit(1) - good_circuit.x(0) - good_initial_gate = XGate() - - return [ - (good_circuit, "normal initial circuit"), - (good_initial_gate, "normal initial single-qubit gate"), - ] - - -def data_invalid_initial_circuits() -> List[Tuple[Any, str]]: - """Returns a list of parameters for ``test_invalid_initial_circuits``. - - Returns: - list: List of tuples containing invalid circuits and labels identifying what they represent. - """ - two_qubit_circuit = QuantumCircuit(2) - two_qubit_circuit.x(1) - two_qubit_gate_circuit = QuantumCircuit(2) - two_qubit_gate_circuit.cx(0, 1) - two_qubit_gate = CXGate() - one_qubit_one_clbit_gate = Measure() - - return [ - (two_qubit_circuit, "two qubit initial circuit with one single-qubit gate"), - (two_qubit_gate_circuit, "two qubit initial circuit with a two-qubit gate"), - (two_qubit_gate, "two qubit gate"), - (one_qubit_one_clbit_gate, "one qubit and one classical bit gate"), - ] - - -@ddt -class TestResonatorSpectroscopy(QiskitExperimentsTestCase): - """Tests for the resonator spectroscopy experiment.""" - - @data(-5e6, -2e6, 0, 1e6, 3e6) - def test_end_to_end(self, freq_shift): - """Test the experiment from end to end.""" - - qubit = 1 - backend = MockIQBackendDefaults( - experiment_helper=ResonatorSpectroscopyHelper( - gate_name="measure", - freq_offset=freq_shift, - iq_cluster_centers=[((0.0, 0.0), (-1.0, 0.0))], - iq_cluster_width=[0.2], - ), - ) - - res_freq = BackendData(backend).meas_freqs[qubit] - - frequencies = np.linspace(res_freq - 20e6, res_freq + 20e6, 51) - spec = ResonatorSpectroscopy([qubit], backend=backend, frequencies=frequencies) - - expdata = spec.run(backend) - self.assertExperimentDone(expdata) - result = expdata.analysis_results("res_freq0") - self.assertRoundTripSerializable(result.value) - - self.assertAlmostEqual(result.value.n, res_freq + freq_shift, delta=0.1e6) - self.assertEqual(str(result.device_components[0]), f"R{qubit}") - self.assertEqual(expdata.metadata["device_components"], [Resonator(qubit)]) - - def test_experiment_config(self): - """Test converting to and from config works""" - exp = ResonatorSpectroscopy([1], frequencies=np.linspace(100, 150, 4) * 1e6) - loaded_exp = ResonatorSpectroscopy.from_config(exp.config()) - self.assertNotEqual(exp, loaded_exp) - self.assertEqualExtended(exp, loaded_exp) - - def test_roundtrip_serializable(self): - """Test round trip JSON serialization""" - exp = ResonatorSpectroscopy([1], frequencies=np.linspace(int(100e6), int(150e6), 4)) - self.assertRoundTripSerializable(exp) - - def test_circuit_roundtrip_serializable(self): - """Test circuits data JSON serialization""" - freq_shift = 20e4 - qubit = 1 - # need backend for dt value in the experiment - backend = MockIQBackendDefaults( - experiment_helper=ResonatorSpectroscopyHelper( - gate_name="measure", - freq_offset=freq_shift, - iq_cluster_centers=[((0.0, 0.0), (-1.0, 0.0))], - iq_cluster_width=[0.2], - ), - ) - res_freq = BackendData(backend).meas_freqs[qubit] - frequencies = np.linspace(res_freq - 20e6, res_freq + 20e6, 3) - exp = ResonatorSpectroscopy([qubit], backend=backend, frequencies=frequencies) - self.assertRoundTripSerializable(exp._transpiled_circuits()) - - @data(-5e6, 0, 3e6) - def test_kerneled_expdata_serialization(self, freq_shift): - """Test experiment data and analysis data JSON serialization""" - qubit = 1 - backend = MockIQBackendDefaults( - experiment_helper=ResonatorSpectroscopyHelper( - gate_name="measure", - freq_offset=freq_shift, - iq_cluster_centers=[((0.0, 0.0), (-1.0, 0.0))], - iq_cluster_width=[0.2], - ), - ) - - res_freq = BackendData(backend).meas_freqs[qubit] - - frequencies = np.linspace(res_freq - 20e6, res_freq + 20e6, 11) - exp = ResonatorSpectroscopy([qubit], backend=backend, frequencies=frequencies) - - expdata = exp.run(backend) - self.assertExperimentDone(expdata) - - # since under _experiment in kwargs there is an argument of the backend which isn't serializable. - expdata._experiment = None - # Checking serialization of the experiment data - self.assertRoundTripSerializable(expdata) - - # Checking serialization of the analysis - self.assertRoundTripSerializable(expdata.analysis_results("res_freq0")) - - def test_parallel_experiment(self): - """Test for parallel experiment""" - # backend initialization - iq_cluster_centers = [ - ((-1.0, 0.0), (1.0, 0.0)), - ((0.0, -1.0), (0.0, 1.0)), - ((3.0, 0.0), (5.0, 0.0)), - ] - - freq_shift = [-5e6, 3e6] - exp_helper_list = [ - ResonatorSpectroscopyHelper( - gate_name="measure", - freq_offset=freq_shift[0], - iq_cluster_centers=iq_cluster_centers, - ), - ResonatorSpectroscopyHelper( - gate_name="measure", - freq_offset=freq_shift[1], - iq_cluster_centers=iq_cluster_centers, - ), - ] - - parallel_backend = MockIQParallelBackendDefaults( - experiment_helper=None, - rng_seed=0, - ) - - qubit1 = 0 - qubit2 = 1 - - backend_data = BackendData(parallel_backend) - res_freq1 = backend_data.meas_freqs[qubit1] - res_freq2 = backend_data.meas_freqs[qubit2] - - frequencies1 = np.linspace(res_freq1 - 20e6, res_freq1 + 20e6, 11) - frequencies2 = np.linspace(res_freq2 - 20e6, res_freq2 + 20e6, 13) - - res_spect1 = ResonatorSpectroscopy( - [qubit1], backend=parallel_backend, frequencies=frequencies1 - ) - res_spect2 = ResonatorSpectroscopy( - [qubit2], backend=parallel_backend, frequencies=frequencies2 - ) - - exp_list = [res_spect1, res_spect2] - - # Initializing parallel helper - parallel_helper = ParallelExperimentHelper(exp_list, exp_helper_list) - - # setting the helper into the backend - parallel_backend.experiment_helper = parallel_helper - - par_experiment = ParallelExperiment( - exp_list, flatten_results=False, backend=parallel_backend - ) - par_experiment.set_run_options(meas_level=MeasLevel.KERNELED, meas_return="single") - - par_data = par_experiment.run() - self.assertExperimentDone(par_data) - - # since under _experiment in kwargs there is an argument of the backend which isn't serializable. - par_data._experiment = None - # Checking serialization of the experiment data - self.assertRoundTripSerializable(par_data) - - for child_data in par_data.child_data(): - self.assertRoundTripSerializable(child_data) - for analysis_result in child_data.analysis_results(): - self.assertRoundTripSerializable(analysis_result) - - def test_initial_circuit_transpiled(self): - """Test that the initial circuit is added to the experiment circuits correctly.""" - # Create backend to assist with circuit creation - backend = MockIQBackendDefaults( - experiment_helper=ResonatorSpectroscopyHelper( - gate_name="measure", - freq_offset=1e6, - iq_cluster_centers=[((0.0, 0.0), (-1.0, 0.0))], - iq_cluster_width=[0.2], - ), - ) - - # Create arbitrary initial circuit - initial_circuit = QuantumCircuit(1, name="initial_circuit") - initial_circuit.x(0) - - # Create resonator spectroscopy experiments. We only use 3 frequencies to reduce the number of - # circuits to check. - res_spec_no_initial = ResonatorSpectroscopy( - [0], - backend=backend, - frequencies=[5e9, 5.05e9, 5.1e9], - ) - res_spec_initial = ResonatorSpectroscopy( - [0], - backend=backend, - frequencies=[5e9, 5.05e9, 5.1e9], - ) - res_spec_initial.set_experiment_options(initial_circuit=initial_circuit) - - # Check depths and widths to verify that an initial circuit has been added. - for circ in res_spec_no_initial.circuits(): - self.assertEqual(circ.width(), 2, msg="Circuit width was not as expected.") - self.assertEqual(circ.depth(), 1, msg="Circuit depth was not as expected.") - for circ in res_spec_initial.circuits(): - self.assertEqual( - circ.width(), 2, msg="Circuit, with initial_circuit, width was not as expected." - ) - self.assertEqual( - circ.depth(), 2, msg="Circuit, with initial_circuit, depth was not as expected." - ) - - # Check depths and widths for transpiled circuits - initial_circuit_depth = initial_circuit.depth() - for circ in res_spec_no_initial._transpiled_circuits(): - self.assertEqual( - circ.width(), - # Width is the number of qubits + 1 classical bit. - backend.num_qubits + 1, - msg="Transpiled circuit width was not as expected.", - ) - self.assertEqual( - circ.depth(), - 1, - msg="Transpiled circuit depth was not as expected.", - ) - for circ in res_spec_initial._transpiled_circuits(): - self.assertEqual( - circ.width(), - # Width is the number of qubits + 1 classical bit. - backend.num_qubits + 1, - msg="Transpiled circuit, with initial_circuit, width was not as expected.", - ) - self.assertEqual( - circ.depth(), - initial_circuit_depth + 1, - msg="Transpiled circuit, with initial_circuit, depth was not as expected.", - ) - - @data(*data_valid_initial_circuits()) - def test_valid_initial_circuits(self, params): - """Test successful setting of valid ``initial_circuit`` values.""" - circuit, circuit_label = params - backend = MockIQBackendDefaults( - experiment_helper=ResonatorSpectroscopyHelper( - gate_name="measure", - freq_offset=1e6, - iq_cluster_centers=[((0.0, 0.0), (-1.0, 0.0))], - iq_cluster_width=[0.2], - ), - ) - - res_spec = ResonatorSpectroscopy([0], backend) - try: - res_spec.set_experiment_options(initial_circuit=circuit) - except QiskitError as exp_exception: - self.fail( - f"Setting initial circuit to '{circuit_label}' failed with exception {exp_exception}." - ) - - @data(*data_invalid_initial_circuits()) - def test_invalid_initial_circuits(self, params): - """Test detection of invalid ``initial_circuit`` values.""" - circuit, circuit_label = params - backend = MockIQBackendDefaults( - experiment_helper=ResonatorSpectroscopyHelper( - gate_name="measure", - freq_offset=1e6, - iq_cluster_centers=[((0.0, 0.0), (-1.0, 0.0))], - iq_cluster_width=[0.2], - ), - ) - - res_spec = ResonatorSpectroscopy([0], backend) - with self.assertRaises( - QiskitError, - msg=f"Setting initial circuit to invalid '{circuit_label}' did not fail with exception.", - ): - res_spec.set_experiment_options(initial_circuit=circuit) diff --git a/test/library/characterization/test_zz_ramsey.py b/test/library/characterization/test_zz_ramsey.py index 6012f5f279..40af9f62e7 100644 --- a/test/library/characterization/test_zz_ramsey.py +++ b/test/library/characterization/test_zz_ramsey.py @@ -13,53 +13,16 @@ """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, named_data, unpack -from qiskit import QuantumCircuit from qiskit_aer import AerSimulator from qiskit_ibm_runtime.fake_provider import FakeVigoV2 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 - circdata = next(i for i in circuit.data if i.operation.name == "u1") - rz = circdata.operation - 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 +from qiskit_experiments.test.zzramsey_test_backend import ZZRamseyTestBackend @ddt @@ -99,11 +62,11 @@ def test_circuits(self, backend): @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 + backend = ZZRamseyTestBackend( + zz_frequency=zz_freq, + t2hahn=10e-6, + initialization_error=0.05, + ) ramsey = ZZRamsey((0, 1), backend, num_rotations=num_rotations) test_data = ramsey.run() diff --git a/test/library/driven_freq_tuning/__init__.py b/test/library/driven_freq_tuning/__init__.py deleted file mode 100644 index 4575d01965..0000000000 --- a/test/library/driven_freq_tuning/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -"""Tests for driven frequency tuning.""" diff --git a/test/library/driven_freq_tuning/test_coeffs.py b/test/library/driven_freq_tuning/test_coeffs.py deleted file mode 100644 index ce1cd9ee85..0000000000 --- a/test/library/driven_freq_tuning/test_coeffs.py +++ /dev/null @@ -1,173 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test for Stark coefficients utility.""" - -from test.base import QiskitExperimentsTestCase - -from ddt import ddt, named_data, data, unpack -import numpy as np - -from qiskit_experiments.library.driven_freq_tuning import coefficients as util -from qiskit_experiments.test import FakeService - - -@ddt -class TestStarkUtil(QiskitExperimentsTestCase): - """Test cases for Stark coefficient utilities.""" - - def test_coefficients(self): - """Test getting group of coefficients.""" - coeffs = util.StarkCoefficients( - pos_coef_o1=1e6, - pos_coef_o2=2e6, - pos_coef_o3=3e6, - neg_coef_o1=-1e6, - neg_coef_o2=-2e6, - neg_coef_o3=-3e6, - offset=0, - ) - self.assertListEqual(coeffs.positive_coeffs(), [3e6, 2e6, 1e6]) - self.assertListEqual(coeffs.negative_coeffs(), [-3e6, -2e6, -1e6]) - - def test_roundtrip_coefficients(self): - """Test serializing and deserializing the coefficient object.""" - coeffs = util.StarkCoefficients( - pos_coef_o1=1e6, - pos_coef_o2=2e6, - pos_coef_o3=3e6, - neg_coef_o1=-1e6, - neg_coef_o2=-2e6, - neg_coef_o3=-3e6, - offset=0, - ) - self.assertRoundTripSerializable(coeffs) - - @named_data( - ["ordinary", 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3], - ["asymmetric_inflection_1st_ord", 10e6, 200e6, -20e6, -50e6, -180e6, -20e6, -10e6], - ["inflection_3st_ord", 10e6, 200e6, -80e6, 80e6, -180e6, -200e6, 100e3], - ) - @unpack - def test_roundtrip_convert_freq_amp( - self, - pos_o1: float, - pos_o2: float, - pos_o3: float, - neg_o1: float, - neg_o2: float, - neg_o3: float, - offset: float, - ): - """Test round-trip conversion between frequency shift and Stark amplitude.""" - coeffs = util.StarkCoefficients( - pos_coef_o1=pos_o1, - pos_coef_o2=pos_o2, - pos_coef_o3=pos_o3, - neg_coef_o1=neg_o1, - neg_coef_o2=neg_o2, - neg_coef_o3=neg_o3, - offset=offset, - ) - target_freqs = np.linspace(-70e6, 70e6, 11) - test_amps = coeffs.convert_freq_to_amp(target_freqs) - test_freqs = coeffs.convert_amp_to_freq(test_amps) - - np.testing.assert_array_almost_equal(test_freqs, target_freqs, decimal=2) - - @data( - [-0.5, 0.5], - [-0.9, 0.9], - [0.25, 1.0], - ) - @unpack - def test_calculate_min_max_shift(self, min_amp, max_amp): - """Test estimating maximum frequency shift within given Stark amplitude budget.""" - - # These coefficients induce inflection points around ±0.75, for testing - coeffs = util.StarkCoefficients( - pos_coef_o1=10e6, - pos_coef_o2=100e6, - pos_coef_o3=-90e6, - neg_coef_o1=80e6, - neg_coef_o2=-180e6, - neg_coef_o3=-200e6, - offset=100e3, - ) - # This numerical solution is correct up to amp resolution of 0.001 - nop = int((max_amp - min_amp) / 0.001) - amps = np.linspace(min_amp, max_amp, nop) - freqs = coeffs.convert_amp_to_freq(amps) - - # This finds strict solution, unless it has a bug - min_freq, max_freq = coeffs.find_min_max_frequency( - min_amp=min_amp, - max_amp=max_amp, - ) - - # Allow 1kHz tolerance because ref is approximate value - self.assertAlmostEqual(min_freq, np.min(freqs), delta=1e3) - self.assertAlmostEqual(max_freq, np.max(freqs), delta=1e3) - - def test_get_coeffs_from_service(self): - """Test retrieve the saved Stark coefficients from the experiment service.""" - mock_experiment_id = "6453f3d1-04ef-4e3b-82c6-1a92e3e066eb" - mock_result_id = "d067ae34-96db-4e8e-adc8-030305d3d404" - mock_backend = "mock_backend" - - ref_coeffs = util.StarkCoefficients( - pos_coef_o1=1e6, - pos_coef_o2=2e6, - pos_coef_o3=3e6, - neg_coef_o1=-1e6, - neg_coef_o2=-2e6, - neg_coef_o3=-3e6, - offset=0, - ) - - service = FakeService() - service.create_experiment( - experiment_type="StarkRamseyXYAmpScan", - backend_name=mock_backend, - experiment_id=mock_experiment_id, - ) - service.create_analysis_result( - experiment_id=mock_experiment_id, - result_data={"value": ref_coeffs}, - result_type="stark_coefficients", - device_components=["Q0"], - tags=[], - quality="Good", - verified=False, - result_id=mock_result_id, - ) - - retrieved = util.retrieve_coefficients_from_service( - service=service, - backend_name=mock_backend, - qubit=0, - ) - - self.assertEqual(retrieved, ref_coeffs) - - def test_get_coeffs_no_data(self): - """Test raises when Stark coefficients don't exist in the result database.""" - mock_backend = "mock_backend" - - service = FakeService() - - with self.assertRaises(RuntimeError): - util.retrieve_coefficients_from_service( - service=service, - backend_name=mock_backend, - qubit=0, - ) diff --git a/test/library/driven_freq_tuning/test_stark_p1_spect.py b/test/library/driven_freq_tuning/test_stark_p1_spect.py deleted file mode 100644 index ed3be9c25b..0000000000 --- a/test/library/driven_freq_tuning/test_stark_p1_spect.py +++ /dev/null @@ -1,304 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test Stark P1 spectroscopy experiment.""" - -from test.base import QiskitExperimentsTestCase - -from ddt import ddt, named_data, unpack -import numpy as np -from qiskit import pulse -from qiskit.circuit import QuantumCircuit, Gate -from qiskit.providers import QubitProperties -from qiskit_ibm_runtime.fake_provider import FakeHanoiV2 - -from qiskit_experiments.framework import ExperimentData, AnalysisResultData -from qiskit_experiments.library import StarkP1Spectroscopy -from qiskit_experiments.library.driven_freq_tuning.p1_spect_analysis import StarkP1SpectAnalysis -from qiskit_experiments.library.driven_freq_tuning.coefficients import StarkCoefficients -from qiskit_experiments.test import FakeService - - -class StarkP1SpectAnalysisReturnXvals(StarkP1SpectAnalysis): - """A test analysis class that returns x values.""" - - def _run_spect_analysis( - self, - xdata: np.ndarray, - ydata: np.ndarray, - ydata_err: np.ndarray, - ): - return [ - AnalysisResultData( - name="xvals", - value=xdata, - ) - ] - - -@ddt -class TestStarkP1Spectroscopy(QiskitExperimentsTestCase): - """Test case for the Stark P1 Spectroscopy experiment.""" - - def test_linear_spaced_parameters(self): - """Test generating parameters with linear spacing.""" - exp = StarkP1Spectroscopy((0,)) - exp.set_experiment_options( - min_xval=-1, - max_xval=1, - num_xvals=5, - spacing="linear", - ) - params = exp.parameters() - ref = np.array([-1.0, -0.5, 0.0, 0.5, 1.0]) - - np.testing.assert_array_almost_equal(params, ref) - - def test_quadratic_spaced_parameters(self): - """Test generating parameters with quadratic spacing.""" - exp = StarkP1Spectroscopy((0,)) - exp.set_experiment_options( - min_xval=-1, - max_xval=1, - num_xvals=5, - spacing="quadratic", - ) - params = exp.parameters() - ref = np.array([-1.0, -0.25, 0.0, 0.25, 1.0]) - - np.testing.assert_array_almost_equal(params, ref) - - def test_invalid_spacing(self): - """Test setting invalid spacing option.""" - exp = StarkP1Spectroscopy((0,)) - with self.assertRaises(ValueError): - exp.set_experiment_options(spacing="invalid_option") - - def test_raises_scanning_frequency_without_service(self): - """Test raises error when frequency is set without no coefficients. - - This covers following situations: - - stark_coefficients options is None - - backend object doesn't provide experiment service - """ - exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) - exp.set_experiment_options( - xvals=[-100e6, -50e6, 0, 50e6, 100e6], - xval_type="frequency", - ) - with self.assertRaises(RuntimeError): - exp.parameters() - - def test_scanning_frequency_with_coeffs(self): - """Test scanning frequency with manually provided Stark coefficients.""" - coeffs = StarkCoefficients( - pos_coef_o1=5e6, - pos_coef_o2=200e6, - pos_coef_o3=-50e6, - neg_coef_o1=5e6, - neg_coef_o2=-180e6, - neg_coef_o3=-40e6, - offset=100e3, - ) - exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) - - ref_amps = np.array([-0.50, -0.25, 0.0, 0.25, 0.50], dtype=float) - test_freqs = coeffs.convert_amp_to_freq(ref_amps) - exp.set_experiment_options( - xvals=test_freqs, - xval_type="frequency", - stark_coefficients=coeffs, - ) - params = exp.parameters() - np.testing.assert_array_almost_equal(params, ref_amps) - - def test_scanning_frequency_around_zero(self): - """Test scanning frequency around zero.""" - coeffs = StarkCoefficients( - pos_coef_o1=5e6, - pos_coef_o2=100e6, - pos_coef_o3=10e6, - neg_coef_o1=-5e6, - neg_coef_o2=-100e6, - neg_coef_o3=-10e6, - offset=500e3, - ) - exp = StarkP1Spectroscopy((0,), backend=FakeHanoiV2()) - exp.set_experiment_options( - xvals=[0, 500e3], - xval_type="frequency", - stark_coefficients=coeffs, - ) - params = exp.parameters() - # Frequency offset is 500 kHz and we need negative shift to tune frequency at zero. - self.assertLess(params[0], 0) - - # Frequency offset is 500 kHz and we don't need tone. - self.assertAlmostEqual(params[1], 0) - - def test_circuits(self): - """Test generated circuits.""" - backend = FakeHanoiV2() - - # For simplicity of the test - backend.target.dt = 1 - backend.target.granularity = 1 - backend.target.pulse_alignment = 1 - backend.target.acquire_alignment = 1 - backend.target.qubit_properties = [QubitProperties(frequency=1e9)] - - exp = StarkP1Spectroscopy((0,), backend) - exp.set_experiment_options( - xvals=[-0.5, 0.5], - stark_freq_offset=10e6, - t1_delay=100, - stark_sigma=15, - stark_risefall=2, - ) - circs = exp.circuits() - - # amp = -0.5 - with pulse.build() as sched1: - # Red shift: must be greater than f01 - pulse.set_frequency(1.01e9, pulse.DriveChannel(0)) - pulse.play( - # Always positive amplitude - pulse.GaussianSquare(100, 0.5, 15, 40), - pulse.DriveChannel(0), - ) - qc1 = QuantumCircuit(1, 1) - qc1.x(0) - qc1.append(Gate("Stark", 1, [-0.5]), [0]) - qc1.measure(0, 0) - qc1.add_calibration("Stark", (0,), sched1, [-0.5]) - - # amp = +0.5 - with pulse.build() as sched2: - # Blue shift: Must be lower than f01 - pulse.set_frequency(0.99e9, pulse.DriveChannel(0)) - pulse.play( - # Always positive amplitude - pulse.GaussianSquare(100, 0.5, 15, 40), - pulse.DriveChannel(0), - ) - qc2 = QuantumCircuit(1, 1) - qc2.x(0) - qc2.append(Gate("Stark", 1, [0.5]), [0]) - qc2.measure(0, 0) - qc2.add_calibration("Stark", (0,), sched2, [0.5]) - - self.assertEqual(circs[0], qc1) - self.assertEqual(circs[1], qc2) - - def test_running_analysis_without_service(self): - """Test running analysis without setting service to the experiment data. - - This uses input xvals as-is. - """ - analysis = StarkP1SpectAnalysisReturnXvals() - - xvals = np.linspace(-1, 1, 11) - ref_xvals = xvals - exp_data = ExperimentData() - for x in xvals: - exp_data.add_data({"counts": {"0": 1000, "1": 0}, "metadata": {"xval": x}}) - analysis.run(exp_data, replace_results=True) - test_xvals = exp_data.analysis_results("xvals").value - np.testing.assert_array_almost_equal(test_xvals, ref_xvals) - - @named_data( - ["ordinary", 5e6, 200e6, -50e6, 5e6, -180e6, -40e6, 100e3], - ["asymmetric_inflection_1st_ord", 10e6, 200e6, -20e6, -50e6, -180e6, -20e6, -10e6], - ["inflection_3st_ord", 10e6, 200e6, -80e6, 80e6, -180e6, -200e6, 100e3], - ) - @unpack - def test_running_analysis_with_service(self, po1, po2, po3, no1, no2, no3, ferr): - """Test running analysis by setting service to the experiment data. - - This must convert x-axis into frequencies with the Stark coefficients. - """ - mock_experiment_id = "6453f3d1-04ef-4e3b-82c6-1a92e3e066eb" - mock_result_id = "d067ae34-96db-4e8e-adc8-030305d3d404" - mock_backend = FakeHanoiV2().name - - coeffs = StarkCoefficients( - pos_coef_o1=po1, - pos_coef_o2=po2, - pos_coef_o3=po3, - neg_coef_o1=no1, - neg_coef_o2=no2, - neg_coef_o3=no3, - offset=ferr, - ) - - service = FakeService() - service.create_experiment( - experiment_type="StarkRamseyXYAmpScan", - backend_name=mock_backend, - experiment_id=mock_experiment_id, - ) - service.create_analysis_result( - experiment_id=mock_experiment_id, - result_data={"value": coeffs}, - result_type="stark_coefficients", - device_components=["Q0"], - tags=[], - quality="Good", - verified=False, - result_id=mock_result_id, - ) - - analysis = StarkP1SpectAnalysisReturnXvals() - - xvals = np.linspace(-1, 1, 11) - ref_fvals = coeffs.convert_amp_to_freq(xvals) - - exp_data = ExperimentData( - service=service, - backend=FakeHanoiV2(), - ) - exp_data.metadata.update({"physical_qubits": [0]}) - for x in xvals: - exp_data.add_data({"counts": {"0": 1000, "1": 0}, "metadata": {"xval": x}}) - analysis.run(exp_data, replace_results=True).block_for_results() - test_fvals = exp_data.analysis_results("xvals").value - np.testing.assert_array_almost_equal(test_fvals, ref_fvals) - - def test_running_analysis_with_user_provided_coeffs(self): - """Test running analysis by manually providing Stark coefficients. - - This must convert x-axis into frequencies with the provided coefficients. - This is just a difference of API from the test_running_analysis_with_service. - Data driven test is omitted here. - """ - coeffs = StarkCoefficients( - pos_coef_o1=5e6, - pos_coef_o2=200e6, - pos_coef_o3=-50e6, - neg_coef_o1=5e6, - neg_coef_o2=-180e6, - neg_coef_o3=-40e6, - offset=100e3, - ) - - analysis = StarkP1SpectAnalysisReturnXvals() - analysis.set_options(stark_coefficients=coeffs) - - xvals = np.linspace(-1, 1, 11) - ref_fvals = coeffs.convert_amp_to_freq(xvals) - - exp_data = ExperimentData() - for x in xvals: - exp_data.add_data({"counts": {"0": 1000, "1": 0}, "metadata": {"xval": x}}) - analysis.run(exp_data, replace_results=True).block_for_results() - test_fvals = exp_data.analysis_results("xvals").value - np.testing.assert_array_almost_equal(test_fvals, ref_fvals) diff --git a/test/library/driven_freq_tuning/test_stark_ramsey_xy.py b/test/library/driven_freq_tuning/test_stark_ramsey_xy.py deleted file mode 100644 index 3791ce9d7a..0000000000 --- a/test/library/driven_freq_tuning/test_stark_ramsey_xy.py +++ /dev/null @@ -1,300 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test Stark Ramsey XY experiments.""" - -from test.base import QiskitExperimentsTestCase - -import numpy as np -from ddt import ddt, unpack, named_data - -from qiskit import pulse -from qiskit_ibm_runtime.fake_provider import FakeHanoiV2 - -from qiskit_experiments.library import StarkRamseyXY, StarkRamseyXYAmpScan -from qiskit_experiments.library.driven_freq_tuning.ramsey_amp_scan_analysis import ( - StarkRamseyXYAmpScanAnalysis, -) -from qiskit_experiments.library.driven_freq_tuning.coefficients import StarkCoefficients -from qiskit_experiments.framework import ExperimentData - - -class TestStarkRamseyXY(QiskitExperimentsTestCase): - """Test case for the Stark Ramsey XY experiment. - - Because the analysis is identical to the RamseyXY, integration test and - test for analysis class is skipped here. These must be covered by - test/library/calibration/test_ramsey_xy.py - """ - - def test_calibration_with_positive_amp(self): - """Test Stark frequency shift chooses the proper sign with positive amplitude. - - * Frequency shift must be negative. - * Stark tone amplitude shift must be positive. - """ - backend = FakeHanoiV2() - exp = StarkRamseyXY( - physical_qubits=[0], - stark_amp=0.1, # positive amplitude - backend=backend, - stark_sigma=15e-9, - stark_risefall=2, - stark_freq_offset=80e6, - ) - param_ram_x, _ = exp.parameterized_circuits() - freq = backend.qubit_properties(0).frequency - 80e6 # negative frequency shift - granularity = backend.target.granularity - dt = backend.dt - duration = granularity * int(round(4 * 15e-9 / dt / granularity)) - sigma = duration / 4 - - with pulse.build() as ref_schedule: - pulse.set_frequency(freq, pulse.DriveChannel(0)) - pulse.play( - pulse.Gaussian(duration=duration, amp=0.1, sigma=sigma), pulse.DriveChannel(0) - ) - - test_schedule = param_ram_x.calibrations["StarkV"][(0,), ()] - self.assertEqual(test_schedule, ref_schedule) - - def test_calibration_with_negative_amp(self): - """Test Stark frequency shift chooses the proper sign with negative amplitude. - - * Frequency shift must be positive. - * Stark tone amplitude shift must be positive. - """ - backend = FakeHanoiV2() - exp = StarkRamseyXY( - physical_qubits=[0], - stark_amp=-0.1, # negative amplitude - backend=backend, - stark_sigma=15e-9, - stark_risefall=2, - stark_freq_offset=80e6, - ) - param_ram_x, _ = exp.parameterized_circuits() - freq = backend.qubit_properties(0).frequency + 80e6 # positive frequency shift - granularity = backend.target.granularity - dt = backend.dt - duration = granularity * int(round(4 * 15e-9 / dt / granularity)) - sigma = duration / 4 - - with pulse.build() as ref_schedule: - pulse.set_frequency(freq, pulse.DriveChannel(0)) - pulse.play( - pulse.Gaussian(duration=duration, amp=0.1, sigma=sigma), pulse.DriveChannel(0) - ) - - test_schedule = param_ram_x.calibrations["StarkV"][(0,), ()] - self.assertEqual(test_schedule, ref_schedule) - - def test_gen_delays(self): - """Test generating delays with experiment options.""" - min_freq = 1e6 - max_freq = 50e6 - exp = StarkRamseyXY( - physical_qubits=[0], - stark_amp=0.1, - min_freq=min_freq, - max_freq=max_freq, - ) - test_delays = exp.parameters() - ref_delays = np.arange(0, 1 / min_freq, 1 / max_freq / 2) - np.testing.assert_array_equal(test_delays, ref_delays) - - def test_circuit_valid_delays(self): - """Test Stark tone durations are valid.""" - backend = FakeHanoiV2() - dt = backend.dt - exp = StarkRamseyXY( - physical_qubits=[0], - stark_amp=0.1, - backend=backend, - delays=np.linspace(0, 10e-6, 5), - ) - circs = exp.circuits() - - for circ in circs: - stark_v = next(iter(circ.calibrations["StarkV"].values())) - self.assertEqual(stark_v.duration % backend.target.granularity, 0) - stark_u = next(iter(circ.calibrations["StarkU"].values())) - self.assertEqual(stark_u.duration % backend.target.granularity, 0) - - stark_u_dur = stark_u.duration - stark_v_dur = stark_v.duration - delay_dt = stark_u_dur - stark_v_dur - self.assertAlmostEqual(circ.metadata["xval"], delay_dt * dt) - - def test_stark_offset_always_positive(self): - """Test raise error by definition when the offset is negative.""" - exp = StarkRamseyXY(physical_qubits=[0], stark_amp=0.1) - with self.assertRaises(ValueError): - exp.set_experiment_options(stark_freq_offset=-10e6) - - def test_circuit_roundtrip_serializable(self): - """Test circuit round trip JSON serialization""" - # backend=None due to bug in serialization of backend obj. - backend = FakeHanoiV2() - exp = StarkRamseyXY( - physical_qubits=[0], - stark_amp=0.1, - backend=backend, - delays=np.linspace(0, 10e-6, 5), - ) - self.assertRoundTripSerializable(exp._transpiled_circuits()) - - -@ddt -class TestStarkRamseyXYAmpScan(QiskitExperimentsTestCase): - """Test case for StarkRamseyXYAmpScan experiment.""" - - def test_frequency_shift_direction(self): - """Check frequency shift direction reflects abstracted amplitude policy. - - When amplitude is positive (negative), it must induce positive (negative) Stark shift - for simplicity. To achieve this, the spectral location of the tone must be - lower (higher) than the qubit frequency f01. Tone amplitude is always positive. - """ - backend = FakeHanoiV2() - exp = StarkRamseyXYAmpScan( - physical_qubits=[0], - backend=backend, - stark_amps=[-0.1, 0.1], - ) - f01 = backend.qubit_properties(0).frequency - - circs = exp.circuits() - - # Check circuit metadata - circs0_x_starkv = next(iter(circs[0].calibrations["StarkV"].values())) - self.assertDictEqual(circs[0].metadata, {"xval": -0.1, "series": "X", "direction": "neg"}) - circs0_y_starkv = next(iter(circs[1].calibrations["StarkV"].values())) - self.assertDictEqual(circs[1].metadata, {"xval": -0.1, "series": "Y", "direction": "neg"}) - circs1_x_starkv = next(iter(circs[2].calibrations["StarkV"].values())) - self.assertDictEqual(circs[2].metadata, {"xval": 0.1, "series": "X", "direction": "pos"}) - circs1_y_starkv = next(iter(circs[3].calibrations["StarkV"].values())) - self.assertDictEqual(circs[3].metadata, {"xval": 0.1, "series": "Y", "direction": "pos"}) - - # Check frequency shift - circ0_x_set_freq = circs0_x_starkv.blocks[0].frequency - circ0_y_set_freq = circs0_y_starkv.blocks[0].frequency - # This must induce negative frequency shift. - # This tone frequency must be greater than f01. - self.assertGreater(circ0_x_set_freq, f01) - self.assertGreater(circ0_y_set_freq, f01) - - circ1_x_set_freq = circs1_x_starkv.blocks[0].frequency - circ1_y_set_freq = circs1_y_starkv.blocks[0].frequency - # This must induce positive frequency shift. - # This tone frequency must be lower than f01. - self.assertLess(circ1_x_set_freq, f01) - self.assertLess(circ1_y_set_freq, f01) - - # Check amplitude is always positive - circ0_x_set_amp = circs0_x_starkv.blocks[1].pulse.parameters["amp"] - circ0_y_set_amp = circs0_y_starkv.blocks[1].pulse.parameters["amp"] - self.assertEqual(circ0_x_set_amp, 0.1) - self.assertEqual(circ0_y_set_amp, 0.1) - circ1_x_set_amp = circs1_x_starkv.blocks[1].pulse.parameters["amp"] - circ1_y_set_amp = circs1_y_starkv.blocks[1].pulse.parameters["amp"] - self.assertEqual(circ1_x_set_amp, 0.1) - self.assertEqual(circ1_y_set_amp, 0.1) - - def test_circuit_valid_delays(self): - """Test Stark tone durations are valid.""" - backend = FakeHanoiV2() - exp = StarkRamseyXYAmpScan( - physical_qubits=[0], - backend=backend, - stark_amps=[0.1], - ) - circs = exp.circuits() - - for circ in circs: - stark_v = next(iter(circ.calibrations["StarkV"].values())) - self.assertEqual(stark_v.duration % backend.target.granularity, 0) - stark_u = next(iter(circ.calibrations["StarkU"].values())) - self.assertEqual(stark_u.duration % backend.target.granularity, 0) - - @named_data( - ["ideal_quadratic", 0.0, 30e6, 0.0, 0.0, -30e6, 0.0, 0.0], - ["with_all_terms", 15e6, 200e6, -100e6, 15e6, -200e6, -100e6, 300e3], - ["asymmetric_shift", -20e6, 200e6, -100e6, -15e6, -180e6, -90e6, 200e3], - ["large_cubic_term", 10e6, 15e6, 30e6, 5e6, -10e6, 40e6, 0.0], - ) - @unpack - def test_ramsey_fast_analysis(self, c1p, c2p, c3p, c1n, c2n, c3n, ferr): - """End-to-end test for Ramsey fast analysis with artificial data.""" - amp = 0.5 - off = 0.5 - rng = np.random.default_rng(seed=123) - shots = 1000 - - xvals = np.linspace(-1.0, 1.0, 101) - const = 2 * np.pi * 50e-9 - exp_data = ExperimentData() - exp_data.metadata.update({"stark_length": 50e-9}) - - ref_coeffs = StarkCoefficients( - pos_coef_o1=c1p, - pos_coef_o2=c2p, - pos_coef_o3=c3p, - neg_coef_o1=c1n, - neg_coef_o2=c2n, - neg_coef_o3=c3n, - offset=ferr, - ) - yvals = ref_coeffs.convert_amp_to_freq(xvals) - - # Generate fake data based on fit model. - for x, y in zip(xvals, yvals): - if x >= 0.0: - direction = "pos" - else: - direction = "neg" - - # Add some sampling error - ramx_count = rng.binomial(shots, amp * np.cos(const * y) + off) - exp_data.add_data( - { - "counts": {"0": shots - ramx_count, "1": ramx_count}, - "metadata": {"xval": x, "series": "X", "direction": direction}, - } - ) - ramy_count = rng.binomial(shots, amp * np.sin(const * y) + off) - exp_data.add_data( - { - "counts": {"0": shots - ramy_count, "1": ramy_count}, - "metadata": {"xval": x, "series": "Y", "direction": direction}, - } - ) - - analysis = StarkRamseyXYAmpScanAnalysis() - analysis.run(exp_data, replace_results=True) - self.assertExperimentDone(exp_data) - - # Check the fitted parameter can approximate the same polynominal. - # Note that coefficient values don't need to exactly match as long as - # frequency shift is predictable. - # Since the fit model is just an empirical polynomial, - # comparing coefficients don't physically sound. - # Curves must be agreed within the tolerance of 1.5 * 1 MHz. - fit_coeffs = exp_data.analysis_results("stark_coefficients").value - fit_yvals = fit_coeffs.convert_amp_to_freq(xvals) - - np.testing.assert_array_almost_equal( - yvals, - fit_yvals, - decimal=-6, - err_msg="Reconstructed phase polynominal doesn't match with the actual phase shift.", - ) diff --git a/test/library/randomized_benchmarking/test_interleaved_rb.py b/test/library/randomized_benchmarking/test_interleaved_rb.py index c64671666b..5f02aace72 100644 --- a/test/library/randomized_benchmarking/test_interleaved_rb.py +++ b/test/library/randomized_benchmarking/test_interleaved_rb.py @@ -16,12 +16,10 @@ from test.library.randomized_benchmarking.mixin import RBTestMixin from ddt import ddt, data, unpack -from qiskit import pulse from qiskit.circuit import Delay, QuantumCircuit, Parameter, Gate from qiskit.circuit.library import SXGate, CXGate, TGate, CZGate from qiskit.exceptions import QiskitError from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.transpiler import InstructionProperties from qiskit_aer import AerSimulator from qiskit_aer.noise import NoiseModel, depolarizing_error from qiskit_ibm_runtime.fake_provider import FakeManilaV2 @@ -282,29 +280,6 @@ def test_interleaving_cnot_gate_with_non_supported_direction(self): with self.assertRaises(QiskitError): exp.circuits() - def test_interleaving_three_qubit_gate_with_calibration(self): - """Test if circuits for 3Q InterleavedRB contain custom calibrations supplied via target.""" - with pulse.build(self.backend) as custom_3q_sched: # meaningless schedule - pulse.play(pulse.GaussianSquare(1600, 0.2, 64, 1300), pulse.drive_channel(0)) - - physical_qubits = (2, 1, 3) - custom_3q_gate = self.ThreeQubitGate() - self.backend.target.add_instruction( - custom_3q_gate, {physical_qubits: InstructionProperties(calibration=custom_3q_sched)} - ) - - exp = rb.InterleavedRB( - interleaved_element=custom_3q_gate, - physical_qubits=physical_qubits, - lengths=[3], - num_samples=1, - backend=self.backend, - seed=1234, - ) - circuits = exp._transpiled_circuits() - qubits = tuple(circuits[0].qubits[q] for q in physical_qubits) - self.assertTrue(circuits[0].has_calibration_for((custom_3q_gate, qubits, []))) - class TestRunInterleavedRB(QiskitExperimentsTestCase, RBTestMixin): """Test for running InterleavedRB.""" diff --git a/test/library/randomized_benchmarking/test_layer_fidelity.py b/test/library/randomized_benchmarking/test_layer_fidelity.py index 00f7ea28fb..bf63b11e06 100644 --- a/test/library/randomized_benchmarking/test_layer_fidelity.py +++ b/test/library/randomized_benchmarking/test_layer_fidelity.py @@ -17,9 +17,7 @@ import numpy as np from ddt import ddt, data, unpack -from qiskit.circuit.library import SXGate from qiskit.exceptions import QiskitError -from qiskit.pulse import Schedule from qiskit_ibm_runtime.fake_provider import FakeManilaV2 from qiskit_experiments.library.randomized_benchmarking import LayerFidelity, LayerFidelityAnalysis @@ -142,27 +140,6 @@ def test_return_same_circuit_for_same_config(self): self.assertEqual(circs1[1].decompose(), circs2[1].decompose()) self.assertEqual(circs1[2].decompose(), circs2[2].decompose()) - # ### Tests for transpiled circuit generation ### - def test_calibrations_via_custom_backend(self): - """Test if calibrations given as custom backend show up in transpiled circuits.""" - qubits = (2,) - my_sched = Schedule(name="custom_sx_gate") - my_backend = copy.deepcopy(FakeManilaV2()) - my_backend.target["sx"][qubits].calibration = my_sched - - exp = LayerFidelity( - physical_qubits=(0, 1, 2, 3), - two_qubit_layers=[[(1, 0), (2, 3)], [(1, 2)]], - lengths=[10, 20, 30], - seed=42, - backend=my_backend, - ) - transpiled = exp._transpiled_circuits() - for qc in transpiled: - self.assertTrue(qc.calibrations) - self.assertTrue(qc.has_calibration_for((SXGate(), [qc.qubits[q] for q in qubits], []))) - self.assertEqual(qc.calibrations["sx"][(qubits, tuple())], my_sched) - def test_backend_with_directed_basis_gates(self): """Test if correct circuits are generated from backend with directed basis gates.""" my_backend = copy.deepcopy(FakeManilaV2()) diff --git a/test/library/randomized_benchmarking/test_standard_rb.py b/test/library/randomized_benchmarking/test_standard_rb.py index b11f5b926a..d4e791b2a5 100644 --- a/test/library/randomized_benchmarking/test_standard_rb.py +++ b/test/library/randomized_benchmarking/test_standard_rb.py @@ -17,9 +17,7 @@ from test.library.randomized_benchmarking.mixin import RBTestMixin from ddt import ddt, data, unpack -from qiskit.circuit.library import SXGate from qiskit.exceptions import QiskitError -from qiskit.pulse import Schedule, InstructionScheduleMap from qiskit_aer import AerSimulator from qiskit_aer.noise import NoiseModel, depolarizing_error from qiskit_ibm_runtime.fake_provider import FakeManilaV2 @@ -156,38 +154,6 @@ def test_full_sampling_2_qubits(self): self.assertNotEqual(circs1[1].decompose(), circs2[1].decompose()) self.assertNotEqual(circs1[2].decompose(), circs2[2].decompose()) - # ### Tests for transpiled circuit generation ### - def test_calibrations_via_transpile_options(self): - """Test if calibrations given as transpile_options show up in transpiled circuits.""" - qubits = (2,) - my_sched = Schedule(name="custom_sx_gate") - my_inst_map = InstructionScheduleMap() - my_inst_map.add(SXGate(), qubits, my_sched) - - exp = rb.StandardRB( - physical_qubits=qubits, lengths=[3], num_samples=4, backend=self.backend, seed=123 - ) - exp.set_transpile_options(inst_map=my_inst_map) - transpiled = exp._transpiled_circuits() - for qc in transpiled: - self.assertTrue(qc.calibrations) - self.assertTrue(qc.has_calibration_for((SXGate(), [qc.qubits[q] for q in qubits], []))) - self.assertEqual(qc.calibrations["sx"][(qubits, tuple())], my_sched) - - def test_calibrations_via_custom_backend(self): - """Test if calibrations given as custom backend show up in transpiled circuits.""" - qubits = (2,) - my_sched = Schedule(name="custom_sx_gate") - my_backend = copy.deepcopy(self.backend) - my_backend.target["sx"][qubits].calibration = my_sched - - exp = rb.StandardRB(physical_qubits=qubits, lengths=[3], num_samples=4, backend=my_backend) - transpiled = exp._transpiled_circuits() - for qc in transpiled: - self.assertTrue(qc.calibrations) - self.assertTrue(qc.has_calibration_for((SXGate(), [qc.qubits[q] for q in qubits], []))) - self.assertEqual(qc.calibrations["sx"][(qubits, tuple())], my_sched) - def test_backend_with_directed_basis_gates(self): """Test if correct circuits are generated from backend with directed basis gates.""" my_backend = copy.deepcopy(self.backend)