From 46e7eec919de59f104676c6c290a7da3e3ddd325 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 31 Jan 2024 13:01:13 +0900 Subject: [PATCH] Rework of Stark module and workflow (#1236) ### Summary This PR moves all Stark experiments to new module `qiskit_experiments.library.driven_freq_tuning` to define a module-wise utility file. This util file contains the `StarkCoefficients` dataclass to combine all seven coefficients from third-order polynomial fit characterizing the Stark shift. This object is shared among all experiments and analyses in new module. In addition to this, `StarkP1Spectroscopy` allows users to scan xval in units of either amplitude or frequency (previously only amplitude was allowed). These two domains are mutually convertible with the `StarkCoefficients` object. The domain conversion functions are also included in the util file. ### Details and comments Experiment option names are updated to be more general, namely `amp` -> `xval` and new option `xval_type` is added. `xval_type` is either `amplitude` or `frequency`. Experimentalist can directly specify the target Stark shift by ```python exp = StarkP1Spectroscopy((0,), backend=backend) freqs = np.linspace(-70e6, 70e6, 31) exp.set_experiment_options( xvals=freqs, xval_type="frequency", ) ``` Note that this requires pre-calibration of Stark shift coefficients with `StarkRamseyXYAmpScan` experiment to convert specified frequencies into tone amplitudes, and one must save the calibration results in the experiment service. If the service is not available, one can also directly provide these coefficients instead of providing a service through the backend. ```python exp.set_experiment_options( xvals=test_freqs, xval_type="frequency", stark_coefficients=StarkCoefficients(...), ) ``` When the coefficients are already calibrated, one can estimate the maximum Stark shift available within the power budget. ```python min_freq, max_freq = util.find_min_max_frequency(-0.9, 0.9, coeffs) ``` --------- Co-authored-by: Will Shanks Co-authored-by: Will Shanks --- docs/apidocs/index.rst | 1 + docs/apidocs/mod_driven_freq_tuning.rst | 6 + .../characterization/stark_experiment.rst | 104 +++ .../stark_experiment_example.png | Bin 0 -> 1203075 bytes qiskit_experiments/__init__.py | 1 + qiskit_experiments/library/__init__.py | 13 +- .../library/characterization/__init__.py | 11 +- .../characterization/analysis/__init__.py | 4 +- .../analysis/ramsey_xy_analysis.py | 398 ----------- .../characterization/analysis/t1_analysis.py | 220 +----- .../library/characterization/ramsey_xy.py | 628 +----------------- .../library/characterization/t1.py | 228 +------ .../library/driven_freq_tuning/__init__.py | 63 ++ .../driven_freq_tuning/coefficients.py | 279 ++++++++ .../library/driven_freq_tuning/p1_spect.py | 269 ++++++++ .../driven_freq_tuning/p1_spect_analysis.py | 163 +++++ .../library/driven_freq_tuning/ramsey.py | 359 ++++++++++ .../driven_freq_tuning/ramsey_amp_scan.py | 311 +++++++++ .../ramsey_amp_scan_analysis.py | 440 ++++++++++++ test/library/driven_freq_tuning/__init__.py | 12 + .../library/driven_freq_tuning/test_coeffs.py | 173 +++++ .../test_stark_p1_spect.py | 224 ++++--- .../test_stark_ramsey_xy.py | 68 +- 23 files changed, 2368 insertions(+), 1607 deletions(-) create mode 100644 docs/apidocs/mod_driven_freq_tuning.rst create mode 100644 docs/manuals/characterization/stark_experiment_example.png create mode 100644 qiskit_experiments/library/driven_freq_tuning/__init__.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/coefficients.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/p1_spect.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/ramsey.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py create mode 100644 qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py create mode 100644 test/library/driven_freq_tuning/__init__.py create mode 100644 test/library/driven_freq_tuning/test_coeffs.py rename test/library/{characterization => driven_freq_tuning}/test_stark_p1_spect.py (55%) rename test/library/{characterization => driven_freq_tuning}/test_stark_ramsey_xy.py (84%) diff --git a/docs/apidocs/index.rst b/docs/apidocs/index.rst index 01efc9c174..fac3299b4d 100644 --- a/docs/apidocs/index.rst +++ b/docs/apidocs/index.rst @@ -30,6 +30,7 @@ Experiment Modules mod_calibration mod_characterization + mod_driven_freq_tuning mod_randomized_benchmarking mod_tomography mod_quantum_volume diff --git a/docs/apidocs/mod_driven_freq_tuning.rst b/docs/apidocs/mod_driven_freq_tuning.rst new file mode 100644 index 0000000000..bdc7fa1462 --- /dev/null +++ b/docs/apidocs/mod_driven_freq_tuning.rst @@ -0,0 +1,6 @@ +.. _qiskit-experiments-driven-freq-tuning: + +.. automodule:: qiskit_experiments.library.driven_freq_tuning + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/manuals/characterization/stark_experiment.rst b/docs/manuals/characterization/stark_experiment.rst index 691171cd67..23b6c56d4e 100644 --- a/docs/manuals/characterization/stark_experiment.rst +++ b/docs/manuals/characterization/stark_experiment.rst @@ -211,6 +211,110 @@ 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 ---------- diff --git a/docs/manuals/characterization/stark_experiment_example.png b/docs/manuals/characterization/stark_experiment_example.png new file mode 100644 index 0000000000000000000000000000000000000000..5d99b87fe587ac3708bf42d7ff14a7ba125366a0 GIT binary patch literal 1203075 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

-r(w6hglC5Ho0#iCq@;j4+ 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 literal 0 HcmV?d00001 diff --git a/qiskit_experiments/__init__.py b/qiskit_experiments/__init__.py index e9c613b259..32532928b8 100644 --- a/qiskit_experiments/__init__.py +++ b/qiskit_experiments/__init__.py @@ -49,6 +49,7 @@ - :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` """ diff --git a/qiskit_experiments/library/__init__.py b/qiskit_experiments/library/__init__.py index 6737402863..29770ed7b3 100644 --- a/qiskit_experiments/library/__init__.py +++ b/qiskit_experiments/library/__init__.py @@ -76,8 +76,9 @@ ~characterization.FineXDrag ~characterization.FineSXDrag ~characterization.MultiStateDiscrimination - ~characterization.StarkRamseyXY - ~characterization.StarkRamseyXYAmpScan + ~driven_freq_tuning.StarkRamseyXY + ~driven_freq_tuning.StarkRamseyXYAmpScan + ~driven_freq_tuning.StarkP1Spectroscopy .. _characterization two qubits: @@ -160,7 +161,6 @@ class instance to manage parameters and pulse schedules. ) from .characterization import ( T1, - StarkP1Spectroscopy, T2Hahn, T2Ramsey, Tphi, @@ -187,8 +187,6 @@ class instance to manage parameters and pulse schedules. CorrelatedReadoutError, ZZRamsey, MultiStateDiscrimination, - StarkRamseyXY, - StarkRamseyXYAmpScan, ) from .randomized_benchmarking import StandardRB, InterleavedRB from .tomography import ( @@ -199,6 +197,11 @@ 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 diff --git a/qiskit_experiments/library/characterization/__init__.py b/qiskit_experiments/library/characterization/__init__.py index 8aaaceee4c..daa29bb4a4 100644 --- a/qiskit_experiments/library/characterization/__init__.py +++ b/qiskit_experiments/library/characterization/__init__.py @@ -24,7 +24,6 @@ :template: autosummary/experiment.rst T1 - StarkP1Spectroscopy T2Ramsey T2Hahn Tphi @@ -50,8 +49,6 @@ ResonatorSpectroscopy MultiStateDiscrimination ZZRamsey - StarkRamseyXY - StarkRamseyXYAmpScan Analysis @@ -63,7 +60,6 @@ T1Analysis T1KerneledAnalysis - StarkP1SpectAnalysis T2RamseyAnalysis T2HahnAnalysis TphiAnalysis @@ -71,7 +67,6 @@ DragCalAnalysis FineAmplitudeAnalysis RamseyXYAnalysis - StarkRamseyXYAmpScanAnalysis ReadoutAngleAnalysis ResonatorSpectroscopyAnalysis LocalReadoutErrorAnalysis @@ -85,8 +80,6 @@ DragCalAnalysis, FineAmplitudeAnalysis, RamseyXYAnalysis, - StarkRamseyXYAmpScanAnalysis, - StarkP1SpectAnalysis, T2RamseyAnalysis, T1Analysis, T1KerneledAnalysis, @@ -101,7 +94,7 @@ MultiStateDiscriminationAnalysis, ) -from .t1 import T1, StarkP1Spectroscopy +from .t1 import T1 from .qubit_spectroscopy import QubitSpectroscopy from .ef_spectroscopy import EFSpectroscopy from .t2ramsey import T2Ramsey @@ -111,7 +104,7 @@ from .rabi import Rabi, EFRabi from .half_angle import HalfAngle from .fine_amplitude import FineAmplitude, FineXAmplitude, FineSXAmplitude, FineZXAmplitude -from .ramsey_xy import RamseyXY, StarkRamseyXY, StarkRamseyXYAmpScan +from .ramsey_xy import RamseyXY from .fine_frequency import FineFrequency from .drag import RoughDrag from .readout_angle import ReadoutAngle diff --git a/qiskit_experiments/library/characterization/analysis/__init__.py b/qiskit_experiments/library/characterization/analysis/__init__.py index 8520060772..f249dcd9be 100644 --- a/qiskit_experiments/library/characterization/analysis/__init__.py +++ b/qiskit_experiments/library/characterization/analysis/__init__.py @@ -14,10 +14,10 @@ from .drag_analysis import DragCalAnalysis from .fine_amplitude_analysis import FineAmplitudeAnalysis -from .ramsey_xy_analysis import RamseyXYAnalysis, StarkRamseyXYAmpScanAnalysis +from .ramsey_xy_analysis import RamseyXYAnalysis from .t2ramsey_analysis import T2RamseyAnalysis from .t2hahn_analysis import T2HahnAnalysis -from .t1_analysis import T1Analysis, T1KerneledAnalysis, StarkP1SpectAnalysis +from .t1_analysis import T1Analysis, T1KerneledAnalysis from .tphi_analysis import TphiAnalysis from .cr_hamiltonian_analysis import CrossResonanceHamiltonianAnalysis from .readout_angle_analysis import ReadoutAngleAnalysis diff --git a/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py b/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py index 1a0ac92837..27a588a550 100644 --- a/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/ramsey_xy_analysis.py @@ -16,11 +16,8 @@ import lmfit import numpy as np -from uncertainties import unumpy as unp import qiskit_experiments.curve_analysis as curve -import qiskit_experiments.visualization as vis -from qiskit_experiments.framework import ExperimentData class RamseyXYAnalysis(curve.CurveAnalysis): @@ -209,398 +206,3 @@ def _evaluate_quality(self, fit_data: curve.CurveFitResult) -> Union[str, None]: return "good" return "bad" - - -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` - - """ - - 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"}, - }, - result_parameters=[ - curve.ParameterRepr("c1_pos", "stark_pos_coef_o1", "Hz"), - curve.ParameterRepr("c2_pos", "stark_pos_coef_o2", "Hz"), - curve.ParameterRepr("c3_pos", "stark_pos_coef_o3", "Hz"), - curve.ParameterRepr("c1_neg", "stark_neg_coef_o1", "Hz"), - curve.ParameterRepr("c2_neg", "stark_neg_coef_o2", "Hz"), - curve.ParameterRepr("c3_neg", "stark_neg_coef_o3", "Hz"), - curve.ParameterRepr("f_err", "stark_ferr", "Hz"), - ], - 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[curve_data.category == "ramsey_xy"] - - # Create phase data by arctan(Y/X) - columns = list(curve_data.columns) - phase_data = np.empty((0, len(columns))) - y_mean = ramsey_xy.yval.mean() - - grouped = ramsey_xy.groupby("name") - for m_id, direction in enumerate(("pos", "neg")): - x_quadrature = grouped.get_group(f"X{direction}") - y_quadrature = grouped.get_group(f"Y{direction}") - if not np.array_equal(x_quadrature.xval, y_quadrature.xval): - raise ValueError( - "Amplitude values of X and Y quadrature are different. " - "Same values must be used." - ) - x_uarray = unp.uarray(x_quadrature.yval, x_quadrature.yerr) - y_uarray = unp.uarray(y_quadrature.yval, y_quadrature.yerr) - - amplitudes = x_quadrature.xval.to_numpy() - - # 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 - tmp = np.empty((len(amplitudes), len(columns)), dtype=object) - tmp[:, columns.index("xval")] = amplitudes - tmp[:, columns.index("yval")] = unwrapped_phase / self._freq_phase_coef() - tmp[:, columns.index("yerr")] = phase_s / self._freq_phase_coef() - tmp[:, columns.index("name")] = f"FREQ{direction}" - tmp[:, columns.index("class_id")] = m_id - tmp[:, columns.index("shots")] = x_quadrature.shots + y_quadrature.shots - tmp[:, columns.index("category")] = category - phase_data = np.r_[phase_data, tmp] - - return curve_data.append_list_values(other=phase_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_figures( - self, - curve_data: curve.ScatterTable, - ) -> List["matplotlib.figure.Figure"]: - - # plot unwrapped phase on first axis - for d in ("pos", "neg"): - sub_data = curve_data[(curve_data.name == f"FREQ{d}") & (curve_data.category == "freq")] - self.plotter.set_series_data( - series_name=f"F{d}", - x_formatted=sub_data.xval.to_numpy(), - y_formatted=sub_data.yval.to_numpy(), - y_formatted_err=sub_data.yerr.to_numpy(), - ) - - # plot raw RamseyXY plot on second axis - for name in ("Xpos", "Ypos", "Xneg", "Yneg"): - sub_data = curve_data[(curve_data.name == name) & (curve_data.category == "ramsey_xy")] - self.plotter.set_series_data( - series_name=name, - x_formatted=sub_data.xval.to_numpy(), - y_formatted=sub_data.yval.to_numpy(), - y_formatted_err=sub_data.yerr.to_numpy(), - ) - - # find base and amplitude guess - ramsey_xy = curve_data[curve_data.category == "ramsey_xy"] - offset_guess = 0.5 * (ramsey_xy.yval.min() + ramsey_xy.yval.max()) - amp_guess = 0.5 * np.ptp(ramsey_xy.yval) - - # plot frequency and Ramsey fit lines - line_data = curve_data[curve_data.category == "fitted"] - for direction in ("pos", "neg"): - sub_data = line_data[line_data.name == f"FREQ{direction}"] - if len(sub_data) == 0: - continue - xval = sub_data.xval.to_numpy() - yn = sub_data.yval.to_numpy() - ys = sub_data.yerr.to_numpy() - 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/characterization/analysis/t1_analysis.py b/qiskit_experiments/library/characterization/analysis/t1_analysis.py index 4bbbabb542..9ef0ed3bc3 100644 --- a/qiskit_experiments/library/characterization/analysis/t1_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t1_analysis.py @@ -12,19 +12,12 @@ """ T1 Analysis class. """ -from typing import Union, Tuple, List, Dict +from typing import Union import numpy as np -from qiskit_ibm_experiment import IBMExperimentService -from qiskit_ibm_experiment.exceptions import IBMApiError -from uncertainties import unumpy as unp import qiskit_experiments.curve_analysis as curve -import qiskit_experiments.data_processing as dp -import qiskit_experiments.visualization as vis -from qiskit_experiments.data_processing.exceptions import DataProcessorError -from qiskit_experiments.database_service.device_component import Qubit -from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData, Options +from qiskit_experiments.framework import Options class T1Analysis(curve.DecayAnalysis): @@ -139,212 +132,3 @@ def _format_data( if avg_slope > 0: curve_data.yval = 1 - curve_data.yval return super()._format_data(curve_data) - - -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. - - 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.characterization.ramsey_xy.StarkRamseyXYAmpScan` - - """ - - stark_coefficients_names = [ - "stark_pos_coef_o1", - "stark_pos_coef_o2", - "stark_pos_coef_o3", - "stark_neg_coef_o1", - "stark_neg_coef_o2", - "stark_neg_coef_o3", - "stark_ferr", - ] - - @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="latest", - x_key="xval", - ) - 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 [] - - @classmethod - def retrieve_coefficients_from_service( - cls, - service: IBMExperimentService, - qubit: int, - backend: str, - ) -> Dict: - """Retrieve stark coefficient dictionary from the experiment service. - - Args: - service: A valid experiment service instance. - qubit: Qubit index. - backend: Name of the backend. - - Returns: - A dictionary of Stark coefficients to convert amplitude to frequency. - None value is returned when the dictionary is incomplete. - """ - out = {} - try: - for name in cls.stark_coefficients_names: - results = service.analysis_results( - device_components=[str(Qubit(qubit))], - result_type=name, - backend_name=backend, - sort_by=["creation_datetime:desc"], - ) - if len(results) == 0: - return None - result_data = getattr(results[0], "result_data") - out[name] = result_data["value"] - except (IBMApiError, ValueError, KeyError, AttributeError): - return None - return out - - def _convert_axis( - self, - xdata: np.ndarray, - coefficients: Dict[str, float], - ) -> np.ndarray: - """A helper method to convert x-axis. - - Args: - xdata: An array of Stark tone amplitude. - coefficients: Stark coefficients to convert amplitudes into frequencies. - - Returns: - An array of amount of Stark shift. - """ - names = self.stark_coefficients_names # alias - positive = np.poly1d([coefficients[names[idx]] for idx in [2, 1, 0, 6]]) - negative = np.poly1d([coefficients[names[idx]] for idx in [5, 4, 3, 6]]) - - new_xdata = np.where(xdata > 0, positive(xdata), negative(xdata)) - self.plotter.set_figure_options( - xlabel="Stark shift", - xval_unit="Hz", - xscale="linear", - ) - return new_xdata - - 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 == "latest" and experiment_data.service is not None: - # Get value from service - stark_coeffs = self.retrieve_coefficients_from_service( - service=experiment_data.service, - qubit=experiment_data.metadata["physical_qubits"][0], - backend=experiment_data.backend_name, - ) - elif isinstance(self.options.stark_coefficients, dict): - # Get value from experiment options - missing = set(self.stark_coefficients_names) - self.options.stark_coefficients.keys() - if any(missing): - raise KeyError( - "Following coefficient data is missing in the " - f"'stark_coefficients' dictionary: {missing}." - ) - stark_coeffs = self.options.stark_coefficients - else: - # No calibration is available - stark_coeffs = None - - # 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 stark_coeffs: - xdata = self._convert_axis(xdata, stark_coeffs) - - # 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/characterization/ramsey_xy.py b/qiskit_experiments/library/characterization/ramsey_xy.py index 1d0238b652..ccb1987481 100644 --- a/qiskit_experiments/library/characterization/ramsey_xy.py +++ b/qiskit_experiments/library/characterization/ramsey_xy.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2023. +# (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 @@ -11,27 +11,16 @@ # that they have been altered from the originals. """Ramsey XY frequency characterization experiment.""" -import warnings -from typing import List, Tuple, Dict, Optional, Sequence +from typing import List, Optional, Sequence import numpy as np -from qiskit import pulse -from qiskit.circuit import QuantumCircuit, Gate, ParameterExpression, Parameter +from qiskit.circuit import QuantumCircuit, Parameter from qiskit.providers.backend import Backend from qiskit.qobj.utils import MeasLevel -from qiskit.utils import optionals as _optional from qiskit_experiments.framework import BaseExperiment, Options, BackendTiming from qiskit_experiments.framework.restless_mixin import RestlessMixin -from qiskit_experiments.library.characterization.analysis import ( - RamseyXYAnalysis, - StarkRamseyXYAmpScanAnalysis, -) - -if _optional.HAS_SYMENGINE: - import symengine as sym -else: - import sympy as sym +from qiskit_experiments.library.characterization.analysis import RamseyXYAnalysis class RamseyXY(BaseExperiment, RestlessMixin): @@ -208,612 +197,3 @@ def _metadata(self): if hasattr(self.run_options, run_opt): metadata[run_opt] = getattr(self.run_options, run_opt) return metadata - - -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 - :py:class:`RamseyXYAnalysis` - - # section: see_also - :class:`qiskit_experiments.library.characterization.ramsey_xy.RamseyXY` - - # section: manual - :doc:`/manuals/characterization/stark_experiment` - - """ - - def __init__( - self, - physical_qubits: Sequence[int], - backend: Optional[Backend] = 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 - - -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 - :py:class:`StarkRamseyXYAmpScanAnalysis` - - # section: see_also - :class:`qiskit_experiments.library.characterization.ramsey_xy.StarkRamseyXY` - :class:`qiskit_experiments.library.characterization.ramsey_xy.RamseyXY` - - # section: manual - :doc:`/manuals/characterization/stark_experiment` - - """ - - def __init__( - self, - physical_qubits: Sequence[int], - backend: Optional[Backend] = 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 - - super().__init__( - physical_qubits=physical_qubits, - analysis=StarkRamseyXYAmpScanAnalysis(), - 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/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 751f9a569b..0554599a25 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -13,24 +13,14 @@ T1 Experiment class. """ -from typing import List, Tuple, Dict, Optional, Union, Sequence +from typing import List, Optional, Union, Sequence import numpy as np -from qiskit import pulse -from qiskit.circuit import QuantumCircuit, Gate, Parameter, ParameterExpression +from qiskit.circuit import QuantumCircuit from qiskit.providers.backend import Backend -from qiskit.utils import optionals as _optional from qiskit_experiments.framework import BackendTiming, BaseExperiment, Options -from qiskit_experiments.library.characterization.analysis.t1_analysis import ( - T1Analysis, - StarkP1SpectAnalysis, -) - -if _optional.HAS_SYMENGINE: - import symengine as sym -else: - import sympy as sym +from qiskit_experiments.library.characterization.analysis.t1_analysis import T1Analysis class T1(BaseExperiment): @@ -119,215 +109,3 @@ def _metadata(self): if hasattr(self.run_options, run_opt): metadata[run_opt] = getattr(self.run_options, run_opt) return metadata - - -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 - :py:class:`.StarkP1SpectAnalysis` - - # section: reference - .. ref_arxiv:: 1 2105.15201 - - # section: see_also - :class:`qiskit_experiments.library.characterization.ramsey_xy.StarkRamseyXY` - :class:`qiskit_experiments.library.characterization.ramsey_xy.StarkRamseyXYAmpScan` - - # section: manual - :doc:`/manuals/characterization/stark_experiment` - - """ - - def __init__( - self, - physical_qubits: Sequence[int], - backend: Optional[Backend] = None, - **experiment_options, - ): - """ - Initialize the T1 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 - - super().__init__( - physical_qubits=physical_qubits, - analysis=StarkP1SpectAnalysis(), - 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_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. - 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. - stark_amps (list[float]): The list of amplitude that will be scanned in the experiment. - If not set, then ``num_stark_amps`` amplitudes spaced according to - the ``spacing`` policy 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( - t1_delay=20e-6, - stark_channel=None, - stark_freq_offset=80e6, - stark_sigma=15e-9, - stark_risefall=2, - min_stark_amp=-1, - max_stark_amp=1, - num_stark_amps=201, - spacing="quadratic", - stark_amps=None, - ) - options.set_validator("spacing", ["linear", "quadratic"]) - 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: - if opt.spacing == "linear": - params = np.linspace(opt.min_stark_amp, opt.max_stark_amp, opt.num_stark_amps) - elif opt.spacing == "quadratic": - min_sqrt = np.sign(opt.min_stark_amp) * np.sqrt(np.abs(opt.min_stark_amp)) - max_sqrt = np.sign(opt.max_stark_amp) * np.sqrt(np.abs(opt.max_stark_amp)) - lin_params = np.linspace(min_sqrt, max_sqrt, opt.num_stark_amps) - params = np.sign(lin_params) * lin_params**2 - else: - raise ValueError(f"Spacing option {opt.spacing} is not valid.") - else: - params = np.asarray(opt.stark_amps, dtype=float) - - 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/__init__.py b/qiskit_experiments/library/driven_freq_tuning/__init__.py new file mode 100644 index 0000000000..0bef002700 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/__init__.py @@ -0,0 +1,63 @@ +# 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 new file mode 100644 index 0000000000..89dd840ed2 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/coefficients.py @@ -0,0 +1,279 @@ +# 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, + ] + ) + + 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 new file mode 100644 index 0000000000..5ee1bbc18e --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/p1_spect.py @@ -0,0 +1,269 @@ +# 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 + +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_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` + + """ + + 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 + + super().__init__( + physical_qubits=physical_qubits, + analysis=StarkP1SpectAnalysis(), + 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 new file mode 100644 index 0000000000..857f440bb8 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/p1_spect_analysis.py @@ -0,0 +1,163 @@ +# 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 + +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` + + """ + + @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 new file mode 100644 index 0000000000..e214a5c4b4 --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/ramsey.py @@ -0,0 +1,359 @@ +# 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_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` + + """ + + 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 new file mode 100644 index 0000000000..528c3fd8bd --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan.py @@ -0,0 +1,311 @@ +# 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 + +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_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` + + """ + + 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 + + super().__init__( + physical_qubits=physical_qubits, + analysis=StarkRamseyXYAmpScanAnalysis(), + 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 new file mode 100644 index 0000000000..9ced48b07a --- /dev/null +++ b/qiskit_experiments/library/driven_freq_tuning/ramsey_amp_scan_analysis.py @@ -0,0 +1,440 @@ +# 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 + +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` + + """ + + 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[curve_data.category == "ramsey_xy"] + + # Create phase data by arctan(Y/X) + columns = list(curve_data.columns) + phase_data = np.empty((0, len(columns))) + y_mean = ramsey_xy.yval.mean() + + grouped = ramsey_xy.groupby("name") + for m_id, direction in enumerate(("pos", "neg")): + x_quadrature = grouped.get_group(f"X{direction}") + y_quadrature = grouped.get_group(f"Y{direction}") + if not np.array_equal(x_quadrature.xval, y_quadrature.xval): + raise ValueError( + "Amplitude values of X and Y quadrature are different. " + "Same values must be used." + ) + x_uarray = unp.uarray(x_quadrature.yval, x_quadrature.yerr) + y_uarray = unp.uarray(y_quadrature.yval, y_quadrature.yerr) + + amplitudes = x_quadrature.xval.to_numpy() + + # 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 + tmp = np.empty((len(amplitudes), len(columns)), dtype=object) + tmp[:, columns.index("xval")] = amplitudes + tmp[:, columns.index("yval")] = unwrapped_phase / self._freq_phase_coef() + tmp[:, columns.index("yerr")] = phase_s / self._freq_phase_coef() + tmp[:, columns.index("name")] = f"FREQ{direction}" + tmp[:, columns.index("class_id")] = m_id + tmp[:, columns.index("shots")] = x_quadrature.shots + y_quadrature.shots + tmp[:, columns.index("category")] = category + phase_data = np.r_[phase_data, tmp] + + return curve_data.append_list_values(other=phase_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 d in ("pos", "neg"): + sub_data = curve_data[(curve_data.name == f"FREQ{d}") & (curve_data.category == "freq")] + self.plotter.set_series_data( + series_name=f"F{d}", + x_formatted=sub_data.xval.to_numpy(), + y_formatted=sub_data.yval.to_numpy(), + y_formatted_err=sub_data.yerr.to_numpy(), + ) + + # plot raw RamseyXY plot on second axis + for name in ("Xpos", "Ypos", "Xneg", "Yneg"): + sub_data = curve_data[(curve_data.name == name) & (curve_data.category == "ramsey_xy")] + self.plotter.set_series_data( + series_name=name, + x_formatted=sub_data.xval.to_numpy(), + y_formatted=sub_data.yval.to_numpy(), + y_formatted_err=sub_data.yerr.to_numpy(), + ) + + # find base and amplitude guess + ramsey_xy = curve_data[curve_data.category == "ramsey_xy"] + offset_guess = 0.5 * (ramsey_xy.yval.min() + ramsey_xy.yval.max()) + amp_guess = 0.5 * np.ptp(ramsey_xy.yval) + + # plot frequency and Ramsey fit lines + line_data = curve_data[curve_data.category == "fitted"] + for direction in ("pos", "neg"): + sub_data = line_data[line_data.name == f"FREQ{direction}"] + if len(sub_data) == 0: + continue + xval = sub_data.xval.to_numpy() + yn = sub_data.yval.to_numpy() + ys = sub_data.yerr.to_numpy() + 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/test/library/driven_freq_tuning/__init__.py b/test/library/driven_freq_tuning/__init__.py new file mode 100644 index 0000000000..4575d01965 --- /dev/null +++ b/test/library/driven_freq_tuning/__init__.py @@ -0,0 +1,12 @@ +# 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 new file mode 100644 index 0000000000..ce1cd9ee85 --- /dev/null +++ b/test/library/driven_freq_tuning/test_coeffs.py @@ -0,0 +1,173 @@ +# 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/characterization/test_stark_p1_spect.py b/test/library/driven_freq_tuning/test_stark_p1_spect.py similarity index 55% rename from test/library/characterization/test_stark_p1_spect.py rename to test/library/driven_freq_tuning/test_stark_p1_spect.py index e3a4f9e2cc..00ddbd5c6f 100644 --- a/test/library/characterization/test_stark_p1_spect.py +++ b/test/library/driven_freq_tuning/test_stark_p1_spect.py @@ -14,6 +14,7 @@ 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 @@ -22,7 +23,8 @@ from qiskit_experiments.framework import ExperimentData, AnalysisResultData from qiskit_experiments.library import StarkP1Spectroscopy -from qiskit_experiments.library.characterization.analysis import StarkP1SpectAnalysis +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 @@ -43,48 +45,17 @@ def _run_spect_analysis( ] +@ddt class TestStarkP1Spectroscopy(QiskitExperimentsTestCase): """Test case for the Stark P1 Spectroscopy experiment.""" - def setUp(self): - super().setUp() - - self.service = FakeService() - - self.service.create_experiment( - experiment_type="StarkRamseyXYAmpScan", - backend_name="fake_hanoi", - experiment_id="123456789", - ) - - self.coeffs = { - "stark_pos_coef_o1": 5e6, - "stark_pos_coef_o2": 200e6, - "stark_pos_coef_o3": -50e6, - "stark_neg_coef_o1": 5e6, - "stark_neg_coef_o2": -180e6, - "stark_neg_coef_o3": -40e6, - "stark_ferr": 100e3, - } - for i, (key, value) in enumerate(self.coeffs.items()): - self.service.create_analysis_result( - experiment_id="123456789", - result_data={"value": value}, - result_type=key, - device_components=["Q0"], - tags=[], - quality="Good", - verified=False, - result_id=str(i), - ) - def test_linear_spaced_parameters(self): """Test generating parameters with linear spacing.""" exp = StarkP1Spectroscopy((0,)) exp.set_experiment_options( - min_stark_amp=-1, - max_stark_amp=1, - num_stark_amps=5, + min_xval=-1, + max_xval=1, + num_xvals=5, spacing="linear", ) params = exp.parameters() @@ -96,9 +67,9 @@ def test_quadratic_spaced_parameters(self): """Test generating parameters with quadratic spacing.""" exp = StarkP1Spectroscopy((0,)) exp.set_experiment_options( - min_stark_amp=-1, - max_stark_amp=1, - num_stark_amps=5, + min_xval=-1, + max_xval=1, + num_xvals=5, spacing="quadratic", ) params = exp.parameters() @@ -112,6 +83,68 @@ def test_invalid_spacing(self): 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() @@ -125,7 +158,7 @@ def test_circuits(self): exp = StarkP1Spectroscopy((0,), backend) exp.set_experiment_options( - stark_amps=[-0.5, 0.5], + xvals=[-0.5, 0.5], stark_freq_offset=10e6, t1_delay=100, stark_sigma=15, @@ -166,18 +199,6 @@ def test_circuits(self): self.assertEqual(circs[0], qc1) self.assertEqual(circs[1], qc2) - def test_retrieve_coefficients(self): - """Test retrieving Stark coefficients from the experiment service.""" - retrieved_coeffs = StarkP1SpectAnalysis.retrieve_coefficients_from_service( - service=self.service, - qubit=0, - backend="fake_hanoi", - ) - self.assertDictEqual( - retrieved_coeffs, - self.coeffs, - ) - def test_running_analysis_without_service(self): """Test running analysis without setting service to the experiment data. @@ -186,71 +207,98 @@ def test_running_analysis_without_service(self): 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 - ref_xvals = xvals np.testing.assert_array_almost_equal(test_xvals, ref_xvals) - def test_running_analysis_with_service(self): + @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=self.service, + 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) - test_xvals = exp_data.analysis_results("xvals").value - ref_xvals = np.where( - xvals > 0, - ( - self.coeffs["stark_pos_coef_o1"] * xvals - + self.coeffs["stark_pos_coef_o2"] * xvals**2 - + self.coeffs["stark_pos_coef_o3"] * xvals**3 - + self.coeffs["stark_ferr"] - ), - ( - self.coeffs["stark_neg_coef_o1"] * xvals - + self.coeffs["stark_neg_coef_o2"] * xvals**2 - + self.coeffs["stark_neg_coef_o3"] * xvals**3 - + self.coeffs["stark_ferr"] - ), - ) - np.testing.assert_array_almost_equal(test_xvals, ref_xvals) + 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. """ - analysis = StarkP1SpectAnalysisReturnXvals() - analysis.set_options( - stark_coefficients={ - "stark_pos_coef_o1": 0.0, - "stark_pos_coef_o2": 200e6, - "stark_pos_coef_o3": 0.0, - "stark_neg_coef_o1": 0.0, - "stark_neg_coef_o2": -200e6, - "stark_neg_coef_o3": 0.0, - "stark_ferr": 0.0, - } + 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) - test_xvals = exp_data.analysis_results("xvals").value - ref_xvals = np.where(xvals > 0, 200e6 * xvals**2, -200e6 * xvals**2) - np.testing.assert_array_almost_equal(test_xvals, ref_xvals) + 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/characterization/test_stark_ramsey_xy.py b/test/library/driven_freq_tuning/test_stark_ramsey_xy.py similarity index 84% rename from test/library/characterization/test_stark_ramsey_xy.py rename to test/library/driven_freq_tuning/test_stark_ramsey_xy.py index 795fde5297..dd0fd59e4a 100644 --- a/test/library/characterization/test_stark_ramsey_xy.py +++ b/test/library/driven_freq_tuning/test_stark_ramsey_xy.py @@ -21,7 +21,10 @@ from qiskit.providers.fake_provider import FakeHanoiV2 from qiskit_experiments.library import StarkRamseyXY, StarkRamseyXYAmpScan -from qiskit_experiments.library.characterization.analysis import StarkRamseyXYAmpScanAnalysis +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 @@ -242,24 +245,33 @@ def test_ramsey_fast_analysis(self, c1p, c2p, c3p, c1n, c2n, c3n, ferr): 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 in xvals: + for x, y in zip(xvals, yvals): if x >= 0.0: - fs = c1p * x + c2p * x**2 + c3p * x**3 + ferr direction = "pos" else: - fs = c1n * x + c2n * x**2 + c3n * x**3 + ferr direction = "neg" # Add some sampling error - ramx_count = rng.binomial(shots, amp * np.cos(const * fs) + off) + 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 * fs) + off) + ramy_count = rng.binomial(shots, amp * np.sin(const * y) + off) exp_data.add_data( { "counts": {"0": shots - ramy_count, "1": ramy_count}, @@ -271,38 +283,18 @@ def test_ramsey_fast_analysis(self, c1p, c2p, c3p, c1n, c2n, c3n, ferr): analysis.run(exp_data, replace_results=True) self.assertExperimentDone(exp_data) - # Check the fitted parameter can approximate the same polynominal - x_pos = np.linspace(0, 1, 51) - x_neg = np.linspace(-1, 0, 51) - ref_yvals_pos = c1p * x_pos + c2p * x_pos**2 + c3p * x_pos**3 + ferr - ref_yvals_neg = c1n * x_neg + c2n * x_neg**2 + c3n * x_neg**3 + ferr - - # Note that these parameter values are not necessary the same with input values - # as long as they can approximate the original phase polynominal. - c1p_est = exp_data.analysis_results("stark_pos_coef_o1").value.n - c2p_est = exp_data.analysis_results("stark_pos_coef_o2").value.n - c3p_est = exp_data.analysis_results("stark_pos_coef_o3").value.n - c1n_est = exp_data.analysis_results("stark_neg_coef_o1").value.n - c2n_est = exp_data.analysis_results("stark_neg_coef_o2").value.n - c3n_est = exp_data.analysis_results("stark_neg_coef_o3").value.n - ferr_est = exp_data.analysis_results("stark_ferr").value.n - - test_yvals_pos = c1p_est * x_pos + c2p_est * x_pos**2 + c3p_est * x_pos**3 + ferr_est - test_yvals_neg = c1n_est * x_neg + c2n_est * x_neg**2 + c3n_est * x_neg**3 + ferr_est - - # Check similality of reconstructed polynominals - # Curves must be agree within the torelance of 1.5 * 1 MHz. - np.testing.assert_array_almost_equal( - test_yvals_pos, - ref_yvals_pos, - decimal=-6, - err_msg="Reconstructed phase polynominal on positive frequency shift side " - "doesn't match with the original curve.", - ) + # 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( - test_yvals_neg, - ref_yvals_neg, + yvals, + fit_yvals, decimal=-6, - err_msg="Reconstructed phase polynominal on negative frequency shift side " - "doesn't match with the original curve.", + err_msg="Reconstructed phase polynominal doesn't match with the actual phase shift.", )