Skip to content

Commit 29aeaa4

Browse files
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 <[email protected]>
1 parent 970b376 commit 29aeaa4

File tree

11 files changed

+743
-38
lines changed

11 files changed

+743
-38
lines changed

qiskit_experiments/library/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ class instance to manage parameters and pulse schedules.
160160
)
161161
from .characterization import (
162162
T1,
163+
StarkP1Spectroscopy,
163164
T2Hahn,
164165
T2Ramsey,
165166
Tphi,

qiskit_experiments/library/characterization/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
:template: autosummary/experiment.rst
2525
2626
T1
27+
StarkP1Spectroscopy
2728
T2Ramsey
2829
T2Hahn
2930
Tphi
@@ -62,6 +63,7 @@
6263
6364
T1Analysis
6465
T1KerneledAnalysis
66+
StarkP1SpectAnalysis
6567
T2RamseyAnalysis
6668
T2HahnAnalysis
6769
TphiAnalysis
@@ -84,6 +86,7 @@
8486
FineAmplitudeAnalysis,
8587
RamseyXYAnalysis,
8688
StarkRamseyXYAmpScanAnalysis,
89+
StarkP1SpectAnalysis,
8790
T2RamseyAnalysis,
8891
T1Analysis,
8992
T1KerneledAnalysis,
@@ -98,7 +101,7 @@
98101
MultiStateDiscriminationAnalysis,
99102
)
100103

101-
from .t1 import T1
104+
from .t1 import T1, StarkP1Spectroscopy
102105
from .qubit_spectroscopy import QubitSpectroscopy
103106
from .ef_spectroscopy import EFSpectroscopy
104107
from .t2ramsey import T2Ramsey

qiskit_experiments/library/characterization/analysis/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
from .ramsey_xy_analysis import RamseyXYAnalysis, StarkRamseyXYAmpScanAnalysis
1818
from .t2ramsey_analysis import T2RamseyAnalysis
1919
from .t2hahn_analysis import T2HahnAnalysis
20-
from .t1_analysis import T1Analysis
21-
from .t1_analysis import T1KerneledAnalysis
20+
from .t1_analysis import T1Analysis, T1KerneledAnalysis, StarkP1SpectAnalysis
2221
from .tphi_analysis import TphiAnalysis
2322
from .cr_hamiltonian_analysis import CrossResonanceHamiltonianAnalysis
2423
from .readout_angle_analysis import ReadoutAngleAnalysis

qiskit_experiments/library/characterization/analysis/t1_analysis.py

Lines changed: 217 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,20 @@
1212
"""
1313
T1 Analysis class.
1414
"""
15-
from typing import Union
15+
from typing import Union, Tuple, List, Dict
1616

1717
import numpy as np
18+
from qiskit_ibm_experiment import IBMExperimentService
19+
from qiskit_ibm_experiment.exceptions import IBMApiError
1820
from uncertainties import unumpy as unp
1921

2022
import qiskit_experiments.curve_analysis as curve
21-
from qiskit_experiments.framework import Options
23+
import qiskit_experiments.data_processing as dp
24+
import qiskit_experiments.visualization as vis
2225
from qiskit_experiments.curve_analysis.curve_data import CurveData
26+
from qiskit_experiments.data_processing.exceptions import DataProcessorError
27+
from qiskit_experiments.database_service.device_component import Qubit
28+
from qiskit_experiments.framework import BaseAnalysis, ExperimentData, AnalysisResultData, Options
2329

2430

2531
class T1Analysis(curve.DecayAnalysis):
@@ -142,3 +148,212 @@ def _format_data(
142148

143149
return super()._format_data(new_curve_data)
144150
return super()._format_data(curve_data)
151+
152+
153+
class StarkP1SpectAnalysis(BaseAnalysis):
154+
"""Analysis class for StarkP1Spectroscopy.
155+
156+
# section: overview
157+
158+
The P1 landscape is hardly predictable because of the random appearance of
159+
lossy TLS notches, and hence this analysis doesn't provide any
160+
generic mathematical model to fit the measurement data.
161+
A developer may subclass this to conduct own analysis.
162+
163+
This analysis just visualizes the measured P1 values against Stark tone amplitudes.
164+
The tone amplitudes can be converted into the amount of Stark shift
165+
when the calibrated coefficients are provided in the analysis option,
166+
or the calibration experiment results are available in the result database.
167+
168+
# section: see_also
169+
:class:`qiskit_experiments.library.characterization.ramsey_xy.StarkRamseyXYAmpScan`
170+
171+
"""
172+
173+
stark_coefficients_names = [
174+
"stark_pos_coef_o1",
175+
"stark_pos_coef_o2",
176+
"stark_pos_coef_o3",
177+
"stark_neg_coef_o1",
178+
"stark_neg_coef_o2",
179+
"stark_neg_coef_o3",
180+
"stark_ferr",
181+
]
182+
183+
@property
184+
def plotter(self) -> vis.CurvePlotter:
185+
"""Curve plotter instance."""
186+
return self.options.plotter
187+
188+
@classmethod
189+
def _default_options(cls) -> Options:
190+
"""Default analysis options.
191+
192+
Analysis Options:
193+
plotter (Plotter): Plotter to visualize P1 landscape.
194+
data_processor (DataProcessor): Data processor to compute P1 value.
195+
stark_coefficients (Union[Dict, str]): Dictionary of Stark shift coefficients to
196+
convert tone amplitudes into amount of Stark shift. This dictionary must include
197+
all keys defined in :attr:`.StarkP1SpectAnalysis.stark_coefficients_names`,
198+
which are calibrated with :class:`.StarkRamseyXYAmpScan`.
199+
Alternatively, it searches for these coefficients in the result database
200+
when "latest" is set. This requires having the experiment service set in
201+
the experiment data to analyze.
202+
x_key (str): Key of the circuit metadata to represent x value.
203+
"""
204+
options = super()._default_options()
205+
206+
p1spect_plotter = vis.CurvePlotter(vis.MplDrawer())
207+
p1spect_plotter.set_figure_options(
208+
xlabel="Stark amplitude",
209+
ylabel="P(1)",
210+
xscale="quadratic",
211+
)
212+
213+
options.update_options(
214+
plotter=p1spect_plotter,
215+
data_processor=dp.DataProcessor("counts", [dp.Probability("1")]),
216+
stark_coefficients="latest",
217+
x_key="xval",
218+
)
219+
return options
220+
221+
# pylint: disable=unused-argument
222+
def _run_spect_analysis(
223+
self,
224+
xdata: np.ndarray,
225+
ydata: np.ndarray,
226+
ydata_err: np.ndarray,
227+
) -> List[AnalysisResultData]:
228+
"""Run further analysis on the spectroscopy data.
229+
230+
.. note::
231+
A subclass can overwrite this method to conduct analysis.
232+
233+
Args:
234+
xdata: X values. This is either amplitudes or frequencies.
235+
ydata: Y values. This is P1 values measured at different Stark tones.
236+
ydata_err: Sampling error of the Y values.
237+
238+
Returns:
239+
A list of analysis results.
240+
"""
241+
return []
242+
243+
@classmethod
244+
def retrieve_coefficients_from_service(
245+
cls,
246+
service: IBMExperimentService,
247+
qubit: int,
248+
backend: str,
249+
) -> Dict:
250+
"""Retrieve stark coefficient dictionary from the experiment service.
251+
252+
Args:
253+
service: A valid experiment service instance.
254+
qubit: Qubit index.
255+
backend: Name of the backend.
256+
257+
Returns:
258+
A dictionary of Stark coefficients to convert amplitude to frequency.
259+
None value is returned when the dictionary is incomplete.
260+
"""
261+
out = {}
262+
try:
263+
for name in cls.stark_coefficients_names:
264+
results = service.analysis_results(
265+
device_components=[str(Qubit(qubit))],
266+
result_type=name,
267+
backend_name=backend,
268+
sort_by=["creation_datetime:desc"],
269+
)
270+
if len(results) == 0:
271+
return None
272+
result_data = getattr(results[0], "result_data")
273+
out[name] = result_data["value"]
274+
except (IBMApiError, ValueError, KeyError, AttributeError):
275+
return None
276+
return out
277+
278+
def _convert_axis(
279+
self,
280+
xdata: np.ndarray,
281+
coefficients: Dict[str, float],
282+
) -> np.ndarray:
283+
"""A helper method to convert x-axis.
284+
285+
Args:
286+
xdata: An array of Stark tone amplitude.
287+
coefficients: Stark coefficients to convert amplitudes into frequencies.
288+
289+
Returns:
290+
An array of amount of Stark shift.
291+
"""
292+
names = self.stark_coefficients_names # alias
293+
positive = np.poly1d([coefficients[names[idx]] for idx in [2, 1, 0, 6]])
294+
negative = np.poly1d([coefficients[names[idx]] for idx in [5, 4, 3, 6]])
295+
296+
new_xdata = np.where(xdata > 0, positive(xdata), negative(xdata))
297+
self.plotter.set_figure_options(
298+
xlabel="Stark shift",
299+
xval_unit="Hz",
300+
xscale="linear",
301+
)
302+
return new_xdata
303+
304+
def _run_analysis(
305+
self,
306+
experiment_data: ExperimentData,
307+
) -> Tuple[List[AnalysisResultData], List["matplotlib.figure.Figure"]]:
308+
309+
x_key = self.options.x_key
310+
311+
# Get calibrated Stark tone coefficients
312+
if self.options.stark_coefficients == "latest" and experiment_data.service is not None:
313+
# Get value from service
314+
stark_coeffs = self.retrieve_coefficients_from_service(
315+
service=experiment_data.service,
316+
qubit=experiment_data.metadata["physical_qubits"][0],
317+
backend=experiment_data.backend_name,
318+
)
319+
elif isinstance(self.options.stark_coefficients, dict):
320+
# Get value from experiment options
321+
missing = set(self.stark_coefficients_names) - self.options.stark_coefficients.keys()
322+
if any(missing):
323+
raise KeyError(
324+
"Following coefficient data is missing in the "
325+
f"'stark_coefficients' dictionary: {missing}."
326+
)
327+
stark_coeffs = self.options.stark_coefficients
328+
else:
329+
# No calibration is available
330+
stark_coeffs = None
331+
332+
# Compute P1 value and sampling error
333+
data = experiment_data.data()
334+
try:
335+
xdata = np.asarray([datum["metadata"][x_key] for datum in data], dtype=float)
336+
except KeyError as ex:
337+
raise DataProcessorError(
338+
f"X value key {x_key} is not defined in circuit metadata."
339+
) from ex
340+
ydata_ufloat = self.options.data_processor(data)
341+
ydata = unp.nominal_values(ydata_ufloat)
342+
ydata_err = unp.std_devs(ydata_ufloat)
343+
344+
# Convert x-axis of amplitudes into Stark shift by consuming calibrated parameters.
345+
if stark_coeffs:
346+
xdata = self._convert_axis(xdata, stark_coeffs)
347+
348+
# Draw figures and create analysis results.
349+
self.plotter.set_series_data(
350+
series_name="stark_p1",
351+
x_formatted=xdata,
352+
y_formatted=ydata,
353+
y_formatted_err=ydata_err,
354+
x_interp=xdata,
355+
y_interp=ydata,
356+
)
357+
analysis_results = self._run_spect_analysis(xdata, ydata, ydata_err)
358+
359+
return analysis_results, [self.plotter.figure()]

qiskit_experiments/library/characterization/ramsey_xy.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def parameters(self) -> np.ndarray:
420420
return np.arange(0, max_period, interval)
421421
return opt.delays
422422

423-
def parameterized_circuits(self) -> Tuple[QuantumCircuit, QuantumCircuit]:
423+
def parameterized_circuits(self) -> Tuple[QuantumCircuit, ...]:
424424
"""Create circuits with parameters for Ramsey XY experiment with Stark tone.
425425
426426
Returns:
@@ -538,10 +538,11 @@ def circuits(self) -> List[QuantumCircuit]:
538538

539539
def _metadata(self) -> Dict[str, any]:
540540
"""Return experiment metadata for ExperimentData."""
541-
return {
542-
"stark_amp": self.experiment_options.stark_amp,
543-
"stark_freq_offset": self.experiment_options.stark_freq_offset,
544-
}
541+
metadata = super()._metadata()
542+
metadata["stark_amp"] = self.experiment_options.stark_amp
543+
metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset
544+
545+
return metadata
545546

546547

547548
class StarkRamseyXYAmpScan(BaseExperiment):
@@ -691,7 +692,7 @@ def parameters(self) -> np.ndarray:
691692

692693
return params
693694

694-
def parameterized_circuits(self) -> Tuple[QuantumCircuit, QuantumCircuit]:
695+
def parameterized_circuits(self) -> Tuple[QuantumCircuit, ...]:
695696
"""Create circuits with parameters for Ramsey XY experiment with Stark tone.
696697
697698
Returns:
@@ -816,7 +817,10 @@ def circuits(self) -> List[QuantumCircuit]:
816817

817818
def _metadata(self) -> Dict[str, any]:
818819
"""Return experiment metadata for ExperimentData."""
819-
return {
820-
"stark_length": self._timing.pulse_time(time=self.experiment_options.stark_length),
821-
"stark_freq_offset": self.experiment_options.stark_freq_offset,
822-
}
820+
metadata = super()._metadata()
821+
metadata["stark_length"] = self._timing.pulse_time(
822+
time=self.experiment_options.stark_length
823+
)
824+
metadata["stark_freq_offset"] = self.experiment_options.stark_freq_offset
825+
826+
return metadata

0 commit comments

Comments
 (0)