|
12 | 12 | """
|
13 | 13 | T1 Analysis class.
|
14 | 14 | """
|
15 |
| -from typing import Union |
| 15 | +from typing import Union, Tuple, List, Dict |
16 | 16 |
|
17 | 17 | import numpy as np
|
| 18 | +from qiskit_ibm_experiment import IBMExperimentService |
| 19 | +from qiskit_ibm_experiment.exceptions import IBMApiError |
18 | 20 | from uncertainties import unumpy as unp
|
19 | 21 |
|
20 | 22 | 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 |
22 | 25 | 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 |
23 | 29 |
|
24 | 30 |
|
25 | 31 | class T1Analysis(curve.DecayAnalysis):
|
@@ -142,3 +148,212 @@ def _format_data(
|
142 | 148 |
|
143 | 149 | return super()._format_data(new_curve_data)
|
144 | 150 | 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()] |
0 commit comments