From 29aeaa48b7206dcfea42ce05a5c2945339c954fb Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sun, 23 Jul 2023 14:48:22 +0900 Subject: [PATCH] Stark P1 spectroscopy experiment (#1010) ### Summary This PR adds `StarkP1Spectroscopy` experiment and a dedicated analysis. User can set amplitudes to scan to run this experiment, and the analysis class can convert the amplitudes into frequencies if Stark coefficients are provided. These coefficients can be calibrated with `StarkRamseyXYAmpScan` experiment introduced in #1009 . ### Details and comments The test experiment result is available in [6799e7ae-414f-4816-b887-b4c3ba498624](https://quantum-computing.ibm.com/experiments/6799e7ae-414f-4816-b887-b4c3ba498624). This experiment result was obtained with the following experiment code: ```python from qiskit_experiments.library import StarkP1Spectroscopy exp = StarkP1Spectroscopy((0, ), backend) exp_data = exp.run().block_for_results() ``` The Stark coefficients can be directly set in the analysis options. By the default setting, the analysis class searches for the coefficients in the experiment service. If analysis results for all coefficients are found (i.e. previously saved in the service), it automatically converts the amplitudes into frequencies for visualization. A public class method `StarkP1SpectAnalysis.retrieve_coefficients_from_service` is also offered so that a user can set a coefficients dictionary in advance. This is convenient when the experiment instance is run repeatedly, because retrieving analysis data from the service causes a communication overhead. For example, ```python from qiskit_experiments.library.characterization.analysis import StarkP1SpectAnalysis # overhead happens only once coeffs = StarkP1SpectAnalysis.retrieve_coefficients_from_service( service=service, qubit=0, backend=backend.name, ) exp.analysis.set_options(stark_coefficients=coeffs) for _ in range(10): exp_data = exp.run().block_for_result() exp_data.save() ``` User can make a subclass of this analysis class `StarkP1SpectAnalysis` to perform custom analysis. This built-in class doesn't perform any analysis except for visualization. Instead, this class provides a convenient hook `._run_spect_analysis` that takes (x, y, y_err) data and returns a list of `AnalysisResultData`. --------- Co-authored-by: Yael Ben-Haim --- qiskit_experiments/library/__init__.py | 1 + .../library/characterization/__init__.py | 5 +- .../characterization/analysis/__init__.py | 3 +- .../characterization/analysis/t1_analysis.py | 219 ++++++++++++++- .../library/characterization/ramsey_xy.py | 24 +- .../library/characterization/t1.py | 231 +++++++++++++++- qiskit_experiments/test/fake_service.py | 8 +- .../notes/mod-stark-1f1afb538a94fe9a.yaml | 7 + test/database_service/test_fake_service.py | 25 +- .../characterization/test_stark_p1_spect.py | 256 ++++++++++++++++++ .../characterization/test_stark_ramsey_xy.py | 2 +- 11 files changed, 743 insertions(+), 38 deletions(-) create mode 100644 test/library/characterization/test_stark_p1_spect.py diff --git a/qiskit_experiments/library/__init__.py b/qiskit_experiments/library/__init__.py index 45a37f0c07..8664654cea 100644 --- a/qiskit_experiments/library/__init__.py +++ b/qiskit_experiments/library/__init__.py @@ -160,6 +160,7 @@ class instance to manage parameters and pulse schedules. ) from .characterization import ( T1, + StarkP1Spectroscopy, T2Hahn, T2Ramsey, Tphi, diff --git a/qiskit_experiments/library/characterization/__init__.py b/qiskit_experiments/library/characterization/__init__.py index 4bf11a7264..8aaaceee4c 100644 --- a/qiskit_experiments/library/characterization/__init__.py +++ b/qiskit_experiments/library/characterization/__init__.py @@ -24,6 +24,7 @@ :template: autosummary/experiment.rst T1 + StarkP1Spectroscopy T2Ramsey T2Hahn Tphi @@ -62,6 +63,7 @@ T1Analysis T1KerneledAnalysis + StarkP1SpectAnalysis T2RamseyAnalysis T2HahnAnalysis TphiAnalysis @@ -84,6 +86,7 @@ FineAmplitudeAnalysis, RamseyXYAnalysis, StarkRamseyXYAmpScanAnalysis, + StarkP1SpectAnalysis, T2RamseyAnalysis, T1Analysis, T1KerneledAnalysis, @@ -98,7 +101,7 @@ MultiStateDiscriminationAnalysis, ) -from .t1 import T1 +from .t1 import T1, StarkP1Spectroscopy from .qubit_spectroscopy import QubitSpectroscopy from .ef_spectroscopy import EFSpectroscopy from .t2ramsey import T2Ramsey diff --git a/qiskit_experiments/library/characterization/analysis/__init__.py b/qiskit_experiments/library/characterization/analysis/__init__.py index 126f1b171f..8520060772 100644 --- a/qiskit_experiments/library/characterization/analysis/__init__.py +++ b/qiskit_experiments/library/characterization/analysis/__init__.py @@ -17,8 +17,7 @@ from .ramsey_xy_analysis import RamseyXYAnalysis, StarkRamseyXYAmpScanAnalysis from .t2ramsey_analysis import T2RamseyAnalysis from .t2hahn_analysis import T2HahnAnalysis -from .t1_analysis import T1Analysis -from .t1_analysis import T1KerneledAnalysis +from .t1_analysis import T1Analysis, T1KerneledAnalysis, StarkP1SpectAnalysis 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/t1_analysis.py b/qiskit_experiments/library/characterization/analysis/t1_analysis.py index 10a9521e94..cf34a15460 100644 --- a/qiskit_experiments/library/characterization/analysis/t1_analysis.py +++ b/qiskit_experiments/library/characterization/analysis/t1_analysis.py @@ -12,14 +12,20 @@ """ T1 Analysis class. """ -from typing import Union +from typing import Union, Tuple, List, Dict 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 -from qiskit_experiments.framework import Options +import qiskit_experiments.data_processing as dp +import qiskit_experiments.visualization as vis from qiskit_experiments.curve_analysis.curve_data import CurveData +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 class T1Analysis(curve.DecayAnalysis): @@ -142,3 +148,212 @@ def _format_data( return super()._format_data(new_curve_data) 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 379591b82d..b697d8f597 100644 --- a/qiskit_experiments/library/characterization/ramsey_xy.py +++ b/qiskit_experiments/library/characterization/ramsey_xy.py @@ -420,7 +420,7 @@ def parameters(self) -> np.ndarray: return np.arange(0, max_period, interval) return opt.delays - def parameterized_circuits(self) -> Tuple[QuantumCircuit, QuantumCircuit]: + def parameterized_circuits(self) -> Tuple[QuantumCircuit, ...]: """Create circuits with parameters for Ramsey XY experiment with Stark tone. Returns: @@ -538,10 +538,11 @@ def circuits(self) -> List[QuantumCircuit]: def _metadata(self) -> Dict[str, any]: """Return experiment metadata for ExperimentData.""" - return { - "stark_amp": self.experiment_options.stark_amp, - "stark_freq_offset": self.experiment_options.stark_freq_offset, - } + 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): @@ -691,7 +692,7 @@ def parameters(self) -> np.ndarray: return params - def parameterized_circuits(self) -> Tuple[QuantumCircuit, QuantumCircuit]: + def parameterized_circuits(self) -> Tuple[QuantumCircuit, ...]: """Create circuits with parameters for Ramsey XY experiment with Stark tone. Returns: @@ -816,7 +817,10 @@ def circuits(self) -> List[QuantumCircuit]: def _metadata(self) -> Dict[str, any]: """Return experiment metadata for ExperimentData.""" - return { - "stark_length": self._timing.pulse_time(time=self.experiment_options.stark_length), - "stark_freq_offset": self.experiment_options.stark_freq_offset, - } + 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 5a70200e64..43275d836c 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -13,13 +13,24 @@ T1 Experiment class. """ -from typing import List, Optional, Union, Sequence -import numpy as np +from typing import List, Tuple, Dict, Optional, Union, Sequence -from qiskit import QuantumCircuit +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 qiskit_experiments.library.characterization.analysis.t1_analysis import T1Analysis +from qiskit_experiments.library.characterization.analysis.t1_analysis import ( + T1Analysis, + StarkP1SpectAnalysis, +) + +if _optional.HAS_SYMENGINE: + import symengine as sym +else: + import sympy as sym class T1(BaseExperiment): @@ -113,3 +124,215 @@ 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/test/fake_service.py b/qiskit_experiments/test/fake_service.py index dc48e9c7e8..2034380842 100644 --- a/qiskit_experiments/test/fake_service.py +++ b/qiskit_experiments/test/fake_service.py @@ -18,8 +18,9 @@ from datetime import datetime, timedelta import uuid -from qiskit_experiments.test.fake_backend import FakeBackend +from qiskit_ibm_experiment import AnalysisResultData +from qiskit_experiments.test.fake_backend import FakeBackend from qiskit_experiments.database_service.device_component import DeviceComponent from qiskit_experiments.database_service.exceptions import ( ExperimentEntryExists, @@ -101,7 +102,6 @@ def __init__(self): "result_id", "chisq", "creation_datetime", - "service", "backend_name", ] ) @@ -422,7 +422,7 @@ def analysis_results( tags: Optional[List[str]] = None, tags_operator: Optional[str] = "OR", **filters: Any, - ) -> List[Dict]: + ) -> List[AnalysisResultData]: """Returns a list of analysis results filtered by the given criteria""" # pylint: disable = unused-argument df = self.results @@ -479,7 +479,7 @@ def analysis_results( ) df = df.iloc[:limit] - return df.to_dict("records") + return [AnalysisResultData(**res) for res in df.to_dict("records")] def delete_analysis_result(self, result_id: str) -> None: """Deletes an analysis result""" diff --git a/releasenotes/notes/mod-stark-1f1afb538a94fe9a.yaml b/releasenotes/notes/mod-stark-1f1afb538a94fe9a.yaml index 2e5b98b743..855635b1aa 100644 --- a/releasenotes/notes/mod-stark-1f1afb538a94fe9a.yaml +++ b/releasenotes/notes/mod-stark-1f1afb538a94fe9a.yaml @@ -10,3 +10,10 @@ features: the required tone amplitude to cause a particular Stark shift. This experiment scans tone amplitude while fixing the Stark tone length, and fits the result with the dedicated fitter :class:`.StarkRamseyXYAmpScanAnalysis`. + - | + New experiment :class:`.StarkP1Spectroscopy` has been added. + This is a variant of :class:`.T1` experiment to conduct spectroscopy of + qubit relaxation at different qubit frequencies. + The spectroscopy data is just visualized with the dedicated analysis + :class:`.StarkP1SpectAnalysis`. A developer may subclass this analysis class to + perform custom analysis on the spectroscopy data. diff --git a/test/database_service/test_fake_service.py b/test/database_service/test_fake_service.py index 994ca7d39e..ddc273517e 100644 --- a/test/database_service/test_fake_service.py +++ b/test/database_service/test_fake_service.py @@ -303,7 +303,7 @@ def test_results_query(self): for result_type in range(2): resids = sorted( [ - res["result_id"] + res.result_id for res in self.service.analysis_results( result_type=str(result_type), limit=None ) @@ -322,7 +322,7 @@ def test_results_query(self): for experiment_id in range(2): resids = sorted( [ - res["result_id"] + res.result_id for res in self.service.analysis_results( experiment_id=str(experiment_id), limit=None ) @@ -341,7 +341,7 @@ def test_results_query(self): for quality in range(2): resids = sorted( [ - res["result_id"] + res.result_id for res in self.service.analysis_results(quality=quality, limit=None) ] ) @@ -354,7 +354,7 @@ def test_results_query(self): for verified in range(2): resids = sorted( [ - res["result_id"] + res.result_id for res in self.service.analysis_results(verified=verified, limit=None) ] ) @@ -367,7 +367,7 @@ def test_results_query(self): for backend_name in range(2): resids = sorted( [ - res["result_id"] + res.result_id for res in self.service.analysis_results( backend_name=str(backend_name), limit=None ) @@ -385,7 +385,7 @@ def test_results_query(self): resids = sorted( [ - res["result_id"] + res.result_id for res in self.service.analysis_results( tags=["a1", "b1"], tags_operator="AND", limit=None ) @@ -403,7 +403,7 @@ def test_results_query(self): resids = sorted( [ - res["result_id"] + res.result_id for res in self.service.analysis_results( tags=["a1", "c1"], tags_operator="AND", limit=None ) @@ -412,10 +412,7 @@ def test_results_query(self): self.assertEqual(len(resids), 0) resids = sorted( - [ - res["result_id"] - for res in self.service.analysis_results(tags=["a0", "c0"], limit=None) - ] + [res.result_id for res in self.service.analysis_results(tags=["a0", "c0"], limit=None)] ) ref_resids = sorted( [res["result_id"] for res in self.resdict.values() if "a0" in res["tags"]] @@ -423,13 +420,13 @@ def test_results_query(self): self.assertTrue(len(resids) > 0) self.assertEqual(resids, ref_resids) - datetimes = [res["creation_datetime"] for res in self.service.analysis_results(limit=None)] + datetimes = [res.creation_datetime for res in self.service.analysis_results(limit=None)] self.assertTrue(len(datetimes) > 0) for i in range(len(datetimes) - 1): self.assertTrue(datetimes[i] >= datetimes[i + 1]) datetimes = [ - res["creation_datetime"] + res.creation_datetime for res in self.service.analysis_results(sort_by="creation_datetime:asc", limit=None) ] self.assertTrue(len(datetimes) > 0) @@ -442,7 +439,7 @@ def test_delete_result(self): """Test FakeService.delete_analysis_result""" results = self.service.analysis_results(experiment_id="6") old_number = len(results) - to_delete = results[0]["result_id"] + to_delete = results[0].result_id self.service.delete_analysis_result(result_id=to_delete) results = self.service.analysis_results(experiment_id="6") self.assertEqual(len(results), old_number - 1) diff --git a/test/library/characterization/test_stark_p1_spect.py b/test/library/characterization/test_stark_p1_spect.py new file mode 100644 index 0000000000..e3a4f9e2cc --- /dev/null +++ b/test/library/characterization/test_stark_p1_spect.py @@ -0,0 +1,256 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test Stark P1 spectroscopy experiment.""" + +from test.base import QiskitExperimentsTestCase + +import numpy as np +from qiskit import pulse +from qiskit.circuit import QuantumCircuit, Gate +from qiskit.providers import QubitProperties +from qiskit.providers.fake_provider import FakeHanoiV2 + +from qiskit_experiments.framework import ExperimentData, AnalysisResultData +from qiskit_experiments.library import StarkP1Spectroscopy +from qiskit_experiments.library.characterization.analysis import StarkP1SpectAnalysis +from qiskit_experiments.test import FakeService + + +class StarkP1SpectAnalysisReturnXvals(StarkP1SpectAnalysis): + """A test analysis class that returns x values.""" + + def _run_spect_analysis( + self, + xdata: np.ndarray, + ydata: np.ndarray, + ydata_err: np.ndarray, + ): + return [ + AnalysisResultData( + name="xvals", + value=xdata, + ) + ] + + +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, + spacing="linear", + ) + params = exp.parameters() + ref = np.array([-1.0, -0.5, 0.0, 0.5, 1.0]) + + np.testing.assert_array_almost_equal(params, ref) + + def test_quadratic_spaced_parameters(self): + """Test generating parameters with quadratic spacing.""" + exp = StarkP1Spectroscopy((0,)) + exp.set_experiment_options( + min_stark_amp=-1, + max_stark_amp=1, + num_stark_amps=5, + spacing="quadratic", + ) + params = exp.parameters() + ref = np.array([-1.0, -0.25, 0.0, 0.25, 1.0]) + + np.testing.assert_array_almost_equal(params, ref) + + def test_invalid_spacing(self): + """Test setting invalid spacing option.""" + exp = StarkP1Spectroscopy((0,)) + with self.assertRaises(ValueError): + exp.set_experiment_options(spacing="invalid_option") + + def test_circuits(self): + """Test generated circuits.""" + backend = FakeHanoiV2() + + # For simplicity of the test + backend.target.dt = 1 + backend.target.granularity = 1 + backend.target.pulse_alignment = 1 + backend.target.acquire_alignment = 1 + backend.target.qubit_properties = [QubitProperties(frequency=1e9)] + + exp = StarkP1Spectroscopy((0,), backend) + exp.set_experiment_options( + stark_amps=[-0.5, 0.5], + stark_freq_offset=10e6, + t1_delay=100, + stark_sigma=15, + stark_risefall=2, + ) + circs = exp.circuits() + + # amp = -0.5 + with pulse.build() as sched1: + # Red shift: must be greater than f01 + pulse.set_frequency(1.01e9, pulse.DriveChannel(0)) + pulse.play( + # Always positive amplitude + pulse.GaussianSquare(100, 0.5, 15, 40), + pulse.DriveChannel(0), + ) + qc1 = QuantumCircuit(1, 1) + qc1.x(0) + qc1.append(Gate("Stark", 1, [-0.5]), [0]) + qc1.measure(0, 0) + qc1.add_calibration("Stark", (0,), sched1, [-0.5]) + + # amp = +0.5 + with pulse.build() as sched2: + # Blue shift: Must be lower than f01 + pulse.set_frequency(0.99e9, pulse.DriveChannel(0)) + pulse.play( + # Always positive amplitude + pulse.GaussianSquare(100, 0.5, 15, 40), + pulse.DriveChannel(0), + ) + qc2 = QuantumCircuit(1, 1) + qc2.x(0) + qc2.append(Gate("Stark", 1, [0.5]), [0]) + qc2.measure(0, 0) + qc2.add_calibration("Stark", (0,), sched2, [0.5]) + + self.assertEqual(circs[0], qc1) + self.assertEqual(circs[1], qc2) + + def test_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. + + This uses input xvals as-is. + """ + analysis = StarkP1SpectAnalysisReturnXvals() + + xvals = np.linspace(-1, 1, 11) + 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): + """Test running analysis by setting service to the experiment data. + + This must convert x-axis into frequencies with the Stark coefficients. + """ + analysis = StarkP1SpectAnalysisReturnXvals() + + xvals = np.linspace(-1, 1, 11) + exp_data = ExperimentData( + service=self.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) + + 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. + """ + 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, + } + ) + + xvals = np.linspace(-1, 1, 11) + 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) diff --git a/test/library/characterization/test_stark_ramsey_xy.py b/test/library/characterization/test_stark_ramsey_xy.py index 8bfe4670c8..6c8157754c 100644 --- a/test/library/characterization/test_stark_ramsey_xy.py +++ b/test/library/characterization/test_stark_ramsey_xy.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (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