From 8615bf561853596558ce8ca024545ba86927fd30 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 12 May 2021 14:53:33 +0200 Subject: [PATCH 01/22] * First draft of SVD node. * Added tests. * Added train method to DataAction. --- .../data_processing/data_action.py | 11 +- qiskit_experiments/data_processing/nodes.py | 99 ++++++++++++++- test/data_processing/fake_experiment.py | 50 ++++++++ test/data_processing/test_data_processing.py | 61 ++-------- test/data_processing/test_nodes.py | 115 ++++++++++++++++++ 5 files changed, 282 insertions(+), 54 deletions(-) create mode 100644 test/data_processing/fake_experiment.py create mode 100644 test/data_processing/test_nodes.py diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index ff08ff9f2c..ebdaeacdf3 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -13,7 +13,7 @@ """Defines the steps that can be used to analyse data.""" from abc import ABCMeta, abstractmethod -from typing import Any +from typing import Any, List class DataAction(metaclass=ABCMeta): @@ -73,3 +73,12 @@ def __call__(self, data: Any) -> Any: def __repr__(self): """String representation of the node.""" return f"{self.__class__.__name__}(validate={self._validate})" + + def train(self, data: List[Any]): + """A method to train a DataAction. + + Certain data processing nodes, such as a SVD, require data to first train. + + Args: + data: A list of datum. Each datum is a point used to train the node. + """ diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 5b0cda418b..055da021a2 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -13,7 +13,7 @@ """Different data analysis steps.""" from abc import abstractmethod -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import numpy as np from qiskit_experiments.data_processing.data_action import DataAction @@ -72,6 +72,103 @@ def __repr__(self): return f"{self.__class__.__name__}(validate: {self._validate}, scale: {self.scale})" +class SVDAvg(IQPart): + """Singular Value Decomposition of averaged IQ data.""" + + def __init__(self, validate: bool = True): + """ + Args: + validate: If set to False the DataAction will not validate its input. + """ + super().__init__(validate=validate) + self._main_axes = None + self._means = None + self._scales = None + + @property + def axis(self) -> List[np.array]: + """Return the axis of the trained SVD""" + if self._main_axes: + return self._main_axes + + raise DataProcessorError("SVD is not trained.") + + @property + def scales(self) -> List[float]: + """Return the scaling of the SVD.""" + if self._scales: + return self._scales + + raise DataProcessorError("SVD is not trained.") + + def _process(self, datum: np.array) -> np.array: + """Project the IQ data onto the axis defined by an SVD and scale it. + + Args: + datum: A 2D array of qubits, and an average complex IQ point as [real, imaginary]. + + Returns: + A 1D array. Each entry is the real part of the averaged IQ data of a qubit. + + Raises: + DataProcessorError: If the SVD has not been previously trained on data. + """ + + if not self._main_axes: + raise DataProcessorError("SVD must be trained on data before it can be used.") + + n_qubits = datum.shape[0] + processed_data = [] + + # process each averaged IQ point with its own axis. + for idx in range(n_qubits): + + centered = np.array([datum[idx][iq] - self._means[idx][iq] for iq in [0, 1]]) + + processed_data.append((self._main_axes[idx] @ centered) / self._scales[idx]) + + return np.array(processed_data) + + def train(self, data: List[Any]): + """Train the SVD on the given data. + + Each element of the given data will be converted to a 2D array of dimension + n_qubits x 2. The number of qubits is inferred from the shape of the data. + For each qubit the data is collected into an array of shape 2 x n_data_points. + The mean of the in-phase a quadratures is subtracted before passing the data + to numpy's svd function. The dominant axis and the scale is saved for each + qubit so that future data points can be projected onto the axis. + + Args: + data: A list of datums. Each datum will be converted to a 2D array. + """ + if not data: + return + + n_qubits = self._format_data(data[0]).shape[0] + + self._main_axes = [] + self._scales = [] + self._means = [] + + for qubit_idx in range(n_qubits): + datums = np.vstack([self._format_data(datum)[qubit_idx] for datum in data]).T + + # Calculate the mean of the data to recenter it in the IQ plane. + mean_i = np.average(datums[0, :]) + mean_q = np.average(datums[1, :]) + + self._means.append((mean_i, mean_q)) + + datums[0, :] = datums[0, :] - mean_i + datums[1, :] = datums[1, :] - mean_q + + u, s, vh = np.linalg.svd(datums) + + self._main_axes.append(u[:, 0]) + self._scales.append(s[0]) + + class ToReal(IQPart): """IQ data post-processing. Isolate the real part of the IQ data.""" diff --git a/test/data_processing/fake_experiment.py b/test/data_processing/fake_experiment.py new file mode 100644 index 0000000000..99567d5f36 --- /dev/null +++ b/test/data_processing/fake_experiment.py @@ -0,0 +1,50 @@ +# 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. + +"""A FakeExperiment for data processor testing.""" + +from qiskit.test import QiskitTestCase +from qiskit.qobj.common import QobjExperimentHeader +from qiskit_experiments.base_experiment import BaseExperiment + + +class FakeExperiment(BaseExperiment): + """Fake experiment class for testing.""" + + def __init__(self): + """Initialise the fake experiment.""" + self._type = None + super().__init__((0,), "fake_test_experiment") + + def circuits(self, backend=None, **circuit_options): + """Fake circuits.""" + return [] + + +class BaseDataProcessorTest(QiskitTestCase): + """Define some basic setup functionality for data processor tests.""" + + def setUp(self): + """Define variables needed for most tests.""" + + self.base_result_args = dict( + backend_name="test_backend", + backend_version="1.0.0", + qobj_id="id-123", + job_id="job-123", + success=True, + ) + + self.header = QobjExperimentHeader( + memory_slots=2, + metadata={"experiment_type": "fake_test_experiment"}, + ) diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 735a3a5298..c0eb048e82 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -16,12 +16,11 @@ from qiskit.result.models import ExperimentResultData, ExperimentResult from qiskit.result import Result -from qiskit.test import QiskitTestCase -from qiskit.qobj.common import QobjExperimentHeader + from qiskit_experiments import ExperimentData -from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.data_processing.data_processor import DataProcessor from qiskit_experiments.data_processing.exceptions import DataProcessorError +from test.data_processing.fake_experiment import FakeExperiment, BaseDataProcessorTest from qiskit_experiments.data_processing.nodes import ( ToReal, ToImag, @@ -29,31 +28,12 @@ ) -class FakeExperiment(BaseExperiment): - """Fake experiment class for testing.""" - - def __init__(self): - """Initialise the fake experiment.""" - self._type = None - super().__init__((0,), "fake_test_experiment") - - def circuits(self, backend=None, **circuit_options): - """Fake circuits.""" - return [] - - -class DataProcessorTest(QiskitTestCase): +class DataProcessorTest(BaseDataProcessorTest): """Class to test DataProcessor.""" def setUp(self): """Setup variables used for testing.""" - self.base_result_args = dict( - backend_name="test_backend", - backend_version="1.0.0", - qobj_id="id-123", - job_id="job-123", - success=True, - ) + super().setUp() mem1 = ExperimentResultData( memory=[ @@ -71,37 +51,14 @@ def setUp(self): ] ) - header1 = QobjExperimentHeader( - clbit_labels=[["meas", 0], ["meas", 1]], - creg_sizes=[["meas", 2]], - global_phase=0.0, - memory_slots=2, - metadata={"experiment_type": "fake_test_experiment", "x_values": 0.0}, - ) - - header2 = QobjExperimentHeader( - clbit_labels=[["meas", 0], ["meas", 1]], - creg_sizes=[["meas", 2]], - global_phase=0.0, - memory_slots=2, - metadata={"experiment_type": "fake_test_experiment", "x_values": 1.0}, - ) - - res1 = ExperimentResult(shots=3, success=True, meas_level=1, data=mem1, header=header1) - res2 = ExperimentResult(shots=3, success=True, meas_level=1, data=mem2, header=header2) + res1 = ExperimentResult(shots=3, success=True, meas_level=1, data=mem1, header=self.header) + res2 = ExperimentResult(shots=3, success=True, meas_level=1, data=mem2, header=self.header) self.result_lvl1 = Result(results=[res1, res2], **self.base_result_args) raw_counts = {"0x0": 4, "0x2": 6} data = ExperimentResultData(counts=dict(**raw_counts)) - header = QobjExperimentHeader( - metadata={"experiment_type": "fake_test_experiment"}, - clbit_labels=[["c", 0], ["c", 1]], - creg_sizes=[["c", 2]], - n_qubits=2, - memory_slots=2, - ) - res = ExperimentResult(shots=9, success=True, meas_level=2, data=data, header=header) + res = ExperimentResult(shots=9, success=True, meas_level=2, data=data, header=self.header) self.exp_data_lvl2 = ExperimentData(FakeExperiment()) self.exp_data_lvl2.add_data(Result(results=[res], **self.base_result_args)) @@ -133,7 +90,7 @@ def test_to_real(self): [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], ], - "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, + "metadata": {"experiment_type": "fake_test_experiment"}, } expected_new = np.array([[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]]) @@ -166,7 +123,7 @@ def test_to_imag(self): [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], ], - "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, + "metadata": {"experiment_type": "fake_test_experiment"}, } expected_new = np.array( diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py new file mode 100644 index 0000000000..6f88934c5d --- /dev/null +++ b/test/data_processing/test_nodes.py @@ -0,0 +1,115 @@ +# 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. + +"""Data processor tests.""" + +from typing import Any, List +import numpy as np + +from qiskit.result.models import ExperimentResultData, ExperimentResult +from qiskit.result import Result +from qiskit_experiments.experiment_data import ExperimentData +from qiskit_experiments.data_processing.nodes import SVDAvg +from test.data_processing.fake_experiment import FakeExperiment, BaseDataProcessorTest + + +class TestSVD(BaseDataProcessorTest): + """Test the SVD nodes.""" + + def setUp(self): + """Setup experiment data.""" + super().setUp() + + def create_experiment(self, iq_data: List[Any]): + """Populate avg_iq_data to use it for testing. + + Args: + iq_data: A List of IQ data. + """ + + results = [] + for circ_data in iq_data: + res = ExperimentResult( + success=True, + meas_level=1, + data=ExperimentResultData(memory=circ_data), + header=self.header, + shots=1024 + ) + results.append(res) + + self.avg_iq_data = ExperimentData(FakeExperiment()) + self.avg_iq_data.add_data(Result(results=results, **self.base_result_args)) + + def test_simple_data(self): + """ + A simple setting where the IQ data of qubit 0 is oriented along (1,1) and + the IQ data of qubit 1 is oriented along (1,-1). + """ + + iq_data = [ + [[0., 0.], [0., 0.]], + [[1., 1.], [-1., 1.]], + [[-1., -1.], [1., -1.]] + ] + + self.create_experiment(iq_data) + + print([datum["memory"] for datum in self.avg_iq_data.data]) + + iq_svd = SVDAvg(validate=False) + iq_svd.train([datum["memory"] for datum in self.avg_iq_data.data]) + + # qubit 0 IQ data is oriented along (1,1) + self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([1,1]) / np.sqrt(2))) + + # qubit 1 IQ data is oriented along (1, -1) + self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([1, -1]) / np.sqrt(2))) + + processed = iq_svd(np.array([[1,1], [1, -1]])) + expected = np.array([1,1])/np.sqrt(2) + self.assertTrue(np.allclose(processed, expected)) + + processed = iq_svd(np.array([[2,2], [2, -2]])) + self.assertTrue(np.allclose(processed, expected*2)) + + # Check that orthogonal data gives 0. + processed = iq_svd(np.array([[1, -1], [1, 1]])) + expected = np.array([0,0]) + self.assertTrue(np.allclose(processed, expected)) + + def test_svd(self): + """Use IQ data gathered from the hardware.""" + + # This data is primarily oriented along the real axis with a slight tilt. + # The is a large offset in the imaginary dimension when comparing qubits + # 0 and 1. + iq_data = [ + [[-6.20601501e+14, -1.33257051e+15], [-1.70921324e+15, -4.05881657e+15]], + [[-5.80546502e+14, -1.33492509e+15], [-1.65094637e+15, -4.05926942e+15]], + [[-4.04649069e+14, -1.33191056e+15], [-1.29680377e+15, -4.03604815e+15]], + [[-2.22203874e+14, -1.30291309e+15], [-8.57663429e+14, -3.97784973e+15]], + [[-2.92074029e+13, -1.28578530e+15], [-9.78824053e+13, -3.92071056e+15]], + [[1.98056981e+14, -1.26883024e+15], [3.77157017e+14, -3.87460328e+15]], + [[4.29955888e+14, -1.25022995e+15], [1.02340118e+15, -3.79508679e+15]], + [[6.38981344e+14, -1.25084614e+15], [1.68918514e+15, -3.78961044e+15]], + [[7.09988897e+14, -1.21906634e+15], [1.91914171e+15, -3.73670664e+15]], + [[7.63169115e+14, -1.20797552e+15], [2.03772603e+15, -3.74653863e+15]] + ] + + self.create_experiment(iq_data) + + iq_svd = SVDAvg(validate=False) + iq_svd.train([datum["memory"] for datum in self.avg_iq_data.data]) + + self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([0.99633018, 0.08559302]))) + self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([0.99627747, 0.0862044]))) From 3659d66b1ecbe69f1842fd490e256e8014d13970 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 13 May 2021 21:06:30 +0200 Subject: [PATCH 02/22] * Added node and data processor training functionality. --- .../data_processing/data_action.py | 13 +++++ .../data_processing/data_processor.py | 50 +++++++++++++++---- qiskit_experiments/data_processing/nodes.py | 9 ++++ test/data_processing/test_nodes.py | 28 +++++++++-- 4 files changed, 86 insertions(+), 14 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index ebdaeacdf3..166acec7a4 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -74,6 +74,19 @@ def __repr__(self): """String representation of the node.""" return f"{self.__class__.__name__}(validate={self._validate})" + @property + def is_trained(self) -> bool: + """Return False if the DataAction needs to be trained. + + Subclasses can override this property to communicate if they have been trained. + By default all data actions are trained. DataActions that have a training + mechanism will have to override this property. + + Return: + True if the data action has been trained. + """ + return True + def train(self, data: List[Any]): """A method to train a DataAction. diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 374751c36b..f29f41676c 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -54,6 +54,15 @@ def append(self, node: DataAction): """ self._nodes.append(node) + @property + def is_trained(self) -> bool: + """Return True if all nodes of the data processor have been trained.""" + for node in self._nodes: + if not node.is_trained: + return False + + return True + def __call__(self, datum: Dict[str, Any]) -> Any: """ Call self on the given datum. This method sequentially calls the stored data actions @@ -66,7 +75,7 @@ def __call__(self, datum: Dict[str, Any]) -> Any: Returns: processed data: The data processed by the data processor. """ - return self._call_internal(datum, False) + return self._call_internal(datum) def call_with_history( self, datum: Dict[str, Any], history_nodes: Set = None @@ -89,10 +98,13 @@ def call_with_history( return self._call_internal(datum, True, history_nodes) def _call_internal( - self, datum: Dict[str, Any], with_history: bool, history_nodes: Set = None + self, + datum: Dict[str, Any], + with_history: bool = False, + history_nodes: Set = None, + call_up_to_node: int = None ) -> Union[Any, Tuple[Any, List]]: - """ - Internal function to process the data with or with storing the history of the computation. + """Process the data with or without storing the history of the computation. Args: datum: A single item of data, typically from an ExperimentData instance, that @@ -101,6 +113,9 @@ def _call_internal( history_nodes: The nodes, specified by index in the data processing chain, to include in the history. If None is given then all nodes will be included in the history. + call_up_to_node: The data processor will use each node in the processing chain + up to the node indexed by call_up_to_node. If this variable is not specified + then all nodes in the data processing chain will be called. Returns: datum_ and history if with_history is True or datum_ if with_history is False. @@ -108,6 +123,7 @@ def _call_internal( Raises: DataProcessorError: If the input key of the data processor is not contained in datum. """ + call_up_to_node = call_up_to_node or len(self._nodes) if self._input_key not in datum: raise DataProcessorError( @@ -118,14 +134,30 @@ def _call_internal( history = [] for index, node in enumerate(self._nodes): - datum_ = node(datum_) - if with_history and ( - history_nodes is None or (history_nodes and index in history_nodes) - ): - history.append((node.__class__.__name__, datum_, index)) + if index < call_up_to_node: + datum_ = node(datum_) + + if with_history and ( + history_nodes is None or (history_nodes and index in history_nodes) + ): + history.append((node.__class__.__name__, datum_, index)) if with_history: return datum_, history else: return datum_ + + def train(self, data: List[Dict[str, Any]]): + """Train the nodes of the data processor. + + Args: + data: The data to use to train the data processor. + """ + + for index, node in enumerate(self._nodes): + if not node.is_trained: + # Process the data up to the untrained node. + train_data = [self._call_internal(datum, call_up_to_node=index) for datum in data] + + node.train(train_data) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 055da021a2..432a87f1a8 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -101,6 +101,15 @@ def scales(self) -> List[float]: raise DataProcessorError("SVD is not trained.") + @property + def is_trained(self) -> bool: + """Return True is the SVD has been trained. + + Returns: + True if the SVD has been trained. + """ + return True if self._main_axes else False + def _process(self, datum: np.array) -> np.array: """Project the IQ data onto the axis defined by an SVD and scale it. diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 6f88934c5d..d3bd95ff8d 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -19,6 +19,7 @@ from qiskit.result import Result from qiskit_experiments.experiment_data import ExperimentData from qiskit_experiments.data_processing.nodes import SVDAvg +from qiskit_experiments.data_processing.data_processor import DataProcessor from test.data_processing.fake_experiment import FakeExperiment, BaseDataProcessorTest @@ -70,13 +71,13 @@ def test_simple_data(self): iq_svd.train([datum["memory"] for datum in self.avg_iq_data.data]) # qubit 0 IQ data is oriented along (1,1) - self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([1,1]) / np.sqrt(2))) + self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([-1,-1]) / np.sqrt(2))) # qubit 1 IQ data is oriented along (1, -1) - self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([1, -1]) / np.sqrt(2))) + self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([-1, 1]) / np.sqrt(2))) processed = iq_svd(np.array([[1,1], [1, -1]])) - expected = np.array([1,1])/np.sqrt(2) + expected = np.array([-1,-1])/np.sqrt(2) self.assertTrue(np.allclose(processed, expected)) processed = iq_svd(np.array([[2,2], [2, -2]])) @@ -111,5 +112,22 @@ def test_svd(self): iq_svd = SVDAvg(validate=False) iq_svd.train([datum["memory"] for datum in self.avg_iq_data.data]) - self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([0.99633018, 0.08559302]))) - self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([0.99627747, 0.0862044]))) + self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([-0.99633018, -0.08559302]))) + self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([-0.99627747, -0.0862044]))) + + def test_train_svd_processor(self): + """Test that we can train a DataProcessor with an SVD.""" + + processor = DataProcessor("memory", [SVDAvg()]) + + self.assertFalse(processor.is_trained) + + iq_data = [ + [[0., 0.], [0., 0.]], + [[1., 1.], [-1., 1.]], + [[-1., -1.], [1., -1.]] + ] + + self.create_experiment(iq_data) + + processor.train(self.avg_iq_data.data) \ No newline at end of file From 10d78d49cd20ffa40e067c8fc8fd9b5295467ed9 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 13 May 2021 21:30:05 +0200 Subject: [PATCH 03/22] * Fixed bug in _call_internal. * Added SVD training test. * Added required dimension to SVD. --- .../data_processing/data_processor.py | 3 ++- qiskit_experiments/data_processing/nodes.py | 4 ++++ test/data_processing/test_nodes.py | 23 +++++++++++++------ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index f29f41676c..d91f37fc57 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -123,7 +123,8 @@ def _call_internal( Raises: DataProcessorError: If the input key of the data processor is not contained in datum. """ - call_up_to_node = call_up_to_node or len(self._nodes) + if call_up_to_node is None: + call_up_to_node = len(self._nodes) if self._input_key not in datum: raise DataProcessorError( diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index a1c24f4cf8..64ffa01b65 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -90,6 +90,10 @@ def __init__(self, validate: bool = True): self._means = None self._scales = None + def _required_dimension(self) -> int: + """Require memory to be a 2D array.""" + return 2 + @property def axis(self) -> List[np.array]: """Return the axis of the trained SVD""" diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index d3bd95ff8d..4f43a7b8ef 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -65,10 +65,8 @@ def test_simple_data(self): self.create_experiment(iq_data) - print([datum["memory"] for datum in self.avg_iq_data.data]) - - iq_svd = SVDAvg(validate=False) - iq_svd.train([datum["memory"] for datum in self.avg_iq_data.data]) + iq_svd = SVDAvg() + iq_svd.train([datum["memory"] for datum in self.avg_iq_data.data()]) # qubit 0 IQ data is oriented along (1,1) self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([-1,-1]) / np.sqrt(2))) @@ -109,8 +107,8 @@ def test_svd(self): self.create_experiment(iq_data) - iq_svd = SVDAvg(validate=False) - iq_svd.train([datum["memory"] for datum in self.avg_iq_data.data]) + iq_svd = SVDAvg() + iq_svd.train([datum["memory"] for datum in self.avg_iq_data.data()]) self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([-0.99633018, -0.08559302]))) self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([-0.99627747, -0.0862044]))) @@ -127,7 +125,18 @@ def test_train_svd_processor(self): [[1., 1.], [-1., 1.]], [[-1., -1.], [1., -1.]] ] + self.create_experiment(iq_data) + + processor.train(self.avg_iq_data.data()) + + self.assertTrue(processor.is_trained) + # Check that we can use the SVD + iq_data = [ + [[2, 2], [2, -2]] + ] self.create_experiment(iq_data) - processor.train(self.avg_iq_data.data) \ No newline at end of file + processed = processor(self.avg_iq_data.data(0)) + expected = np.array([-2, -2]) / np.sqrt(2) + self.assertTrue(np.allclose(processed, expected)) From eb3fe65592c3bbf496d0e97d42281eabc5681c9a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 14 May 2021 15:11:00 +0200 Subject: [PATCH 04/22] * Added run-time options to data processor * Added error propagation mechanisme to the data processor. * Added optional outcome to Probability. * Added AverageIDData Node. * Added averaging and SVD test. --- .../data_processing/data_action.py | 34 ++-- .../data_processing/data_processor.py | 31 +-- qiskit_experiments/data_processing/nodes.py | 159 +++++++++++---- test/data_processing/test_data_processing.py | 186 ++++++++++++++---- test/data_processing/test_nodes.py | 80 ++++++-- 5 files changed, 377 insertions(+), 113 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 166acec7a4..d2f481b565 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -13,7 +13,7 @@ """Defines the steps that can be used to analyse data.""" from abc import ABCMeta, abstractmethod -from typing import Any, List +from typing import Any, List, Optional, Tuple class DataAction(metaclass=ABCMeta): @@ -30,45 +30,51 @@ def __init__(self, validate: bool = True): self._validate = validate @abstractmethod - def _process(self, datum: Any) -> Any: + def _process(self, datum: Any, error: Optional[Any] = None, **options) -> Tuple[Any, Any]: """ Applies the data processing step to the datum. Args: datum: A single item of data which will be processed. + error: An optional error estimation on the datum that can be further propagated. + options: Keyword arguments passed through the data processor at run-time. Returns: - processed data: The data that has been processed. + processed data: The data that has been processed along with the propagated error. """ @abstractmethod - def _format_data(self, datum: Any) -> Any: - """ - Check that the given data has the correct structure. This method may + def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: + """Format and validate the input. + + Check that the given data and error has the correct structure. This method may additionally change the data type, e.g. converting a list to a numpy array. Args: datum: The data instance to check and format. + error: An optional error estimation on the datum to check and format. Returns: - datum: The data that was checked. + datum, error: The formatted datum and its optional error. Raises: - DataProcessorError: If the data does not have the proper format. + DataProcessorError: If either the data or the error do not have the proper format. """ - def __call__(self, data: Any) -> Any: - """ - Call the data action of this node on the data. + def __call__(self, data: Any, error: Optional[Any] = None, **options) -> Tuple[Any, Any]: + """Call the data action of this node on the data and propagate the error. Args: data: The data to process. The action nodes in the data processor will raise errors if the data does not have the appropriate format. + error: An optional error estimation on the datum that can be further processed. + options: Keyword arguments passed through the data processor at run-time. Returns: - processed data: The data processed by self. + processed data: The data processed by self as a tuple of processed datum and + optionally the propagated error estimate. """ - return self._process(self._format_data(data)) + return self._process(*self._format_data(data, error), **options) def __repr__(self): """String representation of the node.""" @@ -88,7 +94,7 @@ def is_trained(self) -> bool: return True def train(self, data: List[Any]): - """A method to train a DataAction. + """Train a DataAction. Certain data processing nodes, such as a SVD, require data to first train. diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index d91f37fc57..34ad550508 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -63,7 +63,7 @@ def is_trained(self) -> bool: return True - def __call__(self, datum: Dict[str, Any]) -> Any: + def __call__(self, datum: Dict[str, Any], **options) -> Tuple[Any, Any]: """ Call self on the given datum. This method sequentially calls the stored data actions on the datum. @@ -71,15 +71,16 @@ def __call__(self, datum: Dict[str, Any]) -> Any: Args: datum: A single item of data, typically from an ExperimentData instance, that needs to be processed. This dict also contains the metadata of each experiment. + options: Run-time options given as keyword arguments that will be passed to the nodes. Returns: processed data: The data processed by the data processor. """ - return self._call_internal(datum) + return self._call_internal(datum, **options) def call_with_history( - self, datum: Dict[str, Any], history_nodes: Set = None - ) -> Tuple[Any, List]: + self, datum: Dict[str, Any], history_nodes: Set = None, **options + ) -> Tuple[Any, Any, List]: """ Call self on the given datum. This method sequentially calls the stored data actions on the datum and also returns the history of the processed data. @@ -90,20 +91,22 @@ def call_with_history( history_nodes: The nodes, specified by index in the data processing chain, to include in the history. If None is given then all nodes will be included in the history. + options: Run-time options given as key word arguments that will be passed to the nodes. Returns: processed data: The datum processed by the data processor. history: The datum processed at each node of the data processor. """ - return self._call_internal(datum, True, history_nodes) + return self._call_internal(datum, True, history_nodes, **options) def _call_internal( self, datum: Dict[str, Any], with_history: bool = False, history_nodes: Set = None, - call_up_to_node: int = None - ) -> Union[Any, Tuple[Any, List]]: + call_up_to_node: int = None, + **options, + ) -> Union[Tuple[Any, Any], Tuple[Any, Any, List]]: """Process the data with or without storing the history of the computation. Args: @@ -116,6 +119,7 @@ def _call_internal( call_up_to_node: The data processor will use each node in the processing chain up to the node indexed by call_up_to_node. If this variable is not specified then all nodes in the data processing chain will be called. + options: Run-time options given as keyword arguments that will be passed to the nodes. Returns: datum_ and history if with_history is True or datum_ if with_history is False. @@ -132,22 +136,23 @@ def _call_internal( ) datum_ = datum[self._input_key] + error_ = None history = [] for index, node in enumerate(self._nodes): if index < call_up_to_node: - datum_ = node(datum_) + datum_, error_ = node(datum_, error_, **options) if with_history and ( history_nodes is None or (history_nodes and index in history_nodes) ): - history.append((node.__class__.__name__, datum_, index)) + history.append((node.__class__.__name__, datum_, error_, index)) if with_history: - return datum_, history + return datum_, error_, history else: - return datum_ + return datum_, error_ def train(self, data: List[Dict[str, Any]]): """Train the nodes of the data processor. @@ -159,6 +164,8 @@ def train(self, data: List[Dict[str, Any]]): for index, node in enumerate(self._nodes): if not node.is_trained: # Process the data up to the untrained node. - train_data = [self._call_internal(datum, call_up_to_node=index) for datum in data] + train_data = [] + for datum in data: + train_data.append(self._call_internal(datum, call_up_to_node=index)[0]) node.train(train_data) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 64ffa01b65..1ba314fb7d 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -33,21 +33,23 @@ def __init__(self, scale: Optional[float] = None, validate: bool = True): super().__init__(validate) @abstractmethod - def _process(self, datum: np.array) -> np.array: + def _process(self, datum: np.array, error: Optional[np.array] = None, **options) -> np.array: """Defines how the IQ point will be processed. Args: datum: A 2D or a 3D array of complex IQ points as [real, imaginary]. + error: A 2D or a 3D array of errors on complex IQ points as [real, imaginary]. + options: Keyword arguments passed through the data processor at run-time. Returns: - Processed IQ point. + Processed IQ point and its associated error estimate. """ @abstractmethod def _required_dimension(self) -> int: """Return the required dimension of the data.""" - def _format_data(self, datum: Any) -> Any: + def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: """Check that the IQ data has the correct format and convert to numpy array. Args: @@ -56,27 +58,58 @@ def _format_data(self, datum: Any) -> Any: or averaged IQ date (two-dimensional). Returns: - datum as a numpy array. + datum and any error estimate as a numpy array. Raises: DataProcessorError: If the datum does not have the correct format. """ datum = np.asarray(datum, dtype=float) + if error is not None: + error = np.asarray(error, dtype=float) + if self._validate and len(datum.shape) != self._required_dimension(): raise DataProcessorError( - f"Single-shot data given {self.__class__.__name__}" + f"Single-shot data given to {self.__class__.__name__}" f"must be a {self._required_dimension()}D array. Instead, a {len(datum.shape)}D " f"array was given." ) - return datum + if error is not None and self._validate and len(error.shape) != self._required_dimension(): + raise DataProcessorError( + f"Erorr on single-shot data given to {self.__class__.__name__}" + f"must be a {self._required_dimension()}D array. Instead, a {len(error.shape)}D " + f"array was given." + ) + + return datum, error def __repr__(self): """String representation of the node.""" return f"{self.__class__.__name__}(validate: {self._validate}, scale: {self.scale})" +class AverageIQData(IQPart): + """A node that averages single-shot data to create averaged IQ data.""" + + def _required_dimension(self) -> int: + """Require memory to be a 2D array.""" + return 3 + + def _process( + self, datum: np.array, error: Optional[np.array] = None, **options + ) -> Tuple[np.array, np.array]: + """Average the single-shot IQ data. + + Args: + datum: A 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. + + Returns: + A 2D array of qubits and complex averaged IQ points as [real, imaginary]. + """ + return np.average(datum, axis=0), np.std(datum, axis=0) + + class SVDAvg(IQPart): """Singular Value Decomposition of averaged IQ data.""" @@ -117,16 +150,21 @@ def is_trained(self) -> bool: Returns: True if the SVD has been trained. """ - return True if self._main_axes else False + return self._main_axes is not None - def _process(self, datum: np.array) -> np.array: + def _process( + self, datum: np.array, error: Optional[np.array] = None, **options + ) -> Tuple[np.array, np.array]: """Project the IQ data onto the axis defined by an SVD and scale it. Args: datum: A 2D array of qubits, and an average complex IQ point as [real, imaginary]. + error: An optional 2D array of qubits, and an error on an average complex IQ + point as [real, imaginary]. Returns: - A 1D array. Each entry is the real part of the averaged IQ data of a qubit. + A Tuple of 1D arrays of the result of the SVD and the associated error. Each entry + is the real part of the averaged IQ data of a qubit. Raises: DataProcessorError: If the SVD has not been previously trained on data. @@ -138,6 +176,9 @@ def _process(self, datum: np.array) -> np.array: n_qubits = datum.shape[0] processed_data = [] + if error is not None: + processed_error = [] + # process each averaged IQ point with its own axis. for idx in range(n_qubits): @@ -145,7 +186,8 @@ def _process(self, datum: np.array) -> np.array: processed_data.append((self._main_axes[idx] @ centered) / self._scales[idx]) - return np.array(processed_data) + # TODO need to propagate errors + return np.array(processed_data), None def train(self, data: List[Any]): """Train the SVD on the given data. @@ -163,14 +205,14 @@ def train(self, data: List[Any]): if not data: return - n_qubits = self._format_data(data[0]).shape[0] + n_qubits = self._format_data(data[0])[0].shape[0] self._main_axes = [] self._scales = [] self._means = [] for qubit_idx in range(n_qubits): - datums = np.vstack([self._format_data(datum)[qubit_idx] for datum in data]).T + datums = np.vstack([self._format_data(datum)[0][qubit_idx] for datum in data]).T # Calculate the mean of the data to recenter it in the IQ plane. mean_i = np.average(datums[0, :]) @@ -181,10 +223,10 @@ def train(self, data: List[Any]): datums[0, :] = datums[0, :] - mean_i datums[1, :] = datums[1, :] - mean_q - u, s, vh = np.linalg.svd(datums) + mat_u, mat_s, _ = np.linalg.svd(datums) - self._main_axes.append(u[:, 0]) - self._scales.append(s[0]) + self._main_axes.append(mat_u[:, 0]) + self._scales.append(mat_s[0]) class ToReal(IQPart): @@ -194,19 +236,30 @@ def _required_dimension(self) -> int: """Require memory to be a 3D array.""" return 3 - def _process(self, datum: np.array) -> np.array: + def _process( + self, datum: np.array, error: Optional[np.array] = None, **options + ) -> Tuple[np.array, np.array]: """Take the real part of the IQ data. Args: datum: A 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. + error: An optional 3D array of shots, qubits, and an error on a complex IQ point + as [real, imaginary]. Returns: - A 2D array of shots, qubits. Each entry is the real part of the given IQ data. + A 2D array of shots, qubits with the associated error if given. Each entry is the + real part of the given IQ data. """ if self.scale is None: - return datum[:, :, 0] + if error is not None: + return datum[:, :, 0], error[:, :, 0] + else: + return datum[:, :, 0], None - return datum[:, :, 0] * self.scale + if error is not None: + return datum[:, :, 0] * self.scale, error[:, :, 0] * self.scale + else: + return datum[:, :, 0] * self.scale, None class ToRealAvg(IQPart): @@ -216,19 +269,30 @@ def _required_dimension(self) -> int: """Require memory to be a 2D array.""" return 2 - def _process(self, datum: np.array) -> np.array: + def _process( + self, datum: np.array, error: Optional[np.array] = None, **options + ) -> Tuple[np.array, np.array]: """Take the real part of the IQ data. Args: datum: A 2D array of qubits, and a complex averaged IQ point as [real, imaginary]. + error: An optional 2D array of qubits, and an error on a complex averaged IQ + point as [real, imaginary]. Returns: - A 1D array. Each entry is the real part of the averaged IQ data of a qubit. + A 1D array of qubit IQ points with the associated error if given. Each entry is the + real part of the averaged IQ data of a qubit. """ if self.scale is None: - return datum[:, 0] + if error is not None: + return datum[:, 0], error[:, 0] + else: + return datum[:, 0], None - return datum[:, 0] * self.scale + if error is not None: + return datum[:, 0] * self.scale, error[:, 0] * self.scale + else: + return datum[:, 0] * self.scale, None class ToImag(IQPart): @@ -238,19 +302,27 @@ def _required_dimension(self) -> int: """Require memory to be a 3D array.""" return 3 - def _process(self, datum: np.array) -> np.array: + def _process(self, datum: np.array, error: Optional[np.array] = None, **options) -> np.array: """Take the imaginary part of the IQ data. Args: datum: A 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. + error: An optional 3D array of shots, qubits, and an error on a complex IQ point + as [real, imaginary]. Returns: A 2D array of shots, qubits. Each entry is the imaginary part of the given IQ data. """ if self.scale is None: - return datum[:, :, 1] + if error is not None: + return datum[:, :, 1], error[:, :, 1] + else: + return datum[:, :, 1], None - return datum[:, :, 1] * self.scale + if error is not None: + return datum[:, :, 1] * self.scale, error[:, :, 1] * self.scale + else: + return datum[:, :, 1] * self.scale, None class ToImagAvg(IQPart): @@ -260,36 +332,47 @@ def _required_dimension(self) -> int: """Require memory to be a 2D array.""" return 2 - def _process(self, datum: np.array) -> np.array: + def _process( + self, datum: np.array, error: Optional[np.array] = None, **options + ) -> Tuple[np.array, np.array]: """Take the imaginary part of the IQ data. Args: datum: A 2D array of qubits, and a complex averaged IQ point as [real, imaginary]. + error: An optional 3D array of shots, qubits, and an error on a complex IQ point + as [real, imaginary]. Returns: - A 1D array. Each entry is the imaginary part of the averaged IQ data of a qubit. + A 2D array of shots, qubits with the associated error if given. Each entry is the + imaginary part of the given IQ data. """ if self.scale is None: - return datum[:, 1] + if error is not None: + return datum[:, 1], error[:, 1] + else: + return datum[:, 1], None - return datum[:, 1] * self.scale + if error is not None: + return datum[:, 1] * self.scale, error[:, 1] * self.scale + else: + return datum[:, 1] * self.scale, None class Probability(DataAction): """Count data post processing. This returns the probabilities of the outcome string used to initialize an instance of Probability.""" - def __init__(self, outcome: str, validate: bool = True): + def __init__(self, outcome: str = "1", validate: bool = True): """Initialize a counts to probability data conversion. Args: - outcome: The bitstring for which to compute the probability. + outcome: The bitstring for which to compute the probability which defaults to "1". validate: If set to False the DataAction will not validate its input. """ self._outcome = outcome super().__init__(validate) - def _format_data(self, datum: dict) -> dict: + def _format_data(self, datum: dict, error: Optional[Any] = None) -> Tuple[dict, Any]: """ Checks that the given data has a counts format. @@ -321,9 +404,11 @@ def _format_data(self, datum: dict) -> dict: f"Count {bit_str} is not a valid count value in {self.__class__.__name__}." ) - return datum + return datum, None - def _process(self, datum: Dict[str, Any]) -> Tuple[float, float]: + def _process( + self, datum: Dict[str, Any], error: Optional[Dict] = None, **options + ) -> Tuple[float, float]: """ Args: datum: The data dictionary,taking the data under counts and @@ -332,8 +417,10 @@ def _process(self, datum: Dict[str, Any]) -> Tuple[float, float]: Returns: processed data: A dict with the populations. """ + outcome = options.get("outcome", self._outcome) + shots = sum(datum.values()) - p_mean = datum.get(self._outcome, 0.0) / shots + p_mean = datum.get(outcome, 0.0) / shots p_var = p_mean * (1 - p_mean) / shots return p_mean, p_var diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index ba1e1de786..8b8061f131 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -12,16 +12,19 @@ """Data processor tests.""" -import numpy as np +# pylint: disable=unbalanced-tuple-unpacking +from test.data_processing.fake_experiment import FakeExperiment, BaseDataProcessorTest +import numpy as np from qiskit.result.models import ExperimentResultData, ExperimentResult from qiskit.result import Result from qiskit_experiments import ExperimentData from qiskit_experiments.data_processing.data_processor import DataProcessor from qiskit_experiments.data_processing.exceptions import DataProcessorError -from test.data_processing.fake_experiment import FakeExperiment, BaseDataProcessorTest from qiskit_experiments.data_processing.nodes import ( + AverageIQData, + SVDAvg, ToReal, ToRealAvg, ToImag, @@ -70,10 +73,11 @@ def test_empty_processor(self): """Check that a DataProcessor without steps does nothing.""" data_processor = DataProcessor("counts") - datum = data_processor(self.exp_data_lvl2.data(0)) + datum, error = data_processor(self.exp_data_lvl2.data(0)) self.assertEqual(datum, {"00": 4, "10": 6}) + self.assertIsNone(error) - datum, history = data_processor.call_with_history(self.exp_data_lvl2.data(0)) + datum, error, history = data_processor.call_with_history(self.exp_data_lvl2.data(0)) self.assertEqual(datum, {"00": 4, "10": 6}) self.assertEqual(history, []) @@ -84,7 +88,7 @@ def test_to_real(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - new_data = processor(exp_data.data(0)) + new_data, error = processor(exp_data.data(0)) expected_old = { "memory": [ @@ -99,9 +103,10 @@ def test_to_real(self): self.assertEqual(exp_data.data(0), expected_old) self.assertTrue(np.allclose(new_data, expected_new)) + self.assertIsNone(error) # Test that we can call with history. - new_data, history = processor.call_with_history(exp_data.data(0)) + new_data, error, history = processor.call_with_history(exp_data.data(0)) self.assertEqual(exp_data.data(0), expected_old) self.assertTrue(np.allclose(new_data, expected_new)) @@ -117,7 +122,7 @@ def test_to_imag(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - new_data = processor(exp_data.data(0)) + new_data, error = processor(exp_data.data(0)) expected_old = { "memory": [ @@ -138,9 +143,10 @@ def test_to_imag(self): self.assertEqual(exp_data.data(0), expected_old) self.assertTrue(np.allclose(new_data, expected_new)) + self.assertIsNone(error) # Test that we can call with history. - new_data, history = processor.call_with_history(exp_data.data(0)) + new_data, error, history = processor.call_with_history(exp_data.data(0)) self.assertEqual(exp_data.data(0), expected_old) self.assertTrue(np.allclose(new_data, expected_new)) @@ -153,10 +159,15 @@ def test_populations(self): processor = DataProcessor("counts") processor.append(Probability("00")) - new_data = processor(self.exp_data_lvl2.data(0)) + new_data, error = processor(self.exp_data_lvl2.data(0)) - self.assertEqual(new_data[0], 0.4) - self.assertEqual(new_data[1], 0.4 * (1 - 0.4) / 10) + self.assertEqual(new_data, 0.4) + self.assertEqual(error, 0.4 * (1 - 0.4) / 10) + + new_data, error = processor(self.exp_data_lvl2.data(0), outcome="10") + + self.assertEqual(new_data, 0.6) + self.assertEqual(error, 0.6 * (1 - 0.6) / 10) def test_validation(self): """Test the validation mechanism.""" @@ -169,21 +180,13 @@ def test_validation(self): processor({"counts": [0, 1, 2]}) -class TestIQSingleAvg(QiskitTestCase): +class TestIQSingleAvg(BaseDataProcessorTest): """Test the IQ data processing nodes single and average.""" def setUp(self): """Setup some IQ data.""" super().setUp() - self.base_result_args = dict( - backend_name="test_backend", - backend_version="1.0.0", - qobj_id="id-123", - job_id="job-123", - success=True, - ) - mem_avg = ExperimentResultData( memory=[[-539698.0, -153030784.0], [5541283.0, -160369600.0]] ) @@ -198,23 +201,16 @@ def setUp(self): ] ) - header = QobjExperimentHeader( - metadata={"experiment_type": "fake_test_experiment"}, - clbit_labels=[["c", 0], ["c", 1]], - creg_sizes=[["c", 2]], - n_qubits=2, - memory_slots=2, - ) res_single = ExperimentResult( shots=3, success=True, meas_level=1, meas_return="single", data=mem_single, - header=header, + header=self.header, ) res_avg = ExperimentResult( - shots=6, success=True, meas_level=1, meas_return="avg", data=mem_avg, header=header + shots=6, success=True, meas_level=1, meas_return="avg", data=mem_avg, header=self.header ) # result_single = Result(results=[res_single], **self.base_result_args) @@ -235,7 +231,7 @@ def test_avg_and_single(self): imag_avg = DataProcessor("memory", [ToImagAvg(scale=1)]) # Test the real single shot node - new_data = real_single(self.exp_data_single.data(0)) + new_data, error = real_single(self.exp_data_single.data(0)) expected = np.array( [ [-56470872.0, -53407256.0], @@ -247,12 +243,13 @@ def test_avg_and_single(self): ] ) self.assertTrue(np.allclose(new_data, expected)) + self.assertIsNone(error) with self.assertRaises(DataProcessorError): real_single(self.exp_data_avg.data(0)) # Test the imaginary single shot node - new_data = imag_single(self.exp_data_single.data(0)) + new_data, error = imag_single(self.exp_data_single.data(0)) expected = np.array( [ [-136691568.0, -176278624.0], @@ -266,12 +263,135 @@ def test_avg_and_single(self): self.assertTrue(np.allclose(new_data, expected)) # Test the real average node - new_data = real_avg(self.exp_data_avg.data(0)) + new_data, error = real_avg(self.exp_data_avg.data(0)) self.assertTrue(np.allclose(new_data, np.array([-539698.0, 5541283.0]))) # Test the imaginary average node - new_data = imag_avg(self.exp_data_avg.data(0)) + new_data, error = imag_avg(self.exp_data_avg.data(0)) self.assertTrue(np.allclose(new_data, np.array([-153030784.0, -160369600.0]))) with self.assertRaises(DataProcessorError): real_avg(self.exp_data_single.data(0)) + + +class TestAveragingAndSVD(BaseDataProcessorTest): + """Test the averaging of single-shot IQ data followed by a SVD.""" + + def setUp(self): + """Here, single-shots average to points at plus/minus 1.""" + super().setUp() + + circ_es = ExperimentResultData( + memory=[ + [[1.1, 0.9], [-0.8, 1.0]], + [[1.2, 1.1], [-0.9, 1.0]], + [[0.8, 1.1], [-1.2, 1.0]], + [[0.9, 0.9], [-1.1, 1.0]], + ] + ) + + circ_gs = ExperimentResultData( + memory=[ + [[-1.1, -0.9], [0.8, -1.0]], + [[-1.2, -1.1], [0.9, -1.0]], + [[-0.8, -1.1], [1.2, -1.0]], + [[-0.9, -0.9], [1.1, -1.0]], + ] + ) + + circ_x90p = ExperimentResultData( + memory=[ + [[-1.0, -1.0], [1.0, -1.0]], + [[-1.0, -1.0], [1.0, -1.0]], + [[1.0, 1.0], [-1.0, 1.0]], + [[1.0, 1.0], [-1.0, 1.0]], + ] + ) + + circ_x45p = ExperimentResultData( + memory=[ + [[-1.0, -1.0], [1.0, -1.0]], + [[-1.0, -1.0], [1.0, -1.0]], + [[-1.0, -1.0], [1.0, -1.0]], + [[1.0, 1.0], [-1.0, 1.0]], + ] + ) + + res_es = ExperimentResult( + shots=4, + success=True, + meas_level=1, + meas_return="single", + data=circ_es, + header=self.header, + ) + + res_gs = ExperimentResult( + shots=4, + success=True, + meas_level=1, + meas_return="single", + data=circ_gs, + header=self.header, + ) + + res_x90p = ExperimentResult( + shots=4, + success=True, + meas_level=1, + meas_return="single", + data=circ_x90p, + header=self.header, + ) + + res_x45p = ExperimentResult( + shots=4, + success=True, + meas_level=1, + meas_return="single", + data=circ_x45p, + header=self.header, + ) + + self.data = ExperimentData(FakeExperiment()) + self.data.add_data( + Result(results=[res_es, res_gs, res_x90p, res_x45p], **self.base_result_args) + ) + + def test_averaging(self): + """Test that averaging of the datums produces the expected IQ points.""" + + processor = DataProcessor("memory", [AverageIQData()]) + + # Test that we get the expected outcome for the excited state + processed, error = processor(self.data.data(0)) + expected_avg = np.array([[1.0, 1.0], [-1.0, 1.0]]) + expected_std = np.array([[0.15811388300841894, 0.1], [0.15811388300841894, 0.0]]) + self.assertTrue(np.allclose(processed, expected_avg)) + self.assertTrue(np.allclose(error, expected_std)) + + # Test that we get the expected outcome for the ground state + processed, error = processor(self.data.data(1)) + expected_avg = np.array([[-1.0, -1.0], [1.0, -1.0]]) + expected_std = np.array([[0.15811388300841894, 0.1], [0.15811388300841894, 0.0]]) + self.assertTrue(np.allclose(processed, expected_avg)) + self.assertTrue(np.allclose(error, expected_std)) + + def test_averaging_and_svd(self): + """Test averaging followed by a SVD.""" + + processor = DataProcessor("memory", [AverageIQData(), SVDAvg()]) + + # Test training using the calibration points + self.assertFalse(processor.is_trained) + processor.train([self.data.data(idx) for idx in [0, 1]]) + self.assertTrue(processor.is_trained) + + # Test the x90p rotation + processed, error = processor(self.data.data(2)) + self.assertTrue(np.allclose(processed, np.array([0, 0]))) + self.assertIsNone(error) + + # Test the x45p rotation + processed, error = processor(self.data.data(3)) + self.assertTrue(np.allclose(processed, np.array([0.5, -0.5])/np.sqrt(2.))) diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 4f43a7b8ef..a01d3ee21b 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -12,44 +12,60 @@ """Data processor tests.""" +# pylint: disable=unbalanced-tuple-unpacking + +from test.data_processing.fake_experiment import FakeExperiment, BaseDataProcessorTest + from typing import Any, List import numpy as np from qiskit.result.models import ExperimentResultData, ExperimentResult from qiskit.result import Result from qiskit_experiments.experiment_data import ExperimentData -from qiskit_experiments.data_processing.nodes import SVDAvg +from qiskit_experiments.data_processing.nodes import SVDAvg, AverageIQData from qiskit_experiments.data_processing.data_processor import DataProcessor -from test.data_processing.fake_experiment import FakeExperiment, BaseDataProcessorTest class TestSVD(BaseDataProcessorTest): """Test the SVD nodes.""" - def setUp(self): - """Setup experiment data.""" - super().setUp() + def __init__(self): + """Init""" + self.iq_experiment = None + super().__init__() - def create_experiment(self, iq_data: List[Any]): + def create_experiment(self, iq_data: List[Any], single_shot: bool = False): """Populate avg_iq_data to use it for testing. Args: iq_data: A List of IQ data. + single_shot: Indicates if the data is single-shot or not. """ - results = [] - for circ_data in iq_data: + if not single_shot: + for circ_data in iq_data: + res = ExperimentResult( + success=True, + meas_level=1, + meas_return="avg", + data=ExperimentResultData(memory=circ_data), + header=self.header, + shots=1024 + ) + results.append(res) + else: res = ExperimentResult( success=True, meas_level=1, - data=ExperimentResultData(memory=circ_data), + meas_return="single", + data=ExperimentResultData(memory=iq_data), header=self.header, shots=1024 ) results.append(res) - self.avg_iq_data = ExperimentData(FakeExperiment()) - self.avg_iq_data.add_data(Result(results=results, **self.base_result_args)) + self.iq_experiment = ExperimentData(FakeExperiment()) + self.iq_experiment.add_data(Result(results=results, **self.base_result_args)) def test_simple_data(self): """ @@ -66,7 +82,7 @@ def test_simple_data(self): self.create_experiment(iq_data) iq_svd = SVDAvg() - iq_svd.train([datum["memory"] for datum in self.avg_iq_data.data()]) + iq_svd.train([datum["memory"] for datum in self.iq_experiment.data()]) # qubit 0 IQ data is oriented along (1,1) self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([-1,-1]) / np.sqrt(2))) @@ -74,15 +90,15 @@ def test_simple_data(self): # qubit 1 IQ data is oriented along (1, -1) self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([-1, 1]) / np.sqrt(2))) - processed = iq_svd(np.array([[1,1], [1, -1]])) + processed, _ = iq_svd(np.array([[1,1], [1, -1]])) expected = np.array([-1,-1])/np.sqrt(2) self.assertTrue(np.allclose(processed, expected)) - processed = iq_svd(np.array([[2,2], [2, -2]])) + processed, _ = iq_svd(np.array([[2,2], [2, -2]])) self.assertTrue(np.allclose(processed, expected*2)) # Check that orthogonal data gives 0. - processed = iq_svd(np.array([[1, -1], [1, 1]])) + processed, _ = iq_svd(np.array([[1, -1], [1, 1]])) expected = np.array([0,0]) self.assertTrue(np.allclose(processed, expected)) @@ -108,7 +124,7 @@ def test_svd(self): self.create_experiment(iq_data) iq_svd = SVDAvg() - iq_svd.train([datum["memory"] for datum in self.avg_iq_data.data()]) + iq_svd.train([datum["memory"] for datum in self.iq_experiment.data()]) self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([-0.99633018, -0.08559302]))) self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([-0.99627747, -0.0862044]))) @@ -127,7 +143,7 @@ def test_train_svd_processor(self): ] self.create_experiment(iq_data) - processor.train(self.avg_iq_data.data()) + processor.train(self.iq_experiment.data()) self.assertTrue(processor.is_trained) @@ -137,6 +153,34 @@ def test_train_svd_processor(self): ] self.create_experiment(iq_data) - processed = processor(self.avg_iq_data.data(0)) + processed, _ = processor(self.iq_experiment.data(0)) expected = np.array([-2, -2]) / np.sqrt(2) self.assertTrue(np.allclose(processed, expected)) + + def test_iq_averaging(self): + """Test averaging of IQ-data.""" + + iq_data = [ + [[-6.20601501e+14, -1.33257051e+15], [-1.70921324e+15, -4.05881657e+15]], + [[-5.80546502e+14, -1.33492509e+15], [-1.65094637e+15, -4.05926942e+15]], + [[-4.04649069e+14, -1.33191056e+15], [-1.29680377e+15, -4.03604815e+15]], + [[-2.22203874e+14, -1.30291309e+15], [-8.57663429e+14, -3.97784973e+15]], + [[-2.92074029e+13, -1.28578530e+15], [-9.78824053e+13, -3.92071056e+15]], + [[1.98056981e+14, -1.26883024e+15], [3.77157017e+14, -3.87460328e+15]], + [[4.29955888e+14, -1.25022995e+15], [1.02340118e+15, -3.79508679e+15]], + [[6.38981344e+14, -1.25084614e+15], [1.68918514e+15, -3.78961044e+15]], + [[7.09988897e+14, -1.21906634e+15], [1.91914171e+15, -3.73670664e+15]], + [[7.63169115e+14, -1.20797552e+15], [2.03772603e+15, -3.74653863e+15]] + ] + + self.create_experiment(iq_data, single_shot=True) + + avg_iq = AverageIQData() + + avg_datum, error = avg_iq(self.iq_experiment.data(0)["memory"]) + + expected_avg = np.array([[8.82943876e+13, -1.27850527e+15], [ 1.43410186e+14, -3.89952402e+15]]) + expected_std = np.array([[5.07650185e+14, 4.44664719e+13], [1.40522641e+15, 1.22326831e+14]]) + + self.assertTrue(np.allclose(avg_datum, expected_avg)) + self.assertTrue(np.allclose(error, expected_std)) From 9ae1ff086843b420ad1e18b672b2792ff68aa635 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 14 May 2021 16:36:30 +0200 Subject: [PATCH 05/22] * Added error propagation for SVD node and tests. * Black and lint. --- qiskit_experiments/data_processing/nodes.py | 16 ++- test/data_processing/test_data_processing.py | 16 ++- test/data_processing/test_nodes.py | 112 +++++++++++-------- 3 files changed, 87 insertions(+), 57 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 1ba314fb7d..b0969764a5 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -107,7 +107,7 @@ def _process( Returns: A 2D array of qubits and complex averaged IQ points as [real, imaginary]. """ - return np.average(datum, axis=0), np.std(datum, axis=0) + return np.average(datum, axis=0), np.std(datum, axis=0) / np.sqrt(datum.shape[0]) class SVDAvg(IQPart): @@ -170,7 +170,7 @@ def _process( DataProcessorError: If the SVD has not been previously trained on data. """ - if not self._main_axes: + if not self.is_trained: raise DataProcessorError("SVD must be trained on data before it can be used.") n_qubits = datum.shape[0] @@ -178,6 +178,8 @@ def _process( if error is not None: processed_error = [] + else: + processed_error = None # process each averaged IQ point with its own axis. for idx in range(n_qubits): @@ -186,8 +188,14 @@ def _process( processed_data.append((self._main_axes[idx] @ centered) / self._scales[idx]) - # TODO need to propagate errors - return np.array(processed_data), None + if error is not None: + angle = np.arctan(self._main_axes[idx][1] / self._main_axes[idx][0]) + error_value = np.sqrt( + (error[idx][0] * np.cos(angle)) ** 2 + (error[idx][1] * np.sin(angle)) ** 2 + ) + processed_error.append(error_value) + + return np.array(processed_data), processed_error def train(self, data: List[Any]): """Train the SVD on the given data. diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 8b8061f131..4ad41c4537 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -314,6 +314,10 @@ def setUp(self): [[-1.0, -1.0], [1.0, -1.0]], [[-1.0, -1.0], [1.0, -1.0]], [[1.0, 1.0], [-1.0, 1.0]], + [[-1.0, -1.0], [1.0, -1.0]], + [[-1.0, -1.0], [1.0, -1.0]], + [[-1.0, -1.0], [1.0, -1.0]], + [[1.0, 1.0], [-1.0, 1.0]], ] ) @@ -345,7 +349,7 @@ def setUp(self): ) res_x45p = ExperimentResult( - shots=4, + shots=8, success=True, meas_level=1, meas_return="single", @@ -366,14 +370,14 @@ def test_averaging(self): # Test that we get the expected outcome for the excited state processed, error = processor(self.data.data(0)) expected_avg = np.array([[1.0, 1.0], [-1.0, 1.0]]) - expected_std = np.array([[0.15811388300841894, 0.1], [0.15811388300841894, 0.0]]) + expected_std = np.array([[0.15811388300841894, 0.1], [0.15811388300841894, 0.0]]) / 2.0 self.assertTrue(np.allclose(processed, expected_avg)) self.assertTrue(np.allclose(error, expected_std)) # Test that we get the expected outcome for the ground state processed, error = processor(self.data.data(1)) expected_avg = np.array([[-1.0, -1.0], [1.0, -1.0]]) - expected_std = np.array([[0.15811388300841894, 0.1], [0.15811388300841894, 0.0]]) + expected_std = np.array([[0.15811388300841894, 0.1], [0.15811388300841894, 0.0]]) / 2.0 self.assertTrue(np.allclose(processed, expected_avg)) self.assertTrue(np.allclose(error, expected_std)) @@ -390,8 +394,10 @@ def test_averaging_and_svd(self): # Test the x90p rotation processed, error = processor(self.data.data(2)) self.assertTrue(np.allclose(processed, np.array([0, 0]))) - self.assertIsNone(error) + self.assertTrue(np.allclose(error, np.array([0.5, 0.5]))) # Test the x45p rotation processed, error = processor(self.data.data(3)) - self.assertTrue(np.allclose(processed, np.array([0.5, -0.5])/np.sqrt(2.))) + expected_std = np.array([np.std([1, 1, 1, -1, 1, 1, 1, -1]) / np.sqrt(8.0)] * 2) + self.assertTrue(np.allclose(processed, np.array([0.5, -0.5]) / np.sqrt(2.0))) + self.assertTrue(np.allclose(error, expected_std)) diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index a01d3ee21b..46a26162ac 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -29,11 +29,6 @@ class TestSVD(BaseDataProcessorTest): """Test the SVD nodes.""" - def __init__(self): - """Init""" - self.iq_experiment = None - super().__init__() - def create_experiment(self, iq_data: List[Any], single_shot: bool = False): """Populate avg_iq_data to use it for testing. @@ -50,7 +45,7 @@ def create_experiment(self, iq_data: List[Any], single_shot: bool = False): meas_return="avg", data=ExperimentResultData(memory=circ_data), header=self.header, - shots=1024 + shots=1024, ) results.append(res) else: @@ -60,10 +55,11 @@ def create_experiment(self, iq_data: List[Any], single_shot: bool = False): meas_return="single", data=ExperimentResultData(memory=iq_data), header=self.header, - shots=1024 + shots=1024, ) results.append(res) + # pylint: disable=attribute-defined-outside-init self.iq_experiment = ExperimentData(FakeExperiment()) self.iq_experiment.add_data(Result(results=results, **self.base_result_args)) @@ -73,11 +69,7 @@ def test_simple_data(self): the IQ data of qubit 1 is oriented along (1,-1). """ - iq_data = [ - [[0., 0.], [0., 0.]], - [[1., 1.], [-1., 1.]], - [[-1., -1.], [1., -1.]] - ] + iq_data = [[[0.0, 0.0], [0.0, 0.0]], [[1.0, 1.0], [-1.0, 1.0]], [[-1.0, -1.0], [1.0, -1.0]]] self.create_experiment(iq_data) @@ -85,21 +77,21 @@ def test_simple_data(self): iq_svd.train([datum["memory"] for datum in self.iq_experiment.data()]) # qubit 0 IQ data is oriented along (1,1) - self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([-1,-1]) / np.sqrt(2))) + self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([-1, -1]) / np.sqrt(2))) # qubit 1 IQ data is oriented along (1, -1) self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([-1, 1]) / np.sqrt(2))) - processed, _ = iq_svd(np.array([[1,1], [1, -1]])) - expected = np.array([-1,-1])/np.sqrt(2) + processed, _ = iq_svd(np.array([[1, 1], [1, -1]])) + expected = np.array([-1, -1]) / np.sqrt(2) self.assertTrue(np.allclose(processed, expected)) - processed, _ = iq_svd(np.array([[2,2], [2, -2]])) - self.assertTrue(np.allclose(processed, expected*2)) + processed, _ = iq_svd(np.array([[2, 2], [2, -2]])) + self.assertTrue(np.allclose(processed, expected * 2)) # Check that orthogonal data gives 0. processed, _ = iq_svd(np.array([[1, -1], [1, 1]])) - expected = np.array([0,0]) + expected = np.array([0, 0]) self.assertTrue(np.allclose(processed, expected)) def test_svd(self): @@ -109,16 +101,16 @@ def test_svd(self): # The is a large offset in the imaginary dimension when comparing qubits # 0 and 1. iq_data = [ - [[-6.20601501e+14, -1.33257051e+15], [-1.70921324e+15, -4.05881657e+15]], - [[-5.80546502e+14, -1.33492509e+15], [-1.65094637e+15, -4.05926942e+15]], - [[-4.04649069e+14, -1.33191056e+15], [-1.29680377e+15, -4.03604815e+15]], - [[-2.22203874e+14, -1.30291309e+15], [-8.57663429e+14, -3.97784973e+15]], - [[-2.92074029e+13, -1.28578530e+15], [-9.78824053e+13, -3.92071056e+15]], - [[1.98056981e+14, -1.26883024e+15], [3.77157017e+14, -3.87460328e+15]], - [[4.29955888e+14, -1.25022995e+15], [1.02340118e+15, -3.79508679e+15]], - [[6.38981344e+14, -1.25084614e+15], [1.68918514e+15, -3.78961044e+15]], - [[7.09988897e+14, -1.21906634e+15], [1.91914171e+15, -3.73670664e+15]], - [[7.63169115e+14, -1.20797552e+15], [2.03772603e+15, -3.74653863e+15]] + [[-6.20601501e14, -1.33257051e15], [-1.70921324e15, -4.05881657e15]], + [[-5.80546502e14, -1.33492509e15], [-1.65094637e15, -4.05926942e15]], + [[-4.04649069e14, -1.33191056e15], [-1.29680377e15, -4.03604815e15]], + [[-2.22203874e14, -1.30291309e15], [-8.57663429e14, -3.97784973e15]], + [[-2.92074029e13, -1.28578530e15], [-9.78824053e13, -3.92071056e15]], + [[1.98056981e14, -1.26883024e15], [3.77157017e14, -3.87460328e15]], + [[4.29955888e14, -1.25022995e15], [1.02340118e15, -3.79508679e15]], + [[6.38981344e14, -1.25084614e15], [1.68918514e15, -3.78961044e15]], + [[7.09988897e14, -1.21906634e15], [1.91914171e15, -3.73670664e15]], + [[7.63169115e14, -1.20797552e15], [2.03772603e15, -3.74653863e15]], ] self.create_experiment(iq_data) @@ -129,6 +121,33 @@ def test_svd(self): self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([-0.99633018, -0.08559302]))) self.assertTrue(np.allclose(iq_svd._main_axes[1], np.array([-0.99627747, -0.0862044]))) + def test_svd_error(self): + """Test the error formula of the SVD.""" + + iq_svd = SVDAvg() + iq_svd._main_axes = np.array([[1.0, 0.0]]) + iq_svd._scales = [1.0] + iq_svd._means = [[0.0, 0.0]] + + # Since the axis is along the real part the imaginary error is irrelevant. + processed, error = iq_svd([[1.0, 0.2]], [[0.2, 0.1]]) + self.assertEqual(processed, np.array([1.0])) + self.assertEqual(error, np.array([0.2])) + + # Since the axis is along the real part the imaginary error is irrelevant. + processed, error = iq_svd([[1.0, 0.2]], [[0.2, 0.3]]) + self.assertEqual(processed, np.array([1.0])) + self.assertEqual(error, np.array([0.2])) + + # Title the axis to an angle of 36.9... degrees + iq_svd._main_axes = np.array([[0.8, 0.6]]) + processed, error = iq_svd([[1.0, 0.0]], [[0.2, 0.3]]) + cos_ = np.cos(np.arctan(0.6 / 0.8)) + sin_ = np.sin(np.arctan(0.6 / 0.8)) + self.assertEqual(processed, np.array([cos_])) + expected_error = np.sqrt((0.2 * cos_) ** 2 + (0.3 * sin_) ** 2) + self.assertEqual(error, np.array([expected_error])) + def test_train_svd_processor(self): """Test that we can train a DataProcessor with an SVD.""" @@ -136,11 +155,7 @@ def test_train_svd_processor(self): self.assertFalse(processor.is_trained) - iq_data = [ - [[0., 0.], [0., 0.]], - [[1., 1.], [-1., 1.]], - [[-1., -1.], [1., -1.]] - ] + iq_data = [[[0.0, 0.0], [0.0, 0.0]], [[1.0, 1.0], [-1.0, 1.0]], [[-1.0, -1.0], [1.0, -1.0]]] self.create_experiment(iq_data) processor.train(self.iq_experiment.data()) @@ -148,9 +163,7 @@ def test_train_svd_processor(self): self.assertTrue(processor.is_trained) # Check that we can use the SVD - iq_data = [ - [[2, 2], [2, -2]] - ] + iq_data = [[[2, 2], [2, -2]]] self.create_experiment(iq_data) processed, _ = processor(self.iq_experiment.data(0)) @@ -161,16 +174,16 @@ def test_iq_averaging(self): """Test averaging of IQ-data.""" iq_data = [ - [[-6.20601501e+14, -1.33257051e+15], [-1.70921324e+15, -4.05881657e+15]], - [[-5.80546502e+14, -1.33492509e+15], [-1.65094637e+15, -4.05926942e+15]], - [[-4.04649069e+14, -1.33191056e+15], [-1.29680377e+15, -4.03604815e+15]], - [[-2.22203874e+14, -1.30291309e+15], [-8.57663429e+14, -3.97784973e+15]], - [[-2.92074029e+13, -1.28578530e+15], [-9.78824053e+13, -3.92071056e+15]], - [[1.98056981e+14, -1.26883024e+15], [3.77157017e+14, -3.87460328e+15]], - [[4.29955888e+14, -1.25022995e+15], [1.02340118e+15, -3.79508679e+15]], - [[6.38981344e+14, -1.25084614e+15], [1.68918514e+15, -3.78961044e+15]], - [[7.09988897e+14, -1.21906634e+15], [1.91914171e+15, -3.73670664e+15]], - [[7.63169115e+14, -1.20797552e+15], [2.03772603e+15, -3.74653863e+15]] + [[-6.20601501e14, -1.33257051e15], [-1.70921324e15, -4.05881657e15]], + [[-5.80546502e14, -1.33492509e15], [-1.65094637e15, -4.05926942e15]], + [[-4.04649069e14, -1.33191056e15], [-1.29680377e15, -4.03604815e15]], + [[-2.22203874e14, -1.30291309e15], [-8.57663429e14, -3.97784973e15]], + [[-2.92074029e13, -1.28578530e15], [-9.78824053e13, -3.92071056e15]], + [[1.98056981e14, -1.26883024e15], [3.77157017e14, -3.87460328e15]], + [[4.29955888e14, -1.25022995e15], [1.02340118e15, -3.79508679e15]], + [[6.38981344e14, -1.25084614e15], [1.68918514e15, -3.78961044e15]], + [[7.09988897e14, -1.21906634e15], [1.91914171e15, -3.73670664e15]], + [[7.63169115e14, -1.20797552e15], [2.03772603e15, -3.74653863e15]], ] self.create_experiment(iq_data, single_shot=True) @@ -179,8 +192,11 @@ def test_iq_averaging(self): avg_datum, error = avg_iq(self.iq_experiment.data(0)["memory"]) - expected_avg = np.array([[8.82943876e+13, -1.27850527e+15], [ 1.43410186e+14, -3.89952402e+15]]) - expected_std = np.array([[5.07650185e+14, 4.44664719e+13], [1.40522641e+15, 1.22326831e+14]]) + expected_avg = np.array([[8.82943876e13, -1.27850527e15], [1.43410186e14, -3.89952402e15]]) + + expected_std = np.array( + [[5.07650185e14, 4.44664719e13], [1.40522641e15, 1.22326831e14]] + ) / np.sqrt(10) self.assertTrue(np.allclose(avg_datum, expected_avg)) self.assertTrue(np.allclose(error, expected_std)) From c470fd63eaae508e860977180a650ed5511d7360 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 14 May 2021 16:40:57 +0200 Subject: [PATCH 06/22] * Added call to super().setUp() --- test/data_processing/fake_experiment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/data_processing/fake_experiment.py b/test/data_processing/fake_experiment.py index 99567d5f36..03f96d12f8 100644 --- a/test/data_processing/fake_experiment.py +++ b/test/data_processing/fake_experiment.py @@ -35,6 +35,7 @@ class BaseDataProcessorTest(QiskitTestCase): def setUp(self): """Define variables needed for most tests.""" + super().setUp() self.base_result_args = dict( backend_name="test_backend", From 75d83d028a9a471b8ddd3cb0fdc013a1e85645fc Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 14 May 2021 16:44:33 +0200 Subject: [PATCH 07/22] * Removed redundent setUp --- test/data_processing/test_data_processing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 4ad41c4537..e938541f03 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -67,8 +67,6 @@ def setUp(self): self.exp_data_lvl2 = ExperimentData(FakeExperiment()) self.exp_data_lvl2.add_data(Result(results=[res], **self.base_result_args)) - super().setUp() - def test_empty_processor(self): """Check that a DataProcessor without steps does nothing.""" data_processor = DataProcessor("counts") From 35f903f477a9a1b88126ed23747bbebf5a1b0ee5 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 16 May 2021 10:15:07 +0200 Subject: [PATCH 08/22] * Made the averaging node independent of IQData. --- qiskit_experiments/data_processing/nodes.py | 66 +++++++++++++------- test/data_processing/test_data_processing.py | 6 +- test/data_processing/test_nodes.py | 22 ++++++- 3 files changed, 68 insertions(+), 26 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index b0969764a5..f9e767c29e 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -20,6 +20,51 @@ from qiskit_experiments.data_processing.exceptions import DataProcessorError +class AverageData(DataAction): + """A node to average data which can be represented as numpy arrays.""" + + def __init__(self, axis: int = 0): + """Initialize a data averaging node. + + Args: + axis: The axis along which to average the data. If not given 0 is the + default axis. + """ + super().__init__() + self._axis = axis + + def _format_data(self, datum: Any, error: Optional[Any] = None): + """Format the data into numpy arrays.""" + datum = np.asarray(datum, dtype=float) + + if error is not None: + error = np.asarray(error, dtype=float) + + return datum, error + + def _process( + self, datum: np.array, error: Optional[np.array] = None, **options + ) -> Tuple[np.array, np.array]: + """Average the data. + + Args: + datum: an array of data. + options: keyword arguments which, if they contain "axis" then this axis + will be used to override the default axis set at construction time. + + Returns: + Two arrays with one less dimension than the given datum and error. The error + is the standard error of the mean, i.e. the standard deviation of the datum + divided by :math:`\sqrt{N}` where :math:`N` is the number of data points. + """ + axis = options.get("axis", self._axis) + + if not isinstance(axis, int): + raise DataProcessorError(f"Axis must be int, received {axis}.") + + return np.average(datum, axis=axis), np.std(datum, axis=axis) / np.sqrt(datum.shape[0]) + + class IQPart(DataAction): """Abstract class for IQ data post-processing.""" @@ -89,27 +134,6 @@ def __repr__(self): return f"{self.__class__.__name__}(validate: {self._validate}, scale: {self.scale})" -class AverageIQData(IQPart): - """A node that averages single-shot data to create averaged IQ data.""" - - def _required_dimension(self) -> int: - """Require memory to be a 2D array.""" - return 3 - - def _process( - self, datum: np.array, error: Optional[np.array] = None, **options - ) -> Tuple[np.array, np.array]: - """Average the single-shot IQ data. - - Args: - datum: A 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. - - Returns: - A 2D array of qubits and complex averaged IQ points as [real, imaginary]. - """ - return np.average(datum, axis=0), np.std(datum, axis=0) / np.sqrt(datum.shape[0]) - - class SVDAvg(IQPart): """Singular Value Decomposition of averaged IQ data.""" diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index e938541f03..00fd548321 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -23,7 +23,7 @@ from qiskit_experiments.data_processing.data_processor import DataProcessor from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.data_processing.nodes import ( - AverageIQData, + AverageData, SVDAvg, ToReal, ToRealAvg, @@ -363,7 +363,7 @@ def setUp(self): def test_averaging(self): """Test that averaging of the datums produces the expected IQ points.""" - processor = DataProcessor("memory", [AverageIQData()]) + processor = DataProcessor("memory", [AverageData()]) # Test that we get the expected outcome for the excited state processed, error = processor(self.data.data(0)) @@ -382,7 +382,7 @@ def test_averaging(self): def test_averaging_and_svd(self): """Test averaging followed by a SVD.""" - processor = DataProcessor("memory", [AverageIQData(), SVDAvg()]) + processor = DataProcessor("memory", [AverageData(), SVDAvg()]) # Test training using the calibration points self.assertFalse(processor.is_trained) diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 46a26162ac..f32119054c 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -21,11 +21,29 @@ from qiskit.result.models import ExperimentResultData, ExperimentResult from qiskit.result import Result +from qiskit.test import QiskitTestCase from qiskit_experiments.experiment_data import ExperimentData -from qiskit_experiments.data_processing.nodes import SVDAvg, AverageIQData +from qiskit_experiments.data_processing.nodes import SVDAvg, AverageData from qiskit_experiments.data_processing.data_processor import DataProcessor +class TestAveraging(QiskitTestCase): + """Test the averaging nodes.""" + + def test_simple(self): + """Simple test of averaging.""" + + datum = np.array([[1,2], [3, 4]]) + + node = AverageData(axis=1) + self.assertTrue(np.allclose(node(datum)[0], np.array([1.5, 3.5]))) + self.assertTrue(np.allclose(node(datum)[1], np.array([0.5, 0.5])/np.sqrt(2))) + + node = AverageData(axis=0) + self.assertTrue(np.allclose(node(datum)[0], np.array([2.0, 3.0]))) + self.assertTrue(np.allclose(node(datum)[1], np.array([1.0, 1.0])/np.sqrt(2))) + + class TestSVD(BaseDataProcessorTest): """Test the SVD nodes.""" @@ -188,7 +206,7 @@ def test_iq_averaging(self): self.create_experiment(iq_data, single_shot=True) - avg_iq = AverageIQData() + avg_iq = AverageData() avg_datum, error = avg_iq(self.iq_experiment.data(0)["memory"]) From 4cf97ab1f5de114b36cc2d57cdbd556889fd0834 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 16 May 2021 17:28:20 +0200 Subject: [PATCH 09/22] * Docstring. --- qiskit_experiments/data_processing/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index f9e767c29e..e47f61986a 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -21,7 +21,7 @@ class AverageData(DataAction): - """A node to average data which can be represented as numpy arrays.""" + """A node to average data representable as numpy arrays.""" def __init__(self, axis: int = 0): """Initialize a data averaging node. From ce614c3ec86ac32023511bbcea72786f623f0171 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 16 May 2021 18:51:15 +0200 Subject: [PATCH 10/22] * Black --- test/data_processing/test_nodes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index f32119054c..792330b2db 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -33,15 +33,15 @@ class TestAveraging(QiskitTestCase): def test_simple(self): """Simple test of averaging.""" - datum = np.array([[1,2], [3, 4]]) + datum = np.array([[1, 2], [3, 4]]) node = AverageData(axis=1) self.assertTrue(np.allclose(node(datum)[0], np.array([1.5, 3.5]))) - self.assertTrue(np.allclose(node(datum)[1], np.array([0.5, 0.5])/np.sqrt(2))) + self.assertTrue(np.allclose(node(datum)[1], np.array([0.5, 0.5]) / np.sqrt(2))) node = AverageData(axis=0) self.assertTrue(np.allclose(node(datum)[0], np.array([2.0, 3.0]))) - self.assertTrue(np.allclose(node(datum)[1], np.array([1.0, 1.0])/np.sqrt(2))) + self.assertTrue(np.allclose(node(datum)[1], np.array([1.0, 1.0]) / np.sqrt(2))) class TestSVD(BaseDataProcessorTest): From 76d72b7823b4ba3f182fac6d816a870c10131785 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 16 May 2021 21:19:40 +0200 Subject: [PATCH 11/22] * Lint --- qiskit_experiments/data_processing/nodes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index e47f61986a..09519f5154 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -55,7 +55,10 @@ def _process( Returns: Two arrays with one less dimension than the given datum and error. The error is the standard error of the mean, i.e. the standard deviation of the datum - divided by :math:`\sqrt{N}` where :math:`N` is the number of data points. + divided by :math:`sqrt{N}` where :math:`N` is the number of data points. + + Raises: + DataProcessorError: If the axis is not an int. """ axis = options.get("axis", self._axis) From 77e4a8ed756472383612ec8058965ec3fbd3dc19 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 16 May 2021 21:29:54 +0200 Subject: [PATCH 12/22] * Black. --- qiskit_experiments/data_processing/nodes.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 09519f5154..8c90f224ef 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -47,18 +47,18 @@ def _process( ) -> Tuple[np.array, np.array]: """Average the data. - Args: - datum: an array of data. - options: keyword arguments which, if they contain "axis" then this axis - will be used to override the default axis set at construction time. + Args: + datum: an array of data. + options: keyword arguments which, if they contain "axis" then this axis + will be used to override the default axis set at construction time. - Returns: - Two arrays with one less dimension than the given datum and error. The error - is the standard error of the mean, i.e. the standard deviation of the datum - divided by :math:`sqrt{N}` where :math:`N` is the number of data points. + Returns: + Two arrays with one less dimension than the given datum and error. The error + is the standard error of the mean, i.e. the standard deviation of the datum + divided by :math:`sqrt{N}` where :math:`N` is the number of data points. - Raises: - DataProcessorError: If the axis is not an int. + Raises: + DataProcessorError: If the axis is not an int. """ axis = options.get("axis", self._axis) From ba72f0309dd74da92cdd88ae0ff931b05f0b302a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Sun, 16 May 2021 23:21:02 +0200 Subject: [PATCH 13/22] * Fix docstring. --- qiskit_experiments/data_processing/nodes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 8c90f224ef..7b918b1d1b 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -53,9 +53,9 @@ def _process( will be used to override the default axis set at construction time. Returns: - Two arrays with one less dimension than the given datum and error. The error - is the standard error of the mean, i.e. the standard deviation of the datum - divided by :math:`sqrt{N}` where :math:`N` is the number of data points. + Two arrays with one less dimension than the given datum and error. The error + is the standard error of the mean, i.e. the standard deviation of the datum + divided by :math:`sqrt{N}` where :math:`N` is the number of data points. Raises: DataProcessorError: If the axis is not an int. From 586432af114b669d611f88e57acb18d1916546aa Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Tue, 18 May 2021 07:25:06 +0200 Subject: [PATCH 14/22] Update qiskit_experiments/data_processing/data_processor.py Co-authored-by: Naoki Kanazawa --- qiskit_experiments/data_processing/data_processor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 34ad550508..1a114b7f60 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -57,11 +57,7 @@ def append(self, node: DataAction): @property def is_trained(self) -> bool: """Return True if all nodes of the data processor have been trained.""" - for node in self._nodes: - if not node.is_trained: - return False - - return True + return all(node.is_trained for node in self._nodes) def __call__(self, datum: Dict[str, Any], **options) -> Tuple[Any, Any]: """ From 88f0415382c46196a4d7ad7924325c87a251c8da Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 18 May 2021 08:53:57 +0200 Subject: [PATCH 15/22] * Made means a function to improve readability. --- qiskit_experiments/data_processing/nodes.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 7b918b1d1b..46f23a61e0 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -162,6 +162,22 @@ def axis(self) -> List[np.array]: raise DataProcessorError("SVD is not trained.") + def means(self, qubit: int, iq_index: int) -> float: + """Return the mean by which to correct the IQ data. + + Before training the SVD the mean of the training data is subtracted from the + training data to avoid large offsets in the data. These means can be retrieved + with this function. + + Args: + qubit: Index of the qubit. + iq_index: Index of either the in-phase (i.e. 0) or the quadrature (i.e. 1). + + Returns: + The mean that was determined during training for the given qubit and IQ index. + """ + return self._means[qubit][iq_index] + @property def scales(self) -> List[float]: """Return the scaling of the SVD.""" @@ -211,7 +227,9 @@ def _process( # process each averaged IQ point with its own axis. for idx in range(n_qubits): - centered = np.array([datum[idx][iq] - self._means[idx][iq] for iq in [0, 1]]) + centered = np.array( + [datum[idx][iq] - self.means(qubit=idx, iq_index=iq) for iq in [0, 1]] + ) processed_data.append((self._main_axes[idx] @ centered) / self._scales[idx]) From 9a252e2955dd7503e9684daa2230e7f5b9a3e0a6 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 18 May 2021 10:21:13 +0200 Subject: [PATCH 16/22] * Removed RealAvg and ImagAvg. --- qiskit_experiments/data_processing/nodes.py | 150 ++++++------------- test/data_processing/test_data_processing.py | 26 +--- test/data_processing/test_nodes.py | 10 +- 3 files changed, 58 insertions(+), 128 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 46f23a61e0..fa979a1565 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -13,7 +13,7 @@ """Different data analysis steps.""" from abc import abstractmethod -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Set import numpy as np from qiskit_experiments.data_processing.data_action import DataAction @@ -82,7 +82,7 @@ def __init__(self, scale: Optional[float] = None, validate: bool = True): @abstractmethod def _process(self, datum: np.array, error: Optional[np.array] = None, **options) -> np.array: - """Defines how the IQ point will be processed. + """Defines how the IQ point is processed. Args: datum: A 2D or a 3D array of complex IQ points as [real, imaginary]. @@ -94,7 +94,7 @@ def _process(self, datum: np.array, error: Optional[np.array] = None, **options) """ @abstractmethod - def _required_dimension(self) -> int: + def _required_dimension(self) -> Set[int]: """Return the required dimension of the data.""" def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: @@ -116,19 +116,26 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An if error is not None: error = np.asarray(error, dtype=float) - if self._validate and len(datum.shape) != self._required_dimension(): - raise DataProcessorError( - f"Single-shot data given to {self.__class__.__name__}" - f"must be a {self._required_dimension()}D array. Instead, a {len(datum.shape)}D " - f"array was given." - ) + if self._validate: + if len(datum.shape) not in self._required_dimension(): + raise DataProcessorError( + f"IQ data given to {self.__class__.__name__} must be an N dimensional" + f"array with N in {self._required_dimension()}. Instead, a {len(datum.shape)}D " + f"array was given." + ) - if error is not None and self._validate and len(error.shape) != self._required_dimension(): - raise DataProcessorError( - f"Erorr on single-shot data given to {self.__class__.__name__}" - f"must be a {self._required_dimension()}D array. Instead, a {len(error.shape)}D " - f"array was given." - ) + if error is not None and len(error.shape) not in self._required_dimension(): + raise DataProcessorError( + f"IQ data error given to {self.__class__.__name__} must be an N dimensional" + f"array with N in {self._required_dimension()}. Instead, a {len(error.shape)}D" + f" array was given." + ) + + if error is not None and len(error.shape) != len(datum.shape): + raise DataProcessorError( + "Datum and error do not have the same shape: " + f"{len(datum.shape)} != {len(error.shape)}." + ) return datum, error @@ -137,7 +144,7 @@ def __repr__(self): return f"{self.__class__.__name__}(validate: {self._validate}, scale: {self.scale})" -class SVDAvg(IQPart): +class SVD(IQPart): """Singular Value Decomposition of averaged IQ data.""" def __init__(self, validate: bool = True): @@ -150,9 +157,9 @@ def __init__(self, validate: bool = True): self._means = None self._scales = None - def _required_dimension(self) -> int: + def _required_dimension(self) -> Set[int]: """Require memory to be a 2D array.""" - return 2 + return {2} @property def axis(self) -> List[np.array]: @@ -285,9 +292,9 @@ def train(self, data: List[Any]): class ToReal(IQPart): """IQ data post-processing. Isolate the real part of single-shot IQ data.""" - def _required_dimension(self) -> int: - """Require memory to be a 3D array.""" - return 3 + def _required_dimension(self) -> Set[int]: + """Require memory to be a 2D or a 3D array.""" + return {2, 3} def _process( self, datum: np.array, error: Optional[np.array] = None, **options @@ -295,120 +302,53 @@ def _process( """Take the real part of the IQ data. Args: - datum: A 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. - error: An optional 3D array of shots, qubits, and an error on a complex IQ point + datum: A 2D or 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. + error: An optional 2D or 3D array of shots, qubits, and an error on a complex IQ point as [real, imaginary]. Returns: - A 2D array of shots, qubits with the associated error if given. Each entry is the - real part of the given IQ data. - """ - if self.scale is None: - if error is not None: - return datum[:, :, 0], error[:, :, 0] - else: - return datum[:, :, 0], None - - if error is not None: - return datum[:, :, 0] * self.scale, error[:, :, 0] * self.scale - else: - return datum[:, :, 0] * self.scale, None - - -class ToRealAvg(IQPart): - """IQ data post-processing. Isolate the real part of averaged IQ data.""" - - def _required_dimension(self) -> int: - """Require memory to be a 2D array.""" - return 2 - - def _process( - self, datum: np.array, error: Optional[np.array] = None, **options - ) -> Tuple[np.array, np.array]: - """Take the real part of the IQ data. - - Args: - datum: A 2D array of qubits, and a complex averaged IQ point as [real, imaginary]. - error: An optional 2D array of qubits, and an error on a complex averaged IQ - point as [real, imaginary]. - - Returns: - A 1D array of qubit IQ points with the associated error if given. Each entry is the - real part of the averaged IQ data of a qubit. + A 1D or 2D array, each entry is the real part of the given IQ data and error. """ if self.scale is None: if error is not None: - return datum[:, 0], error[:, 0] + return datum[..., 0], error[..., 0] else: - return datum[:, 0], None + return datum[..., 0], None if error is not None: - return datum[:, 0] * self.scale, error[:, 0] * self.scale + return datum[..., 0] * self.scale, error[..., 0] * self.scale else: - return datum[:, 0] * self.scale, None + return datum[..., 0] * self.scale, None class ToImag(IQPart): """IQ data post-processing. Isolate the imaginary part of single-shot IQ data.""" - def _required_dimension(self) -> int: - """Require memory to be a 3D array.""" - return 3 + def _required_dimension(self) -> Set[int]: + """Require memory to be a 2D or a 3D array.""" + return {2, 3} def _process(self, datum: np.array, error: Optional[np.array] = None, **options) -> np.array: """Take the imaginary part of the IQ data. Args: - datum: A 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. - error: An optional 3D array of shots, qubits, and an error on a complex IQ point - as [real, imaginary]. - - Returns: - A 2D array of shots, qubits. Each entry is the imaginary part of the given IQ data. - """ - if self.scale is None: - if error is not None: - return datum[:, :, 1], error[:, :, 1] - else: - return datum[:, :, 1], None - - if error is not None: - return datum[:, :, 1] * self.scale, error[:, :, 1] * self.scale - else: - return datum[:, :, 1] * self.scale, None - - -class ToImagAvg(IQPart): - """IQ data post-processing. Isolate the imaginary part of averaged IQ data.""" - - def _required_dimension(self) -> int: - """Require memory to be a 2D array.""" - return 2 - - def _process( - self, datum: np.array, error: Optional[np.array] = None, **options - ) -> Tuple[np.array, np.array]: - """Take the imaginary part of the IQ data. - - Args: - datum: A 2D array of qubits, and a complex averaged IQ point as [real, imaginary]. - error: An optional 3D array of shots, qubits, and an error on a complex IQ point + datum: A 2D or 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. + error: An optional 2D or 3D array of shots, qubits, and an error on a complex IQ point as [real, imaginary]. Returns: - A 2D array of shots, qubits with the associated error if given. Each entry is the - imaginary part of the given IQ data. + A 1D or 2D array, each entry is the imaginary part of the given IQ data and error. """ if self.scale is None: if error is not None: - return datum[:, 1], error[:, 1] + return datum[..., 1], error[..., 1] else: - return datum[:, 1], None + return datum[..., 1], None if error is not None: - return datum[:, 1] * self.scale, error[:, 1] * self.scale + return datum[..., 1] * self.scale, error[..., 1] * self.scale else: - return datum[:, 1] * self.scale, None + return datum[..., 1] * self.scale, None class Probability(DataAction): diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 00fd548321..e97128e5ae 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -24,11 +24,9 @@ from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.data_processing.nodes import ( AverageData, - SVDAvg, + SVD, ToReal, - ToRealAvg, ToImag, - ToImagAvg, Probability, ) @@ -223,13 +221,11 @@ def setUp(self): def test_avg_and_single(self): """Test that the different nodes process the data correctly.""" - real_single = DataProcessor("memory", [ToReal(scale=1)]) - imag_single = DataProcessor("memory", [ToImag(scale=1)]) - real_avg = DataProcessor("memory", [ToRealAvg(scale=1)]) - imag_avg = DataProcessor("memory", [ToImagAvg(scale=1)]) + to_real = DataProcessor("memory", [ToReal(scale=1)]) + to_imag = DataProcessor("memory", [ToImag(scale=1)]) # Test the real single shot node - new_data, error = real_single(self.exp_data_single.data(0)) + new_data, error = to_real(self.exp_data_single.data(0)) expected = np.array( [ [-56470872.0, -53407256.0], @@ -243,11 +239,8 @@ def test_avg_and_single(self): self.assertTrue(np.allclose(new_data, expected)) self.assertIsNone(error) - with self.assertRaises(DataProcessorError): - real_single(self.exp_data_avg.data(0)) - # Test the imaginary single shot node - new_data, error = imag_single(self.exp_data_single.data(0)) + new_data, error = to_imag(self.exp_data_single.data(0)) expected = np.array( [ [-136691568.0, -176278624.0], @@ -261,16 +254,13 @@ def test_avg_and_single(self): self.assertTrue(np.allclose(new_data, expected)) # Test the real average node - new_data, error = real_avg(self.exp_data_avg.data(0)) + new_data, error = to_real(self.exp_data_avg.data(0)) self.assertTrue(np.allclose(new_data, np.array([-539698.0, 5541283.0]))) # Test the imaginary average node - new_data, error = imag_avg(self.exp_data_avg.data(0)) + new_data, error = to_imag(self.exp_data_avg.data(0)) self.assertTrue(np.allclose(new_data, np.array([-153030784.0, -160369600.0]))) - with self.assertRaises(DataProcessorError): - real_avg(self.exp_data_single.data(0)) - class TestAveragingAndSVD(BaseDataProcessorTest): """Test the averaging of single-shot IQ data followed by a SVD.""" @@ -382,7 +372,7 @@ def test_averaging(self): def test_averaging_and_svd(self): """Test averaging followed by a SVD.""" - processor = DataProcessor("memory", [AverageData(), SVDAvg()]) + processor = DataProcessor("memory", [AverageData(), SVD()]) # Test training using the calibration points self.assertFalse(processor.is_trained) diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 792330b2db..fb7a76cde2 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -23,7 +23,7 @@ from qiskit.result import Result from qiskit.test import QiskitTestCase from qiskit_experiments.experiment_data import ExperimentData -from qiskit_experiments.data_processing.nodes import SVDAvg, AverageData +from qiskit_experiments.data_processing.nodes import SVD, AverageData from qiskit_experiments.data_processing.data_processor import DataProcessor @@ -91,7 +91,7 @@ def test_simple_data(self): self.create_experiment(iq_data) - iq_svd = SVDAvg() + iq_svd = SVD() iq_svd.train([datum["memory"] for datum in self.iq_experiment.data()]) # qubit 0 IQ data is oriented along (1,1) @@ -133,7 +133,7 @@ def test_svd(self): self.create_experiment(iq_data) - iq_svd = SVDAvg() + iq_svd = SVD() iq_svd.train([datum["memory"] for datum in self.iq_experiment.data()]) self.assertTrue(np.allclose(iq_svd._main_axes[0], np.array([-0.99633018, -0.08559302]))) @@ -142,7 +142,7 @@ def test_svd(self): def test_svd_error(self): """Test the error formula of the SVD.""" - iq_svd = SVDAvg() + iq_svd = SVD() iq_svd._main_axes = np.array([[1.0, 0.0]]) iq_svd._scales = [1.0] iq_svd._means = [[0.0, 0.0]] @@ -169,7 +169,7 @@ def test_svd_error(self): def test_train_svd_processor(self): """Test that we can train a DataProcessor with an SVD.""" - processor = DataProcessor("memory", [SVDAvg()]) + processor = DataProcessor("memory", [SVD()]) self.assertFalse(processor.is_trained) From 74e928b327bed56bf148658d9b236f3387544514 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 18 May 2021 20:56:21 +0200 Subject: [PATCH 17/22] * Added TrainableDataAction as a subclass of DataAction. --- .../data_processing/__init__.py | 7 +- .../data_processing/data_action.py | 11 +- .../data_processing/data_processor.py | 24 ++- qiskit_experiments/data_processing/nodes.py | 156 +++++++++++------- 4 files changed, 120 insertions(+), 78 deletions(-) diff --git a/qiskit_experiments/data_processing/__init__.py b/qiskit_experiments/data_processing/__init__.py index ad7871477a..3489f694c8 100644 --- a/qiskit_experiments/data_processing/__init__.py +++ b/qiskit_experiments/data_processing/__init__.py @@ -23,6 +23,7 @@ DataProcessor DataAction + TrainableDataAction Data Processing Nodes @@ -33,13 +34,17 @@ Probability ToImag ToReal + SVD + AverageData """ -from .data_action import DataAction +from .data_action import DataAction, TrainableDataAction from .nodes import ( Probability, ToImag, ToReal, + SVD, + AverageData, ) from .data_processor import DataProcessor diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index d2f481b565..c96c343c5b 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -80,19 +80,22 @@ def __repr__(self): """String representation of the node.""" return f"{self.__class__.__name__}(validate={self._validate})" + +class TrainableDataAction(DataAction): + """A base class for data actions that need training.""" + @property + @abstractmethod def is_trained(self) -> bool: """Return False if the DataAction needs to be trained. - Subclasses can override this property to communicate if they have been trained. - By default all data actions are trained. DataActions that have a training - mechanism will have to override this property. + Subclasses must implement this property to communicate if they have been trained. Return: True if the data action has been trained. """ - return True + @abstractmethod def train(self, data: List[Any]): """Train a DataAction. diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 1a114b7f60..6126c120ff 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -14,7 +14,7 @@ from typing import Any, Dict, List, Set, Tuple, Union -from qiskit_experiments.data_processing.data_action import DataAction +from qiskit_experiments.data_processing.data_action import DataAction, TrainableDataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -57,7 +57,12 @@ def append(self, node: DataAction): @property def is_trained(self) -> bool: """Return True if all nodes of the data processor have been trained.""" - return all(node.is_trained for node in self._nodes) + for node in self._nodes: + if isinstance(node, TrainableDataAction): + if not node.is_trained: + return False + + return True def __call__(self, datum: Dict[str, Any], **options) -> Tuple[Any, Any]: """ @@ -158,10 +163,11 @@ def train(self, data: List[Dict[str, Any]]): """ for index, node in enumerate(self._nodes): - if not node.is_trained: - # Process the data up to the untrained node. - train_data = [] - for datum in data: - train_data.append(self._call_internal(datum, call_up_to_node=index)[0]) - - node.train(train_data) + if isinstance(node, TrainableDataAction): + if not node.is_trained: + # Process the data up to the untrained node. + train_data = [] + for datum in data: + train_data.append(self._call_internal(datum, call_up_to_node=index)[0]) + + node.train(train_data) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index fa979a1565..67aac87c6c 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -16,7 +16,7 @@ from typing import Any, Dict, List, Optional, Tuple, Set import numpy as np -from qiskit_experiments.data_processing.data_action import DataAction +from qiskit_experiments.data_processing.data_action import DataAction, TrainableDataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -68,42 +68,24 @@ def _process( return np.average(datum, axis=axis), np.std(datum, axis=axis) / np.sqrt(datum.shape[0]) -class IQPart(DataAction): - """Abstract class for IQ data post-processing.""" +class SVD(TrainableDataAction): + """Singular Value Decomposition of averaged IQ data.""" - def __init__(self, scale: Optional[float] = None, validate: bool = True): + def __init__(self, validate: bool = True): """ Args: - scale: Float with which to multiply the IQ data. validate: If set to False the DataAction will not validate its input. """ - self.scale = scale - super().__init__(validate) - - @abstractmethod - def _process(self, datum: np.array, error: Optional[np.array] = None, **options) -> np.array: - """Defines how the IQ point is processed. - - Args: - datum: A 2D or a 3D array of complex IQ points as [real, imaginary]. - error: A 2D or a 3D array of errors on complex IQ points as [real, imaginary]. - options: Keyword arguments passed through the data processor at run-time. - - Returns: - Processed IQ point and its associated error estimate. - """ - - @abstractmethod - def _required_dimension(self) -> Set[int]: - """Return the required dimension of the data.""" + super().__init__(validate=validate) + self._main_axes = None + self._means = None + self._scales = None def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: - """Check that the IQ data has the correct format and convert to numpy array. + """Check that the IQ data is 2D and convert it to a numpy array. Args: - datum: A single item of data which corresponds to single-shot IQ data. It's - dimension will depend on whether it is single-shot IQ data (three-dimensional) - or averaged IQ date (two-dimensional). + datum: A single item of data which corresponds to single-shot IQ data. Returns: datum and any error estimate as a numpy array. @@ -117,50 +99,20 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An error = np.asarray(error, dtype=float) if self._validate: - if len(datum.shape) not in self._required_dimension(): + if len(datum.shape) != 2: raise DataProcessorError( - f"IQ data given to {self.__class__.__name__} must be an N dimensional" - f"array with N in {self._required_dimension()}. Instead, a {len(datum.shape)}D " - f"array was given." + f"IQ data given to {self.__class__.__name__} must be an 2D array. " + f"Instead, a {len(datum.shape)}D array was given." ) - if error is not None and len(error.shape) not in self._required_dimension(): - raise DataProcessorError( - f"IQ data error given to {self.__class__.__name__} must be an N dimensional" - f"array with N in {self._required_dimension()}. Instead, a {len(error.shape)}D" - f" array was given." - ) - - if error is not None and len(error.shape) != len(datum.shape): + if error is not None and len(error.shape) != 2: raise DataProcessorError( - "Datum and error do not have the same shape: " - f"{len(datum.shape)} != {len(error.shape)}." + f"IQ data error given to {self.__class__.__name__} must be an 2D array." + f"Instead, a {len(error.shape)}D array was given." ) return datum, error - def __repr__(self): - """String representation of the node.""" - return f"{self.__class__.__name__}(validate: {self._validate}, scale: {self.scale})" - - -class SVD(IQPart): - """Singular Value Decomposition of averaged IQ data.""" - - def __init__(self, validate: bool = True): - """ - Args: - validate: If set to False the DataAction will not validate its input. - """ - super().__init__(validate=validate) - self._main_axes = None - self._means = None - self._scales = None - - def _required_dimension(self) -> Set[int]: - """Require memory to be a 2D array.""" - return {2} - @property def axis(self) -> List[np.array]: """Return the axis of the trained SVD""" @@ -289,6 +241,82 @@ def train(self, data: List[Any]): self._scales.append(mat_s[0]) +class IQPart(DataAction): + """Abstract class for IQ data post-processing.""" + + def __init__(self, scale: Optional[float] = None, validate: bool = True): + """ + Args: + scale: Float with which to multiply the IQ data. + validate: If set to False the DataAction will not validate its input. + """ + self.scale = scale + super().__init__(validate) + + @abstractmethod + def _process(self, datum: np.array, error: Optional[np.array] = None, **options) -> np.array: + """Defines how the IQ point is processed. + + Args: + datum: A 2D or a 3D array of complex IQ points as [real, imaginary]. + error: A 2D or a 3D array of errors on complex IQ points as [real, imaginary]. + options: Keyword arguments passed through the data processor at run-time. + + Returns: + Processed IQ point and its associated error estimate. + """ + + @abstractmethod + def _required_dimension(self) -> Set[int]: + """Return the required dimension of the data.""" + + def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: + """Check that the IQ data has the correct format and convert to numpy array. + + Args: + datum: A single item of data which corresponds to single-shot IQ data. It's + dimension will depend on whether it is single-shot IQ data (three-dimensional) + or averaged IQ date (two-dimensional). + + Returns: + datum and any error estimate as a numpy array. + + Raises: + DataProcessorError: If the datum does not have the correct format. + """ + datum = np.asarray(datum, dtype=float) + + if error is not None: + error = np.asarray(error, dtype=float) + + if self._validate: + if len(datum.shape) not in self._required_dimension(): + raise DataProcessorError( + f"IQ data given to {self.__class__.__name__} must be an N dimensional" + f"array with N in {self._required_dimension()}. Instead, a {len(datum.shape)}D " + f"array was given." + ) + + if error is not None and len(error.shape) not in self._required_dimension(): + raise DataProcessorError( + f"IQ data error given to {self.__class__.__name__} must be an N dimensional" + f"array with N in {self._required_dimension()}. Instead, a {len(error.shape)}D" + f" array was given." + ) + + if error is not None and len(error.shape) != len(datum.shape): + raise DataProcessorError( + "Datum and error do not have the same shape: " + f"{len(datum.shape)} != {len(error.shape)}." + ) + + return datum, error + + def __repr__(self): + """String representation of the node.""" + return f"{self.__class__.__name__}(validate: {self._validate}, scale: {self.scale})" + + class ToReal(IQPart): """IQ data post-processing. Isolate the real part of single-shot IQ data.""" From 24e0d3b7d707e62f627df28217c11f2ae3aff0da Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 18 May 2021 21:24:23 +0200 Subject: [PATCH 18/22] * Removed _required_dimension. --- qiskit_experiments/data_processing/nodes.py | 22 ++++----------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 67aac87c6c..12edb34ea6 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -266,10 +266,6 @@ def _process(self, datum: np.array, error: Optional[np.array] = None, **options) Processed IQ point and its associated error estimate. """ - @abstractmethod - def _required_dimension(self) -> Set[int]: - """Return the required dimension of the data.""" - def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: """Check that the IQ data has the correct format and convert to numpy array. @@ -290,18 +286,16 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An error = np.asarray(error, dtype=float) if self._validate: - if len(datum.shape) not in self._required_dimension(): + if len(datum.shape) not in {2, 3}: raise DataProcessorError( f"IQ data given to {self.__class__.__name__} must be an N dimensional" - f"array with N in {self._required_dimension()}. Instead, a {len(datum.shape)}D " - f"array was given." + f"array with N in (2, 3). Instead, a {len(datum.shape)}D array was given." ) - if error is not None and len(error.shape) not in self._required_dimension(): + if error is not None and len(error.shape) not in {2, 3}: raise DataProcessorError( f"IQ data error given to {self.__class__.__name__} must be an N dimensional" - f"array with N in {self._required_dimension()}. Instead, a {len(error.shape)}D" - f" array was given." + f"array with N in (2, 3). Instead, a {len(error.shape)}D array was given." ) if error is not None and len(error.shape) != len(datum.shape): @@ -320,10 +314,6 @@ def __repr__(self): class ToReal(IQPart): """IQ data post-processing. Isolate the real part of single-shot IQ data.""" - def _required_dimension(self) -> Set[int]: - """Require memory to be a 2D or a 3D array.""" - return {2, 3} - def _process( self, datum: np.array, error: Optional[np.array] = None, **options ) -> Tuple[np.array, np.array]: @@ -352,10 +342,6 @@ def _process( class ToImag(IQPart): """IQ data post-processing. Isolate the imaginary part of single-shot IQ data.""" - def _required_dimension(self) -> Set[int]: - """Require memory to be a 2D or a 3D array.""" - return {2, 3} - def _process(self, datum: np.array, error: Optional[np.array] = None, **options) -> np.array: """Take the imaginary part of the IQ data. From e7884bcda49c1b462cb86854d43b9a02b9ffe00a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 18 May 2021 23:03:08 +0200 Subject: [PATCH 19/22] * Removed **options from data processing. --- .../data_processing/data_action.py | 8 ++--- .../data_processing/data_processor.py | 9 ++---- qiskit_experiments/data_processing/nodes.py | 29 +++++++------------ test/data_processing/test_data_processing.py | 5 ---- 4 files changed, 16 insertions(+), 35 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index c96c343c5b..9427bd508a 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -30,14 +30,13 @@ def __init__(self, validate: bool = True): self._validate = validate @abstractmethod - def _process(self, datum: Any, error: Optional[Any] = None, **options) -> Tuple[Any, Any]: + def _process(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: """ Applies the data processing step to the datum. Args: datum: A single item of data which will be processed. error: An optional error estimation on the datum that can be further propagated. - options: Keyword arguments passed through the data processor at run-time. Returns: processed data: The data that has been processed along with the propagated error. @@ -61,20 +60,19 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An DataProcessorError: If either the data or the error do not have the proper format. """ - def __call__(self, data: Any, error: Optional[Any] = None, **options) -> Tuple[Any, Any]: + def __call__(self, data: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: """Call the data action of this node on the data and propagate the error. Args: data: The data to process. The action nodes in the data processor will raise errors if the data does not have the appropriate format. error: An optional error estimation on the datum that can be further processed. - options: Keyword arguments passed through the data processor at run-time. Returns: processed data: The data processed by self as a tuple of processed datum and optionally the propagated error estimate. """ - return self._process(*self._format_data(data, error), **options) + return self._process(*self._format_data(data, error)) def __repr__(self): """String representation of the node.""" diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 6126c120ff..133813cc4e 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -80,7 +80,7 @@ def __call__(self, datum: Dict[str, Any], **options) -> Tuple[Any, Any]: return self._call_internal(datum, **options) def call_with_history( - self, datum: Dict[str, Any], history_nodes: Set = None, **options + self, datum: Dict[str, Any], history_nodes: Set = None ) -> Tuple[Any, Any, List]: """ Call self on the given datum. This method sequentially calls the stored data actions @@ -92,13 +92,12 @@ def call_with_history( history_nodes: The nodes, specified by index in the data processing chain, to include in the history. If None is given then all nodes will be included in the history. - options: Run-time options given as key word arguments that will be passed to the nodes. Returns: processed data: The datum processed by the data processor. history: The datum processed at each node of the data processor. """ - return self._call_internal(datum, True, history_nodes, **options) + return self._call_internal(datum, True, history_nodes) def _call_internal( self, @@ -106,7 +105,6 @@ def _call_internal( with_history: bool = False, history_nodes: Set = None, call_up_to_node: int = None, - **options, ) -> Union[Tuple[Any, Any], Tuple[Any, Any, List]]: """Process the data with or without storing the history of the computation. @@ -120,7 +118,6 @@ def _call_internal( call_up_to_node: The data processor will use each node in the processing chain up to the node indexed by call_up_to_node. If this variable is not specified then all nodes in the data processing chain will be called. - options: Run-time options given as keyword arguments that will be passed to the nodes. Returns: datum_ and history if with_history is True or datum_ if with_history is False. @@ -143,7 +140,7 @@ def _call_internal( for index, node in enumerate(self._nodes): if index < call_up_to_node: - datum_, error_ = node(datum_, error_, **options) + datum_, error_ = node(datum_, error_) if with_history and ( history_nodes is None or (history_nodes and index in history_nodes) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 12edb34ea6..38e23c26fa 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -13,7 +13,7 @@ """Different data analysis steps.""" from abc import abstractmethod -from typing import Any, Dict, List, Optional, Tuple, Set +from typing import Any, Dict, List, Optional, Tuple import numpy as np from qiskit_experiments.data_processing.data_action import DataAction, TrainableDataAction @@ -43,14 +43,12 @@ def _format_data(self, datum: Any, error: Optional[Any] = None): return datum, error def _process( - self, datum: np.array, error: Optional[np.array] = None, **options + self, datum: np.array, error: Optional[np.array] = None ) -> Tuple[np.array, np.array]: """Average the data. Args: datum: an array of data. - options: keyword arguments which, if they contain "axis" then this axis - will be used to override the default axis set at construction time. Returns: Two arrays with one less dimension than the given datum and error. The error @@ -60,12 +58,9 @@ def _process( Raises: DataProcessorError: If the axis is not an int. """ - axis = options.get("axis", self._axis) + standard_error = np.std(datum, axis=self._axis) / np.sqrt(datum.shape[0]) - if not isinstance(axis, int): - raise DataProcessorError(f"Axis must be int, received {axis}.") - - return np.average(datum, axis=axis), np.std(datum, axis=axis) / np.sqrt(datum.shape[0]) + return np.average(datum, axis=self._axis), standard_error class SVD(TrainableDataAction): @@ -155,7 +150,7 @@ def is_trained(self) -> bool: return self._main_axes is not None def _process( - self, datum: np.array, error: Optional[np.array] = None, **options + self, datum: np.array, error: Optional[np.array] = None ) -> Tuple[np.array, np.array]: """Project the IQ data onto the axis defined by an SVD and scale it. @@ -254,13 +249,12 @@ def __init__(self, scale: Optional[float] = None, validate: bool = True): super().__init__(validate) @abstractmethod - def _process(self, datum: np.array, error: Optional[np.array] = None, **options) -> np.array: + def _process(self, datum: np.array, error: Optional[np.array] = None) -> np.array: """Defines how the IQ point is processed. Args: datum: A 2D or a 3D array of complex IQ points as [real, imaginary]. error: A 2D or a 3D array of errors on complex IQ points as [real, imaginary]. - options: Keyword arguments passed through the data processor at run-time. Returns: Processed IQ point and its associated error estimate. @@ -315,7 +309,7 @@ class ToReal(IQPart): """IQ data post-processing. Isolate the real part of single-shot IQ data.""" def _process( - self, datum: np.array, error: Optional[np.array] = None, **options + self, datum: np.array, error: Optional[np.array] = None ) -> Tuple[np.array, np.array]: """Take the real part of the IQ data. @@ -342,7 +336,7 @@ def _process( class ToImag(IQPart): """IQ data post-processing. Isolate the imaginary part of single-shot IQ data.""" - def _process(self, datum: np.array, error: Optional[np.array] = None, **options) -> np.array: + def _process(self, datum: np.array, error: Optional[np.array] = None) -> np.array: """Take the imaginary part of the IQ data. Args: @@ -413,9 +407,7 @@ def _format_data(self, datum: dict, error: Optional[Any] = None) -> Tuple[dict, return datum, None - def _process( - self, datum: Dict[str, Any], error: Optional[Dict] = None, **options - ) -> Tuple[float, float]: + def _process(self, datum: Dict[str, Any], error: Optional[Dict] = None) -> Tuple[float, float]: """ Args: datum: The data dictionary,taking the data under counts and @@ -424,10 +416,9 @@ def _process( Returns: processed data: A dict with the populations. """ - outcome = options.get("outcome", self._outcome) shots = sum(datum.values()) - p_mean = datum.get(outcome, 0.0) / shots + p_mean = datum.get(self._outcome, 0.0) / shots p_var = p_mean * (1 - p_mean) / shots return p_mean, p_var diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index e97128e5ae..c07da481a6 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -160,11 +160,6 @@ def test_populations(self): self.assertEqual(new_data, 0.4) self.assertEqual(error, 0.4 * (1 - 0.4) / 10) - new_data, error = processor(self.exp_data_lvl2.data(0), outcome="10") - - self.assertEqual(new_data, 0.6) - self.assertEqual(error, 0.6 * (1 - 0.6) / 10) - def test_validation(self): """Test the validation mechanism.""" From 1a79dcd89cb8a1129e2431461e9ba8fccc6a5bea Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 18 May 2021 23:08:11 +0200 Subject: [PATCH 20/22] * Used the property --- qiskit_experiments/data_processing/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 38e23c26fa..8c49a63591 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -185,7 +185,7 @@ def _process( [datum[idx][iq] - self.means(qubit=idx, iq_index=iq) for iq in [0, 1]] ) - processed_data.append((self._main_axes[idx] @ centered) / self._scales[idx]) + processed_data.append((self._main_axes[idx] @ centered) / self.scales[idx]) if error is not None: angle = np.arctan(self._main_axes[idx][1] / self._main_axes[idx][0]) From abbce07f81e03095b7a58e705e3a05aff80c6a24 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 18 May 2021 23:12:20 +0200 Subject: [PATCH 21/22] * Removed raises in SVD. --- qiskit_experiments/data_processing/nodes.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 8c49a63591..80bcc5af90 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -111,10 +111,7 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An @property def axis(self) -> List[np.array]: """Return the axis of the trained SVD""" - if self._main_axes: - return self._main_axes - - raise DataProcessorError("SVD is not trained.") + return self._main_axes def means(self, qubit: int, iq_index: int) -> float: """Return the mean by which to correct the IQ data. @@ -135,10 +132,7 @@ def means(self, qubit: int, iq_index: int) -> float: @property def scales(self) -> List[float]: """Return the scaling of the SVD.""" - if self._scales: - return self._scales - - raise DataProcessorError("SVD is not trained.") + return self._scales @property def is_trained(self) -> bool: From 935afda1cacd4a7995bd45faaefc37c4942bf654 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 19 May 2021 07:27:59 +0200 Subject: [PATCH 22/22] * Made scale default to 1.0 --- qiskit_experiments/data_processing/nodes.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 80bcc5af90..307dc398f8 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -233,10 +233,10 @@ def train(self, data: List[Any]): class IQPart(DataAction): """Abstract class for IQ data post-processing.""" - def __init__(self, scale: Optional[float] = None, validate: bool = True): + def __init__(self, scale: float = 1.0, validate: bool = True): """ Args: - scale: Float with which to multiply the IQ data. + scale: Float with which to multiply the IQ data. Defaults to 1.0. validate: If set to False the DataAction will not validate its input. """ self.scale = scale @@ -315,12 +315,6 @@ def _process( Returns: A 1D or 2D array, each entry is the real part of the given IQ data and error. """ - if self.scale is None: - if error is not None: - return datum[..., 0], error[..., 0] - else: - return datum[..., 0], None - if error is not None: return datum[..., 0] * self.scale, error[..., 0] * self.scale else: @@ -341,12 +335,6 @@ def _process(self, datum: np.array, error: Optional[np.array] = None) -> np.arra Returns: A 1D or 2D array, each entry is the imaginary part of the given IQ data and error. """ - if self.scale is None: - if error is not None: - return datum[..., 1], error[..., 1] - else: - return datum[..., 1], None - if error is not None: return datum[..., 1] * self.scale, error[..., 1] * self.scale else: