diff --git a/qiskit_experiments/__init__.py b/qiskit_experiments/__init__.py index 230997f85d..ee316a8243 100644 --- a/qiskit_experiments/__init__.py +++ b/qiskit_experiments/__init__.py @@ -14,9 +14,6 @@ from .version import __version__ -# Base result classes -from .experiment_data import ExperimentData, AnalysisResult - # Experiment modules from . import composite from . import characterization diff --git a/qiskit_experiments/analysis/curve_fitting.py b/qiskit_experiments/analysis/curve_fitting.py index b2d2e83801..72b05b0094 100644 --- a/qiskit_experiments/analysis/curve_fitting.py +++ b/qiskit_experiments/analysis/curve_fitting.py @@ -19,7 +19,6 @@ import numpy as np import scipy.optimize as opt from qiskit.exceptions import QiskitError -from qiskit_experiments.base_analysis import AnalysisResult from qiskit_experiments.analysis.data_processing import filter_data @@ -31,7 +30,7 @@ def curve_fit( sigma: Optional[np.ndarray] = None, bounds: Optional[Union[Dict[str, Tuple[float, float]], Tuple[np.ndarray, np.ndarray]]] = None, **kwargs, -) -> AnalysisResult: +) -> Dict: r"""Perform a non-linear least squares to fit This solves the optimization problem @@ -110,8 +109,8 @@ def fit_func(x, *params): kwargs["absolute_sigma"] = True # Run curve fit - # TODO: Add error handling so if fitting fails we can return an analysis - # result containing this information + # TODO: Add error handling so if fitting fails we can return a + # dictionary containing this information # pylint: disable = unbalanced-tuple-unpacking popt, pcov = opt.curve_fit( fit_func, xdata, ydata, sigma=sigma, p0=param_p0, bounds=param_bounds, **kwargs @@ -138,7 +137,7 @@ def fit_func(x, *params): "xrange": xdata_range, } - return AnalysisResult(result) + return result def multi_curve_fit( @@ -151,7 +150,7 @@ def multi_curve_fit( weights: Optional[np.ndarray] = None, bounds: Optional[Union[Dict[str, Tuple[float, float]], Tuple[np.ndarray, np.ndarray]]] = None, **kwargs, -) -> AnalysisResult: +) -> Dict: r"""Perform a linearized multi-objective non-linear least squares fit. This solves the optimization problem @@ -230,9 +229,7 @@ def f(x, *params): return y # Run linearized curve_fit - analysis_result = curve_fit(f, xdata, ydata, p0, sigma=wsigma, bounds=bounds, **kwargs) - - return analysis_result + return curve_fit(f, xdata, ydata, p0, sigma=wsigma, bounds=bounds, **kwargs) def process_curve_data( diff --git a/qiskit_experiments/analysis/plotting.py b/qiskit_experiments/analysis/plotting.py index bd9312e17a..4f9cdd1be4 100644 --- a/qiskit_experiments/analysis/plotting.py +++ b/qiskit_experiments/analysis/plotting.py @@ -13,11 +13,9 @@ Plotting functions for experiment analysis """ import functools -from typing import Callable, Optional +from typing import Callable, Optional, Dict import numpy as np -from qiskit_experiments.base_analysis import AnalysisResult - try: from matplotlib import pyplot as plt @@ -44,7 +42,7 @@ def wrapped(*args, **kwargs): @requires_matplotlib def plot_curve_fit( func: Callable, - result: AnalysisResult, + result: Dict, confidence_interval: bool = True, ax: Optional["AxesSubplot"] = None, num_fit_points: int = 100, @@ -52,13 +50,13 @@ def plot_curve_fit( grid: bool = True, **kwargs, ) -> "AxesSubplot": - """Generate plot of a curve fit analysis result. + """Generate plot of a curve fitresult. Wraps ``matplotlib.pyplot.plot``. Args: func: the fit funcion for curve_fit. - result: an AnalysisResult from curve_fit. + result: a result dictionary from curve_fit. confidence_interval: if True plot the confidence interval from popt_err. ax: Optional, a matplotlib axes to add the plot to. num_fit_points: the number of points to plot for xrange. diff --git a/qiskit_experiments/base_analysis.py b/qiskit_experiments/base_analysis.py index f6329fe203..31f06b2e4f 100644 --- a/qiskit_experiments/base_analysis.py +++ b/qiskit_experiments/base_analysis.py @@ -16,7 +16,7 @@ from abc import ABC, abstractmethod from qiskit.exceptions import QiskitError -from .experiment_data import ExperimentData, AnalysisResult +from qiskit.providers.experiment import AnalysisResult, ExperimentData class BaseAnalysis(ABC): diff --git a/qiskit_experiments/base_experiment.py b/qiskit_experiments/base_experiment.py index fb82f4646c..62cc0a638f 100644 --- a/qiskit_experiments/base_experiment.py +++ b/qiskit_experiments/base_experiment.py @@ -19,7 +19,7 @@ from qiskit import transpile, assemble from qiskit.exceptions import QiskitError -from .experiment_data import ExperimentData +from qiskit.providers.experiment import ExperimentDataV1 _TRANSPILE_OPTIONS = { @@ -57,7 +57,7 @@ class BaseExperiment(ABC): __analysis_class__ = None # ExperimentData class for experiment - __experiment_data__ = ExperimentData + __experiment_data__ = ExperimentDataV1 # Custom default transpiler options for experiment subclasses __transpile_defaults__ = {"optimization_level": 0} @@ -89,7 +89,6 @@ def __init__(self, qubits, experiment_type=None, circuit_options=None): self._num_qubits = len(qubits) self._physical_qubits = tuple(qubits) if self._num_qubits != len(set(self._physical_qubits)): - print(self._num_qubits, self._physical_qubits) raise QiskitError("Duplicate qubits in physical qubits list.") # Store options and values @@ -113,7 +112,7 @@ def run(self, backend, experiment_data=None, **kwargs): # Create new experiment data if experiment_data is None: - experiment_data = self.__experiment_data__(self) + experiment_data = self.__experiment_data__(backend, self._type) # Filter kwargs run_options = self.__run_defaults__.copy() diff --git a/qiskit_experiments/characterization/t1_experiment.py b/qiskit_experiments/characterization/t1_experiment.py index e1b23dc74d..619bbaf049 100644 --- a/qiskit_experiments/characterization/t1_experiment.py +++ b/qiskit_experiments/characterization/t1_experiment.py @@ -21,9 +21,10 @@ from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.base_analysis import BaseAnalysis +from qiskit.providers.experiment import AnalysisResultV1 +from qiskit.providers.experiment.device_component import Qubit from qiskit_experiments.analysis.curve_fitting import process_curve_data, curve_fit from qiskit_experiments.analysis.data_processing import level2_probability -from qiskit_experiments import AnalysisResult class T1Analysis(BaseAnalysis): @@ -40,12 +41,12 @@ def _run_analysis( amplitude_bounds=None, offset_bounds=None, **kwargs, - ) -> Tuple[AnalysisResult, None]: + ) -> Tuple[AnalysisResultV1, None]: """ Calculate T1 Args: - experiment_data (ExperimentData): the experiment data to analyze + experiment_data (ExperimentDataV1): the experiment data to analyze t1_guess (float): Optional, an initial guess of T1 amplitude_guess (float): Optional, an initial guess of the coefficient of the exponent offset_guess (float): Optional, an initial guess of the offset @@ -58,13 +59,14 @@ def _run_analysis( The analysis result with the estimated T1 """ - unit = experiment_data._data[0]["metadata"]["unit"] - conversion_factor = experiment_data._data[0]["metadata"].get("dt_factor", None) + unit = experiment_data.data(0)["metadata"]["unit"] + conversion_factor = experiment_data.data(0)["metadata"].get("dt_factor", None) + if conversion_factor is None: conversion_factor = 1 if unit == "s" else apply_prefix(1, unit) xdata, ydata, sigma = process_curve_data( - experiment_data._data, lambda datum: level2_probability(datum, "1") + experiment_data.data(), lambda datum: level2_probability(datum, "1") ) xdata *= conversion_factor @@ -95,22 +97,29 @@ def _run_analysis( ), ) - analysis_result = AnalysisResult( - { - "value": fit_result["popt"][1], - "stderr": fit_result["popt_err"][1], - "unit": "s", - "label": "T1", - "fit": fit_result, - "quality": self._fit_quality( - fit_result["popt"], fit_result["popt_err"], fit_result["reduced_chisq"] - ), - } - ) + result_data = { + "value": fit_result["popt"][1], + "stderr": fit_result["popt_err"][1], + "unit": "s", + "label": "T1", + "fit": fit_result, + "quality": self._fit_quality( + fit_result["popt"], fit_result["popt_err"], fit_result["reduced_chisq"] + ), + } - analysis_result["fit"]["circuit_unit"] = unit + result_data["fit"]["circuit_unit"] = unit if unit == "dt": - analysis_result["fit"]["dt"] = conversion_factor + result_data["fit"]["dt"] = conversion_factor + + analysis_result = AnalysisResultV1( + result_data, + "T1", + [Qubit(experiment_data.data(0)["metadata"]["qubit"])], + experiment_data.id, + quality=result_data["quality"], + verified=True, + ) return analysis_result, None @@ -125,9 +134,9 @@ def _fit_quality(fit_out, fit_err, reduced_chisq): and (fit_err[1] is None or fit_err[1] < fit_out[1]) and (fit_err[2] is None or fit_err[2] < 0.1) ): - return "computer_good" + return "good" else: - return "computer_bad" + return "bad" class T1Experiment(BaseExperiment): diff --git a/qiskit_experiments/composite/composite_analysis.py b/qiskit_experiments/composite/composite_analysis.py index 625215e976..7b0eeddcaf 100644 --- a/qiskit_experiments/composite/composite_analysis.py +++ b/qiskit_experiments/composite/composite_analysis.py @@ -14,8 +14,9 @@ """ from qiskit.exceptions import QiskitError -from qiskit_experiments.base_analysis import BaseAnalysis, AnalysisResult +from qiskit_experiments.base_analysis import BaseAnalysis from .composite_experiment_data import CompositeExperimentData +from qiskit.providers.experiment import AnalysisResult class CompositeAnalysis(BaseAnalysis): diff --git a/qiskit_experiments/composite/composite_experiment_data.py b/qiskit_experiments/composite/composite_experiment_data.py index 48dee73d8c..dfb7f4f127 100644 --- a/qiskit_experiments/composite/composite_experiment_data.py +++ b/qiskit_experiments/composite/composite_experiment_data.py @@ -14,7 +14,7 @@ """ from qiskit.result import marginal_counts -from qiskit_experiments.experiment_data import ExperimentData +from qiskit.providers.experiment import ExperimentData class CompositeExperimentData(ExperimentData): diff --git a/qiskit_experiments/composite/parallel_experiment.py b/qiskit_experiments/composite/parallel_experiment.py index 152709df27..71de2518be 100644 --- a/qiskit_experiments/composite/parallel_experiment.py +++ b/qiskit_experiments/composite/parallel_experiment.py @@ -22,7 +22,7 @@ class ParallelExperiment(CompositeExperiment): """Parallel Experiment class""" def __init__(self, experiments): - """Initialize the analysis object. + """Initialize the parallel experiments object. Args: experiments (List[BaseExperiment]): a list of experiments. diff --git a/qiskit_experiments/experiment_data.py b/qiskit_experiments/experiment_data.py deleted file mode 100644 index b7cbda8394..0000000000 --- a/qiskit_experiments/experiment_data.py +++ /dev/null @@ -1,141 +0,0 @@ -# This code is part of Qiskit. -# -# (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 -# 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. -""" -Experiment Data class -""" - -import uuid - -from qiskit.result import Result -from qiskit.providers import Job, BaseJob -from qiskit.exceptions import QiskitError - - -class AnalysisResult(dict): - """Placeholder class""" - - -class ExperimentData: - """ExperimentData container class""" - - def __init__(self, experiment): - """Initialize the analysis object. - - Args: - experiment (BaseExperiment): experiment object that - generated the data. - """ - # Experiment identification metadata - self._id = str(uuid.uuid4()) - self._experiment = experiment - - # Experiment Data - self._data = [] - - # Analysis - self._analysis_results = [] - - def __repr__(self): - line = 51 * "-" - n_res = len(self._analysis_results) - ret = line - ret += f"\nExperiment: {self._experiment._type}" - ret += f"\nExperiment ID: {self.experiment_id}" - ret += "\nStatus: COMPLETE" - ret += f"\nCircuits: {len(self._data)}" - ret += f"\nAnalysis Results: {n_res}" - ret += "\n" + line - if n_res: - ret += "\nLast Analysis Result" - for key, value in self._analysis_results[-1].items(): - ret += f"\n- {key}: {value}" - return ret - - @property - def experiment_id(self): - """Return the experiment id""" - return self._id - - def experiment(self): - """Return Experiment object""" - return self._experiment - - def analysis_result(self, index): - """Return stored analysis results - - Args: - index (int or slice): the result or range of results to return. - - Returns: - AnalysisResult: the result for an integer index. - List[AnalysisResult]: a list of results for slice index. - """ - return self._analysis_results[index] - - def add_analysis_result(self, result): - """Add an Analysis Result - - Args: - result (AnalysisResult): the analysis result to add. - """ - self._analysis_results.append(result) - - @property - def data(self): - """Return stored experiment data""" - return self._data - - def add_data(self, data): - """Add data to the experiment. - - Args: - data (Result or Job or dict or list): the circuit execution data - to add. This can be a Result, Job, or dict object, or a list - of Result, Job, or dict objects. - - Raises: - QiskitError: if the data is not a valid format. - """ - if isinstance(data, dict): - self._add_single_data(data) - elif isinstance(data, Result): - self._add_result_data(data) - elif isinstance(data, (Job, BaseJob)): - self._add_result_data(data.result()) - elif isinstance(data, list): - for dat in data: - self.add_data(dat) - else: - raise QiskitError("Invalid data format.") - - def _add_result_data(self, result: Result): - """Add data from qiskit Result object""" - num_data = len(result.results) - for i in range(num_data): - metadata = result.results[i].header.metadata - if metadata.get("experiment_type") == self._experiment._type: - data = result.data(i) - data["metadata"] = metadata - if "counts" in data: - # Format to Counts object rather than hex dict - data["counts"] = result.get_counts(i) - self._add_single_data(data) - - def _add_single_data(self, data): - """Add a single data dictionary to the experiment. - - Args: - data (dict): a data dictionary for a single circuit exection. - """ - # This method is intended to be overriden by subclasses when necessary. - if data.get("metadata", {}).get("experiment_type") == self._experiment._type: - self._data.append(data) diff --git a/test/test_t1.py b/test/test_t1.py index 75f794db11..95aa0f5f12 100644 --- a/test/test_t1.py +++ b/test/test_t1.py @@ -20,7 +20,7 @@ from qiskit.providers import BaseBackend from qiskit.providers.models import QasmBackendConfiguration from qiskit.result import Result -from qiskit_experiments import ExperimentData +from qiskit.providers.experiment import ExperimentDataV1, ResultQuality from qiskit_experiments.composite import ParallelExperiment from qiskit_experiments.characterization import T1Experiment, T1Analysis @@ -179,8 +179,8 @@ def test_t1_end2end(self): shots=10000, ).analysis_result(0) - self.assertEqual(res["quality"], "computer_good") - self.assertAlmostEqual(res["value"], t1, delta=3) + self.assertEqual(res.quality, ResultQuality.GOOD) + self.assertAlmostEqual(res.data()["value"], t1, delta=3) def test_t1_parallel(self): """ @@ -208,26 +208,41 @@ def test_t1_analysis(self): Test T1Analysis """ - data = ExperimentData(None) - numbers = [750, 1800, 2750, 3550, 4250, 4850, 5450, 5900, 6400, 6800, 7000, 7350, 7700] + backend_res = { + "backend_name": "T1 backend", + "backend_version": "0", + "qobj_id": 0, + "job_id": 0, + "success": True, + "results": [], + } + shots = 10000 + numbers = [750, 1800, 2750, 3550, 4250, 4850, 5450, 5900, 6400, 6800, 7000, 7350, 7700] for i, count0 in enumerate(numbers): - data._data.append( + backend_res["results"].append( { - "counts": {"0": count0, "1": 10000 - count0}, - "metadata": { - "xval": 3 * i + 1, - "experiment_type": "T1Experiment", - "qubit": 0, - "unit": "ns", - "dt_factor_in_sec": None, + "success": True, + "shots": shots, + "data": {"counts": {"0": count0, "1": shots - count0}}, + "header": { + "metadata": { + "xval": 3 * i + 1, + "experiment_type": "T1Experiment", + "qubit": 0, + "unit": "ns", + "dt_factor_in_sec": None, + } }, } ) + data = ExperimentDataV1(None, "T1Experiment") + data.add_data(Result.from_dict(backend_res)) + res = T1Analysis()._run_analysis(data)[0] - self.assertEqual(res["quality"], "computer_good") - self.assertAlmostEqual(res["value"], 25e-9, delta=3) + self.assertEqual(res.quality, ResultQuality.GOOD) + self.assertAlmostEqual(res.data()["value"], 25e-9, delta=3) def test_t1_metadata(self): """ @@ -256,24 +271,39 @@ def test_t1_low_quality(self): A test where the fit's quality will be low """ - data = ExperimentData(None) + backend_res = { + "backend_name": "T1 backend", + "backend_version": "0", + "qobj_id": 0, + "job_id": 0, + "success": True, + "results": [], + } + shots = 20 for i in range(10): - data._data.append( + backend_res["results"].append( { - "counts": {"0": 10, "1": 10}, - "metadata": { - "xval": i, - "experiment_type": "T1Experiment", - "qubit": 0, - "unit": "ns", - "dt_factor_in_sec": None, + "success": True, + "shots": shots, + "data": {"counts": {"0": 10, "1": shots - 10}}, + "header": { + "metadata": { + "xval": i, + "experiment_type": "T1Experiment", + "qubit": 0, + "unit": "ns", + "dt_factor_in_sec": None, + } }, } ) + data = ExperimentDataV1(None, "T1Experiment") + data.add_data(Result.from_dict(backend_res)) + res = T1Analysis()._run_analysis(data)[0] - self.assertEqual(res["quality"], "computer_bad") + self.assertEqual(res.quality, ResultQuality.BAD) if __name__ == "__main__":