From 74a65ba72f0e9b05bfecf689b0215fa0bd7cc21a Mon Sep 17 00:00:00 2001 From: Helena Zhang Date: Wed, 7 Feb 2024 18:38:38 -0500 Subject: [PATCH] put curve fit back in analysis results and deprecate access also deprecate accessing analysis results via numerical indices --- .../measurement/readout_mitigation.rst | 2 +- .../curve_analysis/base_curve_analysis.py | 22 +------------------ .../composite_curve_analysis.py | 13 ++++++++++- .../curve_analysis/curve_analysis.py | 13 ++++++++++- .../framework/experiment_data.py | 15 +++++++++++++ .../tomography/mit_tomography_analysis.py | 2 +- ...experiment-artifacts-c481f4e07226ce9e.yaml | 21 ++++++++++-------- test/curve_analysis/test_baseclass.py | 2 +- .../test_db_experiment_data.py | 17 +++++++------- test/framework/test_composite.py | 10 ++++++--- test/library/calibration/test_fine_drag.py | 4 ++-- .../test_cross_resonance_hamiltonian.py | 4 ++-- .../characterization/test_readout_angle.py | 7 +++--- .../characterization/test_readout_error.py | 15 +++++++------ test/library/quantum_volume/test_qv.py | 5 +++-- 15 files changed, 90 insertions(+), 62 deletions(-) diff --git a/docs/manuals/measurement/readout_mitigation.rst b/docs/manuals/measurement/readout_mitigation.rst index 418e7c7be4..3c1486be1b 100644 --- a/docs/manuals/measurement/readout_mitigation.rst +++ b/docs/manuals/measurement/readout_mitigation.rst @@ -78,7 +78,7 @@ circuits, one for all “0” and one for all “1” results. exp.analysis.set_options(plot=True) result = exp.run(backend) - mitigator = result.analysis_results(0).value + mitigator = result.analysis_results("Local Readout Mitigator").value The resulting measurement matrix can be illustrated by comparing it to the identity. diff --git a/qiskit_experiments/curve_analysis/base_curve_analysis.py b/qiskit_experiments/curve_analysis/base_curve_analysis.py index a2e4386a5b..5aefcbe9f0 100644 --- a/qiskit_experiments/curve_analysis/base_curve_analysis.py +++ b/qiskit_experiments/curve_analysis/base_curve_analysis.py @@ -207,7 +207,7 @@ def _default_options(cls) -> Options: options.plotter = CurvePlotter(MplDrawer()) options.plot_raw_data = False - options.return_fit_parameters = False + options.return_fit_parameters = True options.return_data_points = False options.data_processor = None options.normalization = False @@ -230,26 +230,6 @@ def _default_options(cls) -> Options: return options - def set_options(self, **fields): - """Set the analysis options for :meth:`run` method. - - Args: - fields: The fields to update the options - - Raises: - KeyError: When removed option ``curve_fitter`` is set. - """ - - if "return_fit_parameters" in fields: - warnings.warn( - "@Parameters_* result entry has moved to the experiment data artifact " - "regardless of option value. Setting this value doesn't affect result data.", - DeprecationWarning, - ) - del fields["return_fit_parameters"] - - super().set_options(**fields) - @abstractmethod def _run_data_processing( self, diff --git a/qiskit_experiments/curve_analysis/composite_curve_analysis.py b/qiskit_experiments/curve_analysis/composite_curve_analysis.py index 4be49540e2..82aa3862e1 100644 --- a/qiskit_experiments/curve_analysis/composite_curve_analysis.py +++ b/qiskit_experiments/curve_analysis/composite_curve_analysis.py @@ -39,7 +39,7 @@ ) from qiskit_experiments.framework.containers import FigureType, ArtifactData -from .base_curve_analysis import DATA_ENTRY_PREFIX, BaseCurveAnalysis +from .base_curve_analysis import DATA_ENTRY_PREFIX, BaseCurveAnalysis, PARAMS_ENTRY_PREFIX from .curve_data import CurveFitResult from .scatter_table import ScatterTable from .utils import eval_with_uncertainties @@ -363,6 +363,17 @@ def _run_analysis( else: quality = "bad" + if self.options.return_fit_parameters: + # Store fit status overview entry regardless of success. + # This is sometime useful when debugging the fitting code. + overview = AnalysisResultData( + name=PARAMS_ENTRY_PREFIX + analysis.name, + value=fit_data, + quality=quality, + extra=metadata, + ) + result_data.append(overview) + if fit_data.success: # Add fit data to curve data table model_names = analysis.model_names() diff --git a/qiskit_experiments/curve_analysis/curve_analysis.py b/qiskit_experiments/curve_analysis/curve_analysis.py index d9d2acfa69..d1266e1615 100644 --- a/qiskit_experiments/curve_analysis/curve_analysis.py +++ b/qiskit_experiments/curve_analysis/curve_analysis.py @@ -32,7 +32,7 @@ from qiskit_experiments.framework.containers import FigureType, ArtifactData from qiskit_experiments.data_processing.exceptions import DataProcessorError -from .base_curve_analysis import BaseCurveAnalysis, DATA_ENTRY_PREFIX +from .base_curve_analysis import BaseCurveAnalysis, DATA_ENTRY_PREFIX, PARAMS_ENTRY_PREFIX from .curve_data import FitOptions, CurveFitResult from .scatter_table import ScatterTable from .utils import ( @@ -484,6 +484,17 @@ def _run_analysis( # to generate the figure plot_bool = plot == "always" or (plot == "selective" and quality == "bad") + if self.options.return_fit_parameters: + # Store fit status overview entry regardless of success. + # This is sometime useful when debugging the fitting code. + overview = AnalysisResultData( + name=PARAMS_ENTRY_PREFIX + self.name, + value=fit_data, + quality=quality, + extra=self.options.extra, + ) + result_data.append(overview) + if fit_data.success: # Add fit data to curve data table model_names = self.model_names() diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index b66499ba31..494f0b61d5 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -1545,6 +1545,21 @@ def analysis_results( ) self._retrieve_analysis_results(refresh=refresh) + if index == 0: + warnings.warn( + "Curve fit results have moved to experiment artifacts and will be removed " + "from analysis results in a future release. Use " + 'expdata.artifacts("fit_summary").data to access curve fit results.', + DeprecationWarning, + ) + elif isinstance(index, (int, slice)): + warnings.warn( + "Accessing analysis results via a numerical index is deprecated and will be " + "removed in a future release. Use the ID or name of the analysis result " + "instead.", + DeprecationWarning, + ) + if dataframe: return self._analysis_results.get_data(index, columns=columns) diff --git a/qiskit_experiments/library/tomography/mit_tomography_analysis.py b/qiskit_experiments/library/tomography/mit_tomography_analysis.py index 25ef45ac8c..892afa41a9 100644 --- a/qiskit_experiments/library/tomography/mit_tomography_analysis.py +++ b/qiskit_experiments/library/tomography/mit_tomography_analysis.py @@ -99,7 +99,7 @@ def _run_analysis(self, experiment_data): roerror_analysis.run(roerror_data, replace_results=True).block_for_results() # Construct noisy measurement basis - mitigator = roerror_data.analysis_results(0).value + mitigator = roerror_data.analysis_results("Local Readout Mitigator").value # Run mitigated tomography analysis with noisy mitigated basis # Tomo analysis instance is internally copied by setting option with run. diff --git a/releasenotes/notes/experiment-artifacts-c481f4e07226ce9e.yaml b/releasenotes/notes/experiment-artifacts-c481f4e07226ce9e.yaml index d51d891ff3..7af6898480 100644 --- a/releasenotes/notes/experiment-artifacts-c481f4e07226ce9e.yaml +++ b/releasenotes/notes/experiment-artifacts-c481f4e07226ce9e.yaml @@ -7,17 +7,20 @@ features: and :meth:`.delete_artifact` have been added to manipulate the artifacts. These will be uploaded to the cloud service in JSON form along with the rest of the :class:`.ExperimentData` object when saved. For more information, see the :doc:`artifacts how-to `. -upgrade: +deprecations: + - | + Setting the option ``return_data_points`` to ``True`` in curve analysis has been deprecated. + Data points are now automatically provided in :class:`ExperimentData` objects via the ``curve_data`` + artifact. - | Direct access to the curve fit summary in :class:`.ExperimentData` has moved from :meth:`.analysis_results` to :meth:`.artifacts`, where values are stored in the :attr:`~.ArtifactData.data` attribute of :class:`.ArtifactData` objects. For example, to access the - chi-squared of the fit, ``expdata.analysis_results(0).chisq`` is now - ``expdata.artifacts("fit_summary").data.chisq``. Note that this may also shift the indices of - analysis results that are not curve fit results. For more information, see the :doc:`artifacts how-to - `. -deprecations: + chi-squared of the fit, ``expdata.analysis_results(0).chisq`` is deprecated in favor of + ``expdata.artifacts("fit_summary").data.chisq``. In a future release, the curve fit summary + will be removed from :meth:`.analysis_results`. For more information on artifacts, see the + :doc:`artifacts how-to `. - | - Setting the option ``return_data_points`` to ``True`` in curve analysis has been deprecated. - Data points are now automatically provided in :class:`ExperimentData` objects via the ``curve_data`` - artifact. \ No newline at end of file + Using numerical indices with :meth:`.ExperimentData.analysis_results`, including both integers and + slices, is now deprecated. Access analysis results by analysis result name or ID instead. + \ No newline at end of file diff --git a/test/curve_analysis/test_baseclass.py b/test/curve_analysis/test_baseclass.py index 4b1458018e..8c9626f870 100644 --- a/test/curve_analysis/test_baseclass.py +++ b/test/curve_analysis/test_baseclass.py @@ -571,7 +571,7 @@ def test_selective_figure_generation(self): for res in result.child_data(): # only generate a figure if the quality is bad - if res.analysis_results(0).quality == "bad": + if res.analysis_results("amp").quality == "bad": self.assertEqual(len(res._figures), 1) else: self.assertEqual(len(res._figures), 0) diff --git a/test/database_service/test_db_experiment_data.py b/test/database_service/test_db_experiment_data.py index c43e663232..020e871cc7 100644 --- a/test/database_service/test_db_experiment_data.py +++ b/test/database_service/test_db_experiment_data.py @@ -493,14 +493,15 @@ def test_add_get_analysis_result(self): [res.result_id for res in exp_data.analysis_results()], result_ids, ) - self.assertEqual( - exp_data.analysis_results(1).result_id, - result_ids[1], - ) - self.assertEqual( - [res.result_id for res in exp_data.analysis_results(slice(2, 4))], - result_ids[2:4], - ) + with self.assertWarns(DeprecationWarning): + self.assertEqual( + exp_data.analysis_results(1).result_id, + result_ids[1], + ) + self.assertEqual( + [res.result_id for res in exp_data.analysis_results(slice(2, 4))], + result_ids[2:4], + ) def test_add_get_analysis_results(self): """Test adding and getting a list of analysis results.""" diff --git a/test/framework/test_composite.py b/test/framework/test_composite.py index 2f2b677b2f..c1667a3f60 100644 --- a/test/framework/test_composite.py +++ b/test/framework/test_composite.py @@ -950,9 +950,13 @@ def test_batch_transpile_options_integrated(self): expdata = self.batch2.run(backend, noise_model=noise_model, shots=1000) self.assertExperimentDone(expdata) - self.assertEqual(expdata.child_data(0).analysis_results(0).value, 8) - self.assertEqual(expdata.child_data(1).child_data(0).analysis_results(0).value, 16) - self.assertEqual(expdata.child_data(1).child_data(1).analysis_results(0).value, 4) + self.assertEqual(expdata.child_data(0).analysis_results("non-zero counts").value, 8) + self.assertEqual( + expdata.child_data(1).child_data(0).analysis_results("non-zero counts").value, 16 + ) + self.assertEqual( + expdata.child_data(1).child_data(1).analysis_results("non-zero counts").value, 4 + ) def test_separate_jobs(self): """Test the separate_job experiment option""" diff --git a/test/library/calibration/test_fine_drag.py b/test/library/calibration/test_fine_drag.py index f2c6385be5..50c495c3dd 100644 --- a/test/library/calibration/test_fine_drag.py +++ b/test/library/calibration/test_fine_drag.py @@ -57,14 +57,14 @@ def test_end_to_end(self): exp_data = drag.run(MockIQBackend(FineDragHelper())) self.assertExperimentDone(exp_data) - self.assertEqual(exp_data.analysis_results(0).quality, "good") + self.assertEqual(exp_data.analysis_results("d_theta").quality, "good") def test_end_to_end_no_schedule(self): """Test that we can run without a schedule.""" exp_data = FineXDrag([0]).run(MockIQBackend(FineDragHelper())) self.assertExperimentDone(exp_data) - self.assertEqual(exp_data.analysis_results(0).quality, "good") + self.assertEqual(exp_data.analysis_results("d_theta").quality, "good") def test_circuits_roundtrip_serializable(self): """Test circuits serialization of the experiment.""" diff --git a/test/library/characterization/test_cross_resonance_hamiltonian.py b/test/library/characterization/test_cross_resonance_hamiltonian.py index 61176cca24..00b574430a 100644 --- a/test/library/characterization/test_cross_resonance_hamiltonian.py +++ b/test/library/characterization/test_cross_resonance_hamiltonian.py @@ -210,7 +210,7 @@ def test_integration(self, ix, iy, iz, zx, zy, zz): exp_data = expr.run() self.assertExperimentDone(exp_data, timeout=1000) - self.assertEqual(exp_data.analysis_results(0).quality, "good") + self.assertEqual(exp_data.analysis_results("omega_ix").quality, "good") # These values are computed from other analysis results in post hook. # Thus at least one of these values should be round-trip tested. @@ -265,7 +265,7 @@ def test_integration_backward_compat(self): exp_data = expr.run() self.assertExperimentDone(exp_data, timeout=1000) - self.assertEqual(exp_data.analysis_results(0).quality, "good") + self.assertEqual(exp_data.analysis_results("omega_ix").quality, "good") self.assertAlmostEqual(exp_data.analysis_results("omega_ix").value.n, ix, delta=delta) self.assertAlmostEqual(exp_data.analysis_results("omega_iy").value.n, iy, delta=delta) diff --git a/test/library/characterization/test_readout_angle.py b/test/library/characterization/test_readout_angle.py index a26ffd4e2b..be9b4057bc 100644 --- a/test/library/characterization/test_readout_angle.py +++ b/test/library/characterization/test_readout_angle.py @@ -38,7 +38,8 @@ def test_readout_angle_end2end(self): exp = ReadoutAngle([0]) expdata = exp.run(backend, shots=10000) self.assertExperimentDone(expdata) - res = expdata.analysis_results(0) + + res = expdata.analysis_results("readout_angle") self.assertAlmostEqual(res.value % (2 * np.pi), np.pi / 2, places=2) backend = MockIQBackend( @@ -47,7 +48,7 @@ def test_readout_angle_end2end(self): exp = ReadoutAngle([0]) expdata = exp.run(backend, shots=10000) self.assertExperimentDone(expdata) - res = expdata.analysis_results(0) + res = expdata.analysis_results("readout_angle") self.assertAlmostEqual(res.value % (2 * np.pi), 15 * np.pi / 8, places=2) def test_kerneled_expdata_serialization(self): @@ -69,4 +70,4 @@ def test_kerneled_expdata_serialization(self): self.assertRoundTripSerializable(expdata) # Checking serialization of the analysis - self.assertRoundTripSerializable(expdata.analysis_results(0)) + self.assertRoundTripSerializable(expdata.analysis_results("readout_angle")) diff --git a/test/library/characterization/test_readout_error.py b/test/library/characterization/test_readout_error.py index 0e32e75d0e..6bf871bcb6 100644 --- a/test/library/characterization/test_readout_error.py +++ b/test/library/characterization/test_readout_error.py @@ -39,7 +39,8 @@ def test_local_analysis_ideal(self): exp = LocalReadoutError(backend=backend) expdata = exp.run(backend) self.assertExperimentDone(expdata) - mitigator = expdata.analysis_results(0).value + + mitigator = expdata.analysis_results("Local Readout Mitigator").value qubits = list(range(num_qubits)) self.assertEqual(mitigator._num_qubits, num_qubits) @@ -56,7 +57,7 @@ def test_correlated_analysis_ideal(self): exp = CorrelatedReadoutError(backend=backend) expdata = exp.run(backend) self.assertExperimentDone(expdata) - mitigator = expdata.analysis_results(0).value + mitigator = expdata.analysis_results("Correlated Readout Mitigator").value qubits = list(range(num_qubits)) self.assertEqual(mitigator._num_qubits, num_qubits) @@ -89,7 +90,7 @@ def test_local_analysis(self): expdata.metadata.update(run_meta) exp = LocalReadoutError(qubits) result = exp.analysis.run(expdata) - mitigator = result.analysis_results(0).value + mitigator = result.analysis_results("Local Readout Mitigator").value self.assertEqual(len(qubits), mitigator._num_qubits) self.assertEqual(qubits, mitigator._qubits) @@ -159,7 +160,7 @@ def test_correlated_analysis(self): expdata.metadata.update(run_meta) exp = CorrelatedReadoutError(qubits) result = exp.analysis.run(expdata) - mitigator = result.analysis_results(0).value + mitigator = result.analysis_results("Correlated Readout Mitigator").value self.assertEqual(len(qubits), mitigator._num_qubits) self.assertEqual(qubits, mitigator._qubits) @@ -182,8 +183,8 @@ def test_parallel_running(self): exp = ParallelExperiment([exp1, exp2], flatten_results=False) expdata = exp.run(backend=backend) self.assertExperimentDone(expdata) - mit1 = expdata.child_data(0).analysis_results(0).value - mit2 = expdata.child_data(1).analysis_results(0).value + mit1 = expdata.child_data(0).analysis_results("Correlated Readout Mitigator").value + mit2 = expdata.child_data(1).analysis_results("Correlated Readout Mitigator").value assignment_matrix1 = mit1.assignment_matrix() assignment_matrix2 = mit2.assignment_matrix() self.assertFalse(matrix_equal(assignment_matrix1, assignment_matrix2)) @@ -211,7 +212,7 @@ def test_json_serialization(self): exp = LocalReadoutError(qubits) exp_data = exp.run(backend) self.assertExperimentDone(exp_data) - mitigator = exp_data.analysis_results(0).value + mitigator = exp_data.analysis_results("Local Readout Mitigator").value serialized = json.dumps(mitigator, cls=ExperimentEncoder) loaded = json.loads(serialized, cls=ExperimentDecoder) self.assertTrue(matrix_equal(mitigator.assignment_matrix(), loaded.assignment_matrix())) diff --git a/test/library/quantum_volume/test_qv.py b/test/library/quantum_volume/test_qv.py index 13fce97922..08e2009301 100644 --- a/test/library/quantum_volume/test_qv.py +++ b/test/library/quantum_volume/test_qv.py @@ -113,12 +113,13 @@ def test_qv_sigma_decreasing(self): qv_exp.set_experiment_options(trials=2) expdata1 = qv_exp.run(backend) self.assertExperimentDone(expdata1) - result_data1 = expdata1.analysis_results(0) + + result_data1 = expdata1.analysis_results("mean_HOP") expdata2 = qv_exp.run(backend, analysis=None) self.assertExperimentDone(expdata2) expdata2.add_data(expdata1.data()) qv_exp.analysis.run(expdata2) - result_data2 = expdata2.analysis_results(0) + result_data2 = expdata2.analysis_results("mean_HOP") self.assertTrue(result_data1.extra["trials"] == 2, "number of trials is incorrect") self.assertTrue(