diff --git a/qiskit_experiments/library/__init__.py b/qiskit_experiments/library/__init__.py index 1a00a8d3f5..62e4ee8347 100644 --- a/qiskit_experiments/library/__init__.py +++ b/qiskit_experiments/library/__init__.py @@ -95,6 +95,7 @@ :template: autosummary/experiment.rst ~stark.StarkRamseyXY + ~stark.StarkRamseyFast .. _calibration: @@ -172,7 +173,7 @@ class instance to manage parameters and pulse schedules. from .randomized_benchmarking import StandardRB, InterleavedRB from .tomography import StateTomography, ProcessTomography from .quantum_volume import QuantumVolume -from .stark import StarkRamseyXY +from .stark import StarkRamseyXY, StarkRamseyFast # Experiment Sub-modules from . import calibration diff --git a/qiskit_experiments/library/stark/__init__.py b/qiskit_experiments/library/stark/__init__.py index ba81e65390..2f3408410d 100644 --- a/qiskit_experiments/library/stark/__init__.py +++ b/qiskit_experiments/library/stark/__init__.py @@ -24,7 +24,18 @@ :template: autosummary/experiment.rst StarkRamseyXY + StarkRamseyFast + +Analysis +======== +.. autosummary:: + :toctree: ../stubs/ + :template: autosummary/analysis.rst + + StarkRamseyFastAnalysis """ from .ramsey_xy import StarkRamseyXY +from .ramsey_fast import StarkRamseyFast +from .ramsey_fast_analysis import StarkRamseyFastAnalysis diff --git a/qiskit_experiments/library/stark/ramsey_fast.py b/qiskit_experiments/library/stark/ramsey_fast.py new file mode 100644 index 0000000000..c787f476fd --- /dev/null +++ b/qiskit_experiments/library/stark/ramsey_fast.py @@ -0,0 +1,287 @@ +# 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 fast experiment class with pulsed Stark tone drive. +""" + +from typing import List, Tuple, Dict, Optional + +import numpy as np +from qiskit import pulse, circuit +from qiskit.providers.backend import Backend +from qiskit.utils import optionals as _optional +from qiskit_experiments.framework import BaseExperiment, Options, BackendTiming + +from .ramsey_fast_analysis import StarkRamseyFastAnalysis + +if _optional.HAS_SYMENGINE: + import symengine as sym +else: + import sympy as sym + + +class StarkRamseyFast(BaseExperiment): + """Experiment to calibrate Stark shift as a function of amplitude. + + # section: overview + + This experiment is identical to :class:`StarkRamseyXY` but scans + Stark tone amplitude at the fixed flat-top duration. + This experiment consists of following two circuits: + + .. 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 + + This experiment is usually conducted beyond other spectroscopy experiments. + Because Stark drive cannot directly control the resulting Stark shift, + an experimentalist must characterize the amount of the shift + as a function of the drive amplitude to + modulate qubit frequency to the target value. + + Because frequency offset must be zero at zero Stark tone amplitude + up to frequency miscalibration, one can directly scan tone amplitude from zero to + certain amplitude instead of conducting :class:`StarkRamseyXY` experiment at each amplitude. + This drastically saves time of experiment to characterize Stark shift. + Note that usually Stark shift is asymmetric with respect to the frequency offset, + because of the anti-crossing occurring at around higher energy transitions, + and thus the Stark amplitude parameter must be scanned in both direction. + + This experiment gives several coefficients to convert the tone amplitude into + the frequency shift, which may be supplied to following spectroscopy experiments. + + # section: analysis_ref + :py:class:`StarkRamseyFastAnalysis` + + # section: see_also + qiskit_experiments.library.stark.ramsey_xy.StarkRamseyXY + qiskit_experiments.library.characterization.ramsey_xy.RamseyXY + + """ + + def __init__( + self, + qubit: int, + backend: Optional[Backend] = None, + **experiment_options, + ): + """Create new experiment. + + Args: + qubit: Index of qubit. + backend: Optional, the backend to run the experiment on. + experiment_options: Extra experiment options. See self.experiment_options. + """ + self._timing = None + + super().__init__(qubits=[qubit], analysis=StarkRamseyFastAnalysis(), 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 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_delay (float): The time to accumulate Stark shift phase in seconds. + stark_risefall (float): Ratio of sigma to the duration of + the rising and falling edges of the Stark tone. + min_stark_amp: Minimum Stark tone amplitude. + max_stark_amp: Maximum Stark tone amplitude. + num_stark_amps (int): Number of circuits per Ramsey X and Y with different amplitude. + stark_amps (list[float]): The list of amplitudes 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_delay=50e-9, + stark_risefall=2, + min_stark_amp=-0.8, + max_stark_amp=0.8, + 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 amplitudes(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: + return np.linspace(opt.min_stark_amp, opt.max_stark_amp, opt.num_stark_amps) + return opt.stark_amps + + def parameterized_circuits(self) -> Tuple[circuit.QuantumCircuit, circuit.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 = circuit.Parameter("stark_amp") + sym_param = param._symbol_expr + + # Pulse gates + stark_v = circuit.Gate("StarkV", 1, [param]) + stark_u = circuit.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 = circuit.ParameterExpression( + symbol_map={param: sym_param}, + expr=-sym.sign(sym_param), + ) + abs_of_amp = circuit.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 + delay_dt = self._timing.round_pulse(time=opt.stark_delay) + + 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 + delay_dt, + amp=abs_of_amp, + sigma=sigma_dt, + risefall_sigma_ratio=opt.stark_risefall, + ), + stark_channel, + ) + + ram_x = circuit.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 = circuit.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[circuit.QuantumCircuit]: + """Create circuits. + + Returns: + A list of circuits with a variable delay. + """ + ramx_circ, ramy_circ = self.parameterized_circuits() + param = next(iter(ramx_circ.parameters)) + + circs = [] + for amp in self.amplitudes(): + if np.isclose(amp, 0.0): + # To avoid singular point in the fit guess function. + continue + + 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.""" + return { + "stark_delay": self._timing.pulse_time(time=self.experiment_options.stark_delay), + "stark_freq_offset": self.experiment_options.stark_freq_offset, + } diff --git a/qiskit_experiments/library/stark/ramsey_fast_analysis.py b/qiskit_experiments/library/stark/ramsey_fast_analysis.py new file mode 100644 index 0000000000..d9c968e518 --- /dev/null +++ b/qiskit_experiments/library/stark/ramsey_fast_analysis.py @@ -0,0 +1,165 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Stark Ramsey fast experiment analysis. +""" + +from typing import List, Union + +import lmfit +import numpy as np + +import qiskit_experiments.curve_analysis as curve +import qiskit_experiments.visualization as vis +from qiskit_experiments.framework import ExperimentData + + +class StarkRamseyFastAnalysis(curve.CurveAnalysis): + """StarkRamseyAnalysis + + TODO write docstring. + """ + + def __init__(self): + + super().__init__( + # Ramsey phase := 2π Δf(x) Δt; Δf(x) = c2 x^2 + c3 x^3 + f_err + # dt := 2π Δt (const.) + models=[ + lmfit.models.ExpressionModel( + expr="amp * cos(dt * (c2_pos * x**2 + c3_pos * x**3 + f_err)) + offset", + name="Xpos", + data_sort_key={"series": "X", "direction": "pos"}, + ), + lmfit.models.ExpressionModel( + expr="amp * sin(dt * (c2_pos * x**2 + c3_pos * x**3 + f_err)) + offset", + name="Ypos", + data_sort_key={"series": "Y", "direction": "pos"}, + ), + lmfit.models.ExpressionModel( + expr="amp * cos(dt * (c2_neg * x**2 + c3_neg * x**3 + f_err)) + offset", + name="Xneg", + data_sort_key={"series": "X", "direction": "neg"}, + ), + lmfit.models.ExpressionModel( + expr="amp * sin(dt * (c2_neg * x**2 + c3_neg * x**3 + f_err)) + offset", + name="Yneg", + data_sort_key={"series": "Y", "direction": "neg"}, + ), + ], + ) + + @classmethod + def _default_options(cls): + """Default analysis options.""" + ramsey_plotter = vis.CurvePlotter(vis.MplDrawer()) + ramsey_plotter.set_figure_options( + xlabel="Stark tone amplitude", + ylabel="Ramsey P(1)", + ylim=(0, 1), + series_params={ + "Xpos": {"color": "blue", "symbol": "o", "label": "Ramsey X(+)"}, + "Ypos": {"color": "blue", "symbol": "^", "label": "Ramsey Y(+)"}, + "Xneg": {"color": "red", "symbol": "o", "label": "Ramsey X(-)"}, + "Yneg": {"color": "red", "symbol": "^", "label": "Ramsey Y(-)"}, + }, + ) + ramsey_plotter.set_options( + style=vis.PlotStyle({"figsize": (12, 5)}) + ) + + options = super()._default_options() + options.update_options( + result_parameters=[ + curve.ParameterRepr("c2_pos", "stark_pos_coef_o2", "Hz"), + curve.ParameterRepr("c3_pos", "stark_pos_coef_o3", "Hz"), + curve.ParameterRepr("c2_neg", "stark_neg_coef_o2", "Hz"), + curve.ParameterRepr("c3_neg", "stark_neg_coef_o3", "Hz"), + curve.ParameterRepr("f_err", "stark_offset", "Hz"), + ], + plotter=ramsey_plotter, + ) + + return options + + def _generate_fit_guesses( + self, + user_opt: curve.FitOptions, + curve_data: curve.CurveData, + ) -> 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. + """ + # Compute offset guess + user_opt.p0.set_if_empty( + offset=np.mean(curve_data.y), + f_err=0.0, + ) + est_offs = user_opt.p0["offset"] + + # Compute amplitude guess + amps = np.zeros(0) + for direction in ("pos", "neg"): + ram_x_off = curve_data.get_subset_of(f"X{direction}").y - est_offs + ram_y_off = curve_data.get_subset_of(f"Y{direction}").y - est_offs + amps = np.concatenate([amps, np.sqrt(ram_x_off ** 2 + ram_y_off ** 2)]) + user_opt.p0.set_if_empty(amp=np.median(amps)) + est_a = user_opt.p0["amp"] + d_const = user_opt.p0["dt"] + + # Compute polynominal coefficients + for direction in ("pos", "neg"): + ram_x_data = curve_data.get_subset_of(f"X{direction}") + ram_y_data = curve_data.get_subset_of(f"Y{direction}") + xvals = ram_x_data.x + + # Get normalized sinusoidals + xnorm = (ram_x_data.y - est_offs) / est_a + ynorm = (ram_y_data.y - est_offs) / est_a + + # Compute derivative to extract polynominals from sinusoidal + dx = np.diff(xnorm) / np.diff(xvals) + dy = np.diff(ynorm) / np.diff(xvals) + + # Eliminate sinusoidal + phase_poly = np.sqrt(dx**2 + dy**2) + + # Do polyfit up to 2rd order. + # This must correspond to the 3rd order coeff because of the derivative. + # The intercept is ignored. + vmat_xpoly = np.vstack((xvals[1:] ** 2, xvals[1:])).T + coeffs = np.linalg.lstsq(vmat_xpoly, phase_poly, rcond=-1)[0] + + poly_guess = { + f"c2_{direction}": coeffs[1] / 2 / d_const, + f"c3_{direction}": coeffs[0] / 3 / d_const, + } + user_opt.p0.set_if_empty(**poly_guess) + + return user_opt + + def _initialize( + self, + experiment_data: ExperimentData, + ): + super()._initialize(experiment_data) + + # Set scaling factor to convert phase to frequency + scale = 2 * np.pi * experiment_data.metadata["stark_delay"] + self.set_options(fixed_parameters={"dt": scale})