From b3e7fbda84609ca22bc1c2f6c99aefc2350790ce Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 2 Nov 2021 00:51:58 +0900 Subject: [PATCH 01/55] fix SVD shape --- qiskit_experiments/data_processing/nodes.py | 85 ++++++++++++-------- test/data_processing/test_data_processing.py | 14 ++-- test/data_processing/test_nodes.py | 14 ++-- 3 files changed, 68 insertions(+), 45 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 24af0f3842..278e986ae2 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -99,7 +99,8 @@ class SVD(TrainableDataAction): """Singular Value Decomposition of averaged IQ data.""" def __init__(self, validate: bool = True): - """ + """Create new action. + Args: validate: If set to False the DataAction will not validate its input. """ @@ -107,12 +108,17 @@ def __init__(self, validate: bool = True): self._main_axes = None self._means = None self._scales = None + self._n_circs = 0 + self._n_shots = 0 + self._n_slots = 0 + self._n_iq = 0 def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: """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. + datum: Data which corresponds to single-shot IQ data. + error: Optional, accompanied error. Returns: datum and any error estimate as a numpy array. @@ -125,14 +131,32 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An if error is not None: error = np.asarray(error, dtype=float) + self._n_circs = 0 + self._n_shots = 0 + self._n_slots = 0 + self._n_iq = 0 + + # identify shape + try: + # level1 single mode + self._n_circs, self._n_shots, self._n_slots, self._n_iq = datum.shape + except ValueError: + try: + # level1 average mode + self._n_circs, self._n_slots, self._n_iq = datum.shape + except ValueError as ex: + raise DataProcessorError( + f"Data given to {self.__class__.__name__} is not likely level1 data." + ) from ex + if self._validate: - if len(datum.shape) not in {2, 3}: + if self._n_iq != 2: raise DataProcessorError( f"IQ data given to {self.__class__.__name__} must be a 2D array. " - f"Instead, a {len(datum.shape)}D array was given." + f"Instead, a {self._n_iq}D array was given." ) - if error is not None and len(error.shape) not in {2, 3}: + if error is not None and error.shape != datum.shape: raise DataProcessorError( f"IQ data error given to {self.__class__.__name__} must be a 2D array." f"Instead, a {len(error.shape)}D array was given." @@ -192,45 +216,42 @@ def _process( Raises: DataProcessorError: If the SVD has not been previously trained on data. """ - if not self.is_trained: raise DataProcessorError("SVD must be trained on data before it can be used.") - n_qubits = datum.shape[0] if len(datum.shape) == 2 else datum.shape[1] - processed_data = [] - - if error is not None: - processed_error = [] + # IQ axis is reduced by projection + if self._n_shots == 0: + # level1 single mode + dims = self._n_circs, self._n_slots else: - processed_error = None + # level1 average mode + dims = self._n_circs, self._n_shots, self._n_slots - # process each averaged IQ point with its own axis. - for idx in range(n_qubits): + singular_vals = np.zeros(dims, dtype=float) + error_vals = np.zeros(dims, dtype=float) + for idx in range(self._n_slots): + scale = self.scales[idx] centered = np.array( [datum[..., idx, iq] - self.means(qubit=idx, iq_index=iq) for iq in [0, 1]] ) + angle = np.arctan(self._main_axes[idx][1] / self._main_axes[idx][0]) - processed_data.append((self._main_axes[idx] @ centered) / self.scales[idx]) + singular_vals[..., idx] = (self._main_axes[idx] @ centered) / scale 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 + error_vals[..., idx] = ( + np.sqrt( + (error[..., idx, 0] * np.cos(angle)) ** 2 + + (error[..., idx, 1] * np.sin(angle)) ** 2 + ) + / scale ) - processed_error.append(error_value / self.scales[idx]) - if len(processed_data) == 1: - if error is None: - return processed_data[0], None - else: - return processed_data[0], processed_error[0] + if self._n_circs == 1: + return singular_vals[0], error_vals[0] - if error is None: - return np.array(processed_data), None - else: - return np.array(processed_data), np.array(processed_error) + return singular_vals, error_vals def train(self, data: List[Any]): """Train the SVD on the given data. @@ -248,14 +269,14 @@ def train(self, data: List[Any]): if data is None: return - n_qubits = self._format_data(data[0])[0].shape[0] + data, _ = self._format_data(data) self._main_axes = [] self._scales = [] self._means = [] - for qubit_idx in range(n_qubits): - datums = np.vstack([self._format_data(datum)[0][qubit_idx] for datum in data]).T + for qubit_idx in range(self._n_slots): + datums = np.vstack([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, :]) diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index b1c2d690c2..a4771a159a 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -322,7 +322,7 @@ def setUp(self): [[0.9, 0.9], [-1.1, 1.0]], ] ) - self._sig_gs = np.array([[1.0], [-1.0]]) / np.sqrt(2.0) + self._sig_gs = np.array([1.0, -1.0]) / np.sqrt(2.0) circ_gs = ExperimentResultData( memory=[ @@ -332,7 +332,7 @@ def setUp(self): [[-0.9, -0.9], [1.1, -1.0]], ] ) - self._sig_es = np.array([[-1.0], [1.0]]) / np.sqrt(2.0) + self._sig_es = np.array([-1.0, 1.0]) / np.sqrt(2.0) circ_x90p = ExperimentResultData( memory=[ @@ -342,7 +342,7 @@ def setUp(self): [[1.0, 1.0], [-1.0, 1.0]], ] ) - self._sig_x90 = np.array([[0], [0]]) + self._sig_x90 = np.array([0, 0]) circ_x45p = ExperimentResultData( memory=[ @@ -352,7 +352,7 @@ def setUp(self): [[1.0, 1.0], [-1.0, 1.0]], ] ) - self._sig_x45 = np.array([[0.5], [-0.5]]) / np.sqrt(2.0) + self._sig_x45 = np.array([0.5, -0.5]) / np.sqrt(2.0) res_es = ExperimentResult( shots=4, @@ -460,7 +460,7 @@ def test_process_all_data(self): self._sig_x90.reshape(1, 2), self._sig_x45.reshape(1, 2), ) - ).T + ) # Test processing of all data processed = processor(self.data.data())[0] @@ -480,7 +480,7 @@ def test_normalize(self): processor.train([self.data.data(idx) for idx in [0, 1]]) self.assertTrue(processor.is_trained) - all_expected = np.array([[0.0, 1.0, 0.5, 0.75], [1.0, 0.0, 0.5, 0.25]]) + all_expected = np.array([[0.0, 1.0], [1.0, 0.0], [0.5, 0.5], [0.75, 0.25]]) # Test processing of all data processed = processor(self.data.data())[0] @@ -559,7 +559,7 @@ def test_normalize(self): processor.train([self.data.data(idx) for idx in [0, 1]]) self.assertTrue(processor.is_trained) - all_expected = np.array([[0.0, 1.0, 0.5, 0.75], [1.0, 0.0, 0.5, 0.25]]) + all_expected = np.array([[0.0, 1.0], [1.0, 0.0], [0.5, 0.5], [0.75, 0.25]]) # Test processing of all data processed = processor(self.data.data())[0] diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 9832dcf533..1c4392f528 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -118,15 +118,17 @@ 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]])) + # Note: input data shape [n_circs, n_slots, n_iq] for avg mode simulation + + 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)) @@ -166,18 +168,18 @@ def test_svd_error(self): 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]]) + 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]]) + 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])) # Tilt 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]]) + 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_])) From 05e27d4fc81f8499e2e4f33b5386b975d51760de Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 2 Nov 2021 00:53:53 +0900 Subject: [PATCH 02/55] fix docs --- 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 278e986ae2..ab1e716720 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -117,7 +117,7 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An """Check that the IQ data is 2D and convert it to a numpy array. Args: - datum: Data which corresponds to single-shot IQ data. + datum: Whole data. error: Optional, accompanied error. Returns: From 714080c4dca3c1f7ba24c0f3d42b769a0b5ca292 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 2 Nov 2021 00:57:03 +0900 Subject: [PATCH 03/55] fix comment --- qiskit_experiments/data_processing/nodes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index ab1e716720..199398f3cd 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -221,10 +221,10 @@ def _process( # IQ axis is reduced by projection if self._n_shots == 0: - # level1 single mode + # level1 average mode dims = self._n_circs, self._n_slots else: - # level1 average mode + # level1 single mode dims = self._n_circs, self._n_shots, self._n_slots singular_vals = np.zeros(dims, dtype=float) From 7323449696f62d1ef77eb10e1f473b6bd97a4ffa Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 2 Nov 2021 16:02:36 +0900 Subject: [PATCH 04/55] fix var name --- qiskit_experiments/data_processing/nodes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 199398f3cd..711beb7d30 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -227,7 +227,7 @@ def _process( # level1 single mode dims = self._n_circs, self._n_shots, self._n_slots - singular_vals = np.zeros(dims, dtype=float) + processed_data = np.zeros(dims, dtype=float) error_vals = np.zeros(dims, dtype=float) for idx in range(self._n_slots): @@ -237,7 +237,7 @@ def _process( ) angle = np.arctan(self._main_axes[idx][1] / self._main_axes[idx][0]) - singular_vals[..., idx] = (self._main_axes[idx] @ centered) / scale + processed_data[..., idx] = (self._main_axes[idx] @ centered) / scale if error is not None: error_vals[..., idx] = ( @@ -249,9 +249,9 @@ def _process( ) if self._n_circs == 1: - return singular_vals[0], error_vals[0] + return processed_data[0], error_vals[0] - return singular_vals, error_vals + return processed_data, error_vals def train(self, data: List[Any]): """Train the SVD on the given data. From 1981b6e864195c15be71364337480b8cda04eb93 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 3 Nov 2021 02:32:17 +0900 Subject: [PATCH 05/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- 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 711beb7d30..10bf79cc31 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -117,7 +117,10 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An """Check that the IQ data is 2D and convert it to a numpy array. Args: - datum: Whole data. + datum: All IQ data. This data has different dimensions depending on whether + single-shot or averaged data is being processed. Single-shot data is four dimensional, + i.e., n. circuits x n. shots x n. slots x 2, while averaged IQ data is three dimensional, i.e., + n. circuits x n. slots x 2. Here, n. slots is the number of measured qubits. error: Optional, accompanied error. Returns: From c4b8823e51eb20c037aca060cbbc6d189eec30bd Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 3 Nov 2021 02:32:25 +0900 Subject: [PATCH 06/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- 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 10bf79cc31..58df9e9b7c 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -141,7 +141,7 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An # identify shape try: - # level1 single mode + # level1 single-shot data self._n_circs, self._n_shots, self._n_slots, self._n_iq = datum.shape except ValueError: try: From 159d3a23161e3eca049e6ffaafa6e0e7882b49e1 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 3 Nov 2021 02:32:33 +0900 Subject: [PATCH 07/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- 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 58df9e9b7c..e73b75a442 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -145,7 +145,7 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An self._n_circs, self._n_shots, self._n_slots, self._n_iq = datum.shape except ValueError: try: - # level1 average mode + # level1 data averaged over shots self._n_circs, self._n_slots, self._n_iq = datum.shape except ValueError as ex: raise DataProcessorError( From a7e7b9c721e2ab3f9fdf183852e4c9186de152ed Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 3 Nov 2021 02:45:31 +0900 Subject: [PATCH 08/55] comments --- qiskit_experiments/data_processing/nodes.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index e73b75a442..e4d78b7d1d 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -118,9 +118,10 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An Args: datum: All IQ data. This data has different dimensions depending on whether - single-shot or averaged data is being processed. Single-shot data is four dimensional, - i.e., n. circuits x n. shots x n. slots x 2, while averaged IQ data is three dimensional, i.e., - n. circuits x n. slots x 2. Here, n. slots is the number of measured qubits. + single-shot or averaged data is being processed. + Single-shot data is four dimensional, i.e., ``[#circuits, #shots, #slots, 2]``, + while averaged IQ data is three dimensional, i.e., ``[#circuits, #slots, 2]``. + Here, ``#slots`` is the number of classical registers used in the circuit. error: Optional, accompanied error. Returns: @@ -155,8 +156,8 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An if self._validate: if self._n_iq != 2: raise DataProcessorError( - f"IQ data given to {self.__class__.__name__} must be a 2D array. " - f"Instead, a {self._n_iq}D array was given." + f"IQ data given to {self.__class__.__name__} does not have two-dimensions " + f"(I and Q). Instead, {self._n_iq} dimensions were found." ) if error is not None and error.shape != datum.shape: @@ -238,11 +239,10 @@ def _process( centered = np.array( [datum[..., idx, iq] - self.means(qubit=idx, iq_index=iq) for iq in [0, 1]] ) - angle = np.arctan(self._main_axes[idx][1] / self._main_axes[idx][0]) - processed_data[..., idx] = (self._main_axes[idx] @ centered) / scale if error is not None: + angle = np.arctan(self._main_axes[idx][1] / self._main_axes[idx][0]) error_vals[..., idx] = ( np.sqrt( (error[..., idx, 0] * np.cos(angle)) ** 2 @@ -252,8 +252,13 @@ def _process( ) if self._n_circs == 1: - return processed_data[0], error_vals[0] + if error is None: + return processed_data[0], None + else: + return processed_data[0], error_vals[0] + if error is None: + return processed_data, None return processed_data, error_vals def train(self, data: List[Any]): From 326e95ca8a51055179b0dc9bb7bdd085de306e5a Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 3 Nov 2021 02:33:15 +0900 Subject: [PATCH 09/55] update except for SVD --- .../data_processing/data_action.py | 54 ++-- .../data_processing/data_processor.py | 121 ++++--- qiskit_experiments/data_processing/nodes.py | 295 ++++++++---------- requirements.txt | 1 + 4 files changed, 226 insertions(+), 245 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 9427bd508a..46bd8514f3 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -13,66 +13,64 @@ """Defines the steps that can be used to analyse data.""" from abc import ABCMeta, abstractmethod -from typing import Any, List, Optional, Tuple +from typing import Any, List + +import numpy as np class DataAction(metaclass=ABCMeta): - """ - Abstract action done on measured data to process it. Each subclass of DataAction must - define the way it formats, validates and processes data. + """Abstract action done on measured data to process it. + + Each subclass of DataAction must define the way it formats, validates and processes data. """ def __init__(self, validate: bool = True): - """ + """Create new node. + Args: validate: If set to False the DataAction will not validate its input. """ self._validate = validate @abstractmethod - def _process(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: - """ - Applies the data processing step to the datum. + def _process(self, data: np.ndarray) -> np.ndarray: + """Applies the data processing step to the data. Args: - datum: A single item of data which will be processed. - error: An optional error estimation on the datum that can be further propagated. + data: A full data array to process. This is numpy array of arbitrary dtype. + If elements are ufloat objects consisting of nominal value and + standard error values, error propagation is automatically computed. Returns: - processed data: The data that has been processed along with the propagated error. + The data that has been processed. """ - @abstractmethod - def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: + def _format_data(self, data: np.ndarray) -> np.ndarray: """Format and validate the input. - Check that the given data and error has the correct structure. This method may + Check that the given data 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. + data: A full data array to format. Returns: - datum, error: The formatted datum and its optional error. - - Raises: - DataProcessorError: If either the data or the error do not have the proper format. + The data that has been validated and formatted. """ + return data - 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. + def __call__(self, data: np.ndarray) -> np.ndarray: + """Call the data action of this node on the data. 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. + data: A numpy array with arbitrary dtype. If elements are ufloat objects + consisting of nominal value and standard error values, + error propagation is automatically computed. Returns: - processed data: The data processed by self as a tuple of processed datum and - optionally the propagated error estimate. + The data that has been processed. """ - return self._process(*self._format_data(data, error)) + return self._process(*self._format_data(data)) 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 4cb45d71ad..1579323e68 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -12,6 +12,9 @@ """Actions done on the data to bring it in a usable form.""" +import numpy as np +from uncertainties import unumpy as unp + from typing import Any, Dict, List, Set, Tuple, Union from qiskit_experiments.data_processing.data_action import DataAction, TrainableDataAction @@ -36,7 +39,7 @@ class DataProcessor: def __init__( self, input_key: str, - data_actions: List[DataAction] = None, + data_actions: List[Union[DataAction, TrainableDataAction]] = None, ): """Create a chain of data processing actions. @@ -44,13 +47,12 @@ def __init__( input_key: The initial key in the datum Dict[str, Any] under which the data processor will find the data to process. data_actions: A list of data processing actions to construct this data processor with. - If None is given an empty DataProcessor will be created. - to_array: Boolean indicating if the input data will be converted to a numpy array. + If nothing is given the processor returns unprocessed data. """ self._input_key = input_key self._nodes = data_actions if data_actions else [] - def append(self, node: DataAction): + def append(self, node: Union[DataAction, TrainableDataAction]): """ Append new data action node to this data processor. @@ -69,14 +71,15 @@ def is_trained(self) -> bool: return True - def __call__(self, data: Union[Dict, List[Dict]], **options) -> Tuple[Any, Any]: + def __call__(self, data: Union[Dict, List[Dict]], **options) -> np.ndarray: """ Call self on the given datum. This method sequentially calls the stored data actions on the datum. Args: - data: The data, typically from ExperimentData.data(...), that needs to be processed. - This dict or list of dicts also contains the metadata of each experiment. + data: The data, typically from ``ExperimentData.data(...)``, + that needs to be processed. This dict or list of dicts also contains + the metadata of each experiment. options: Run-time options given as keyword arguments that will be passed to the nodes. Returns: @@ -86,21 +89,22 @@ def __call__(self, data: Union[Dict, List[Dict]], **options) -> Tuple[Any, Any]: def call_with_history( self, data: Union[Dict, List[Dict]], history_nodes: Set = None - ) -> Tuple[Any, Any, List]: + ) -> Tuple[np.ndarray, 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. Args: - data: The data, typically from ExperimentData.data(...), that needs to be processed. - This dict or list of dicts also contains the metadata of each experiment. + data: The data, typically from ``ExperimentData.data(...)``, + that needs to be processed. This dict or list of dicts also contains + the metadata of each experiment. 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. Returns: - processed data: The datum processed by the data processor. - history: The datum processed at each node of the data processor. + A tuple of (processed data, history), that are the data processed by the processor + and its intermediate state in each specified node, respectively. """ return self._call_internal(data, True, history_nodes) @@ -110,7 +114,7 @@ def _call_internal( with_history: bool = False, history_nodes: Set = None, call_up_to_node: int = None, - ) -> Union[Tuple[Any, Any], Tuple[Any, Any, List]]: + ) -> Union[np.ndarray, Tuple[np.ndarray, List]]: """Process the data with or without storing the history of the computation. Args: @@ -125,28 +129,25 @@ def _call_internal( 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. + When ``with_history`` is ``False`` it returns an numpy array of processed data. + Otherwise it returns a tuple of above with a list of intermediate data at each step. """ if call_up_to_node is None: call_up_to_node = len(self._nodes) - datum_, error_ = self._data_extraction(data), None + data_to_process = self._data_extraction(data) history = [] - for index, node in enumerate(self._nodes): + for index, node in enumerate(self._nodes[:call_up_to_node]): + data_to_process = node(data_to_process) - if index < call_up_to_node: - datum_, error_ = node(datum_, error_) - - if with_history and ( - history_nodes is None or (history_nodes and index in history_nodes) - ): - history.append((node.__class__.__name__, datum_, error_, index)) + if with_history and (history_nodes is None or index in history_nodes): + history.append((node.__class__.__name__, data_to_process, index)) if with_history: - return datum_, error_, history + return data_to_process, history else: - return datum_, error_ + return data_to_process def train(self, data: List[Dict[str, Any]]): """Train the nodes of the data processor. @@ -161,16 +162,15 @@ def train(self, data: List[Dict[str, Any]]): # Process the data up to the untrained node. node.train(self._call_internal(data, call_up_to_node=index)[0]) - def _data_extraction(self, data: Union[Dict, List[Dict]]) -> List: + def _data_extraction(self, data: Union[Dict, List[Dict]]) -> np.ndarray: """Extracts the data on which to run the nodes. If the datum is a list of dicts then the data under self._input_key is extracted - from each dict and appended to a list which therefore contains all the data. If the - data processor has to_array set to True then the list will be converted to a numpy - array. + from each dict and appended to a list which therefore contains all the data. Args: - data: A list of such dicts where the data is contained under the key self._input_key. + data: A list of such dicts where the data is contained under the key + ``self._input_key``. Returns: The data formatted in such a way that it is ready to be processed by the nodes. @@ -178,26 +178,59 @@ def _data_extraction(self, data: Union[Dict, List[Dict]]) -> List: Raises: DataProcessorError: - If the input datum is not a list or a dict. - - If the data processor received a single datum but requires all the data to - process it properly. - If the input key of the data processor is not contained in the data. + - If the data processor receives multiple data with different measurement + configuration, i.e. Jagged array. """ if isinstance(data, dict): data = [data] + data_to_process = [] + dims = None + for datum in data: + try: + outcome = datum[self._input_key] + except TypeError as error: + raise DataProcessorError( + f"{self.__class__.__name__} only extracts data from " + f"lists or dicts, received {type(data)}." + ) from error + except KeyError as error: + raise DataProcessorError( + f"The input key {self._input_key} was not found in the input datum." + ) from error + + if self._input_key == "counts": + # asarray(dict) returns zero-dim array since dict is also iterable. + # outcome should be array-like. + outcome = [outcome] + outcome = np.asarray(outcome) + + # Validate data shape + if dims is None: + dims = outcome.shape + else: + # This is because each data node creates full array of all result data. + # Jagged array cannot be numerically operated with numpy array. + if outcome.shape != dims: + raise DataProcessorError( + "Input data is likely a mixture of job results with different measurement " + "configuration. Data processor doesn't support jagged array." + ) + + data_to_process.append(outcome) + try: - data_ = [_datum[self._input_key] for _datum in iter(data)] - except KeyError as error: - raise DataProcessorError( - f"The input key {self._input_key} was not found in the input datum." - ) from error - except TypeError as error: - raise DataProcessorError( - f"{self.__class__.__name__} only extracts data from " - f"lists or dicts, received {type(data)}." - ) from error - - return data_ + # Likely level1 or below. Return ufloat array with un-computed std_dev. + # This will also return a standard ndarray with dtype=object. + nominal_values = np.asarray(data_to_process, float) + return unp.uarray( + nominal_values=nominal_values, + std_devs=np.full_like(nominal_values, np.nan, dtype=float), + ) + except TypeError: + # Likely level2 counts or level2 memory data. Cannot be typecasted to ufloat. + return np.asarray(data_to_process, dtype=object) def __repr__(self): """String representation of data processors.""" diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 24af0f3842..12876c3e9e 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -14,8 +14,11 @@ from abc import abstractmethod from numbers import Number -from typing import Any, Dict, List, Optional, Tuple, Union, Sequence +from typing import Any, List, Optional, Tuple, Union, Sequence + import numpy as np +from uncertainties import unumpy as unp, ufloat +from uncertainties.core import Variable from qiskit_experiments.data_processing.data_action import DataAction, TrainableDataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -34,65 +37,67 @@ def __init__(self, axis: int, validate: bool = True): super().__init__(validate) 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) + def _format_data(self, data: np.ndarray) -> np.ndarray: + """Format the data into numpy arrays. + Args: + data: A full data array to format. + + Returns: + The data that has been validated and formatted. + """ if self._validate: - if len(datum.shape) <= self._axis: + if len(data.shape) <= self._axis: raise DataProcessorError( - f"Cannot average the {len(datum.shape)} dimensional " + f"Cannot average the {len(data.shape)} dimensional " f"array along axis {self._axis}." ) - if error is not None: - error = np.asarray(error, dtype=float) - - return datum, error + return data - def _process( - self, datum: np.array, error: Optional[np.array] = None - ) -> Tuple[np.array, np.array]: + def _process(self, data: np.ndarray) -> np.ndarray: """Average the data. - Args: - datum: an array of data. + Args: + data: A full data array to format. - 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. + Notes: + The error propagation is computed if any error values is already involved. + Otherwise error is computed by 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. + Returns: + Arrays with one less dimension than the given data. """ - standard_error = np.std(datum, axis=self._axis) / np.sqrt(datum.shape[self._axis]) + errors = unp.std_devs(data) + + # Error is not defined or no error. Newly compute variance from nominal values. + if np.isnan(errors).all() or not errors.any(): + nominals = unp.nominal_values(data) + errors = np.std(nominals, axis=self._axis) / np.sqrt(nominals.shape[self._axis]) + return unp.uarray(nominal_values=nominals, std_devs=errors) - return np.average(datum, axis=self._axis), standard_error + # Compute average with error propagation + return np.mean(data, axis=self._axis) class MinMaxNormalize(DataAction): """Normalizes the data.""" - 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) + def _process(self, data: np.ndarray) -> np.ndarray: + """Normalize the data to the interval [0, 1]. - return datum, error + Args: + data: A full data array to process. - def _process( - self, datum: np.array, error: Optional[np.array] = None - ) -> Tuple[np.array, np.array]: - """Normalize the data to the interval [0, 1].""" - min_y, max_y = np.min(datum), np.max(datum) + Returns: + The data that has been processed. + """ + min_y, max_y = np.min(data), np.max(data) - if error is not None: - return (datum - min_y) / (max_y - min_y), error / (max_y - min_y) - else: - return (datum - min_y) / (max_y - min_y), None + # Uncertainty of min_y and max_y are also considered. + return (data - min_y) / (max_y - min_y) class SVD(TrainableDataAction): @@ -285,61 +290,39 @@ def __init__(self, scale: float = 1.0, validate: bool = True): super().__init__(validate) @abstractmethod - def _process(self, datum: np.array, error: Optional[np.array] = None) -> np.array: + def _process(self, data: np.ndarray) -> np.ndarray: """Defines how the IQ point is processed. - The dimension of the input datum corresponds to different types of data: - - 2D represents average IQ Data. - - 3D represents either a single-shot datum or all data of averaged data. - - 4D represents all data of single-shot data. + The last dimension of the array should correspond to [real, imaginary] part of data. Args: - datum: A N dimensional array of complex IQ points as [real, imaginary]. - error: A N dimensional array of errors on complex IQ points as [real, imaginary]. + data: A full data array to process. Returns: - Processed IQ point and its associated error estimate. + The data that has been processed. """ - 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. + def _format_data(self, data: np.ndarray) -> np.ndarray: + """Format and validate the input. + + Check that the given data has the correct structure. This method may + additionally change the data type, e.g. converting a list 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). + data: A full data array to format. Returns: - datum and any error estimate as a numpy array. - - Raises: - DataProcessorError: If the datum does not have the correct format. + The data that has been validated and formatted. """ - 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 {2, 3, 4}: - raise DataProcessorError( - f"IQ data given to {self.__class__.__name__} must be an N dimensional" - f"array with N in (2, 3, 4). Instead, a {len(datum.shape)}D array was given." - ) - - if error is not None and len(error.shape) not in {2, 3, 4}: - raise DataProcessorError( - f"IQ data error given to {self.__class__.__name__} must be an N dimensional" - f"array with N in (2, 3, 4). Instead, a {len(error.shape)}D array was given." - ) - - if error is not None and len(error.shape) != len(datum.shape): + if data.shape[-1] != 2: raise DataProcessorError( - "Datum and error do not have the same shape: " - f"{len(datum.shape)} != {len(error.shape)}." + f"IQ data given to {self.__class__.__name__} must be multi-dimensional array" + "of [d0, d1, ..., 2] in which the last dimension corresponds to IQ elements." + f"Input data contains element with length {data.shape[-1]} != 2." ) - return datum, error + return data def __repr__(self): """String representation of the node.""" @@ -349,46 +332,31 @@ def __repr__(self): 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 - ) -> Tuple[np.array, np.array]: + def _process(self, data: np.ndarray) -> np.ndarray: """Take the real part of the IQ data. Args: - datum: An N dimensional array of shots, qubits, and a complex IQ point as - [real, imaginary]. - error: An N dimensional optional array of shots, qubits, and an error on a - complex IQ point as [real, imaginary]. + data: An N-dimensional array of complex IQ point as [real, imaginary]. Returns: - A N-1 dimensional array, each entry is the real part of the given IQ data and error. + A N-1 dimensional array, each entry is the real part of the given IQ data. """ - if error is not None: - return datum[..., 0] * self.scale, error[..., 0] * self.scale - else: - return datum[..., 0] * self.scale, None + return data[..., 0] * self.scale 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) -> np.array: + def _process(self, data: np.ndarray) -> np.ndarray: """Take the imaginary part of the IQ data. Args: - datum: An N dimensional array of shots, qubits, and a complex IQ point as - [real, imaginary]. - error: An N dimensional optional array of shots, qubits, and an error on a - complex IQ point as [real, imaginary]. + data: An N-dimensional array of complex IQ point as [real, imaginary]. Returns: - A N-1 dimensional array, each entry is the imaginary part of the given IQ data - and error. + A N-1 dimensional array, each entry is the imaginary part of the given IQ data. """ - if error is not None: - return datum[..., 1] * self.scale, error[..., 1] * self.scale - else: - return datum[..., 1] * self.scale, None + return data[..., 1] * self.scale class Probability(DataAction): @@ -429,6 +397,8 @@ class Probability(DataAction): \text{E}[p] = \frac{F + 0.5}{N + 1}, \quad \text{Var}[p] = \frac{\text{E}[p] (1 - \text{E}[p])}{N + 2} + + This node will deprecate standard error provided by the previous node. """ def __init__( @@ -459,85 +429,71 @@ def __init__( "Prior for probability node must be a float or pair of floats." ) self._alpha_prior = list(alpha_prior) + super().__init__(validate) - def _format_data(self, datum: dict, error: Optional[Any] = None) -> Tuple[dict, Any]: + def _format_data(self, data: np.ndarray) -> np.ndarray: """ Checks that the given data has a counts format. Args: - datum: An instance of data the should be a dict with bit strings as keys - and counts as values. + data: An object data array containing count dictionaries. Returns: - The datum as given. + The ``data`` as given. Raises: DataProcessorError: if the data is not a counts dict or a list of counts dicts. """ - if self._validate: - - if isinstance(datum, dict): - data = [datum] - elif isinstance(datum, list): - data = datum - else: - raise DataProcessorError(f"Datum must be dict or list, received {type(datum)}.") + VALID_COUNT = int, float, np.integer - for datum_ in data: - if not isinstance(datum_, dict): + if self._validate: + for datum in data: + if not isinstance(datum, dict): raise DataProcessorError( - f"Given counts datum {datum_} to " - f"{self.__class__.__name__} is not a valid count format." + f"Data entry must be dictionary of counts, received {type(datum)}." ) - - for bit_str, count in datum_.items(): + for bit_str, count in datum.items(): if not isinstance(bit_str, str): raise DataProcessorError( - f"Key {bit_str} is not a valid count key in{self.__class__.__name__}." + f"Key {bit_str} is not a valid count key in {self.__class__.__name__}." ) - - if not isinstance(count, (int, float, np.integer)): + if isinstance(count, Variable): + # This code eliminates variance in count values. + # This variance may be generated by readout error mitigation when + # A-matrix is computed with some error. + datum[bit_str] = count.nominal_value + if not isinstance(count, VALID_COUNT): raise DataProcessorError( - f"Count {bit_str} is not a valid count value in {self.__class__.__name__}." + f"Count {bit_str} is not a valid value in {self.__class__.__name__}." ) - return datum, None + # This is bare ndarray. No error will be attached to data. + return data + + def _process(self, data: np.ndarray) -> np.ndarray: + """Compute mean and standard error from the beta distribution. - def _process( - self, - datum: Union[Dict[str, Any], List[Dict[str, Any]]], - error: Optional[Union[Dict, List]] = None, - ) -> Union[Tuple[float, float], Tuple[np.array, np.array]]: - """ Args: - datum: The data dictionary,taking the data under counts and - adding the corresponding probabilities. + data: A object data array of counts dictionary. Returns: - processed data: A dict with the populations and standard deviation. + The data that has been processed. """ - if isinstance(datum, dict): - return self._population_error(datum) - else: - populations, errors = [], [] + probabilities = np.empty(data.size, dtype=object) - for datum_ in datum: - pop, error = self._population_error(datum_) - populations.append(pop) - errors.append(error) + for idx, counts_dict in enumerate(data): + shots = sum(counts_dict.values()) + freq = counts_dict.get(self._outcome, 0) + alpha_posterior = [freq + self._alpha_prior[0], shots - freq + self._alpha_prior[1]] + alpha_sum = sum(alpha_posterior) - return np.array(populations), np.array(errors) + p_mean = alpha_posterior[0] / alpha_sum + p_var = p_mean * (1 - p_mean) / (alpha_sum + 1) - def _population_error(self, counts_dict: Dict[str, int]) -> Tuple[float, float]: - """Helper method""" - shots = sum(counts_dict.values()) - freq = counts_dict.get(self._outcome, 0) - alpha_posterior = [freq + self._alpha_prior[0], shots - freq + self._alpha_prior[1]] - alpha_sum = sum(alpha_posterior) - p_mean = alpha_posterior[0] / alpha_sum - p_var = p_mean * (1 - p_mean) / (alpha_sum + 1) - return p_mean, np.sqrt(p_var) + probabilities[idx] = ufloat(nominal_value=p_mean, std_dev=np.sqrt(p_var)) + + return probabilities class BasisExpectationValue(DataAction): @@ -547,40 +503,33 @@ class BasisExpectationValue(DataAction): The sign becomes P(0) -> 1, P(1) -> -1. """ - def _format_data( - self, datum: np.ndarray, error: Optional[np.ndarray] = None - ) -> Tuple[Any, Any]: - """Check that the input data are probabilities. + def _format_data(self, data: np.ndarray) -> np.ndarray: + """Format and validate the input. Args: - datum: An array representing probabilities. - error: An array representing error. + data: A full data array to format. Returns: - Arrays of probability and its error + The data that has been validated and formatted. Raises: DataProcessorError: When input value is not in [0, 1] """ - if not all(0.0 <= p <= 1.0 for p in datum): - raise DataProcessorError( - f"Input data for node {self.__class__.__name__} is not likely probability." - ) - return datum, error + if self._validate: + if not all(0.0 <= p <= 1.0 for p in data): + raise DataProcessorError( + f"Input data for node {self.__class__.__name__} is not likely probability." + ) - def _process( - self, datum: np.array, error: Optional[np.array] = None - ) -> Tuple[np.array, np.array]: - """Compute eigenvalue. + return data + + def _process(self, data: np.ndarray) -> np.ndarray: + """Compute basis eigenvalue. Args: - datum: An array representing probabilities. - error: An array representing error. + data: A full data array to process. Returns: - Arrays of eigenvalues and its error + The data that has been processed. """ - if error is not None: - return 2 * (0.5 - datum), 2 * error - else: - return 2 * (0.5 - datum), None + return 2 * (0.5 - data) diff --git a/requirements.txt b/requirements.txt index 6380bdb153..39dd8dfc05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ scipy>=1.4 qiskit-terra>=0.18.0 qiskit-ibmq-provider>=0.16.0 matplotlib>=3.4 +uncertainties From 7c2088a7210f4e9026eb7d76c6646a7d5bbfe7e5 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 3 Nov 2021 05:01:48 +0900 Subject: [PATCH 10/55] update note tests --- .../data_processing/data_action.py | 7 +- .../data_processing/data_processor.py | 23 ++- qiskit_experiments/data_processing/nodes.py | 102 ++++-------- test/data_processing/test_data_processing.py | 4 +- test/data_processing/test_nodes.py | 145 ++++++++++++------ 5 files changed, 154 insertions(+), 127 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 46bd8514f3..9ef3429afe 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -13,7 +13,6 @@ """Defines the steps that can be used to analyse data.""" from abc import ABCMeta, abstractmethod -from typing import Any, List import numpy as np @@ -70,7 +69,7 @@ def __call__(self, data: np.ndarray) -> np.ndarray: Returns: The data that has been processed. """ - return self._process(*self._format_data(data)) + return self._process(self._format_data(data)) def __repr__(self): """String representation of the node.""" @@ -92,11 +91,11 @@ def is_trained(self) -> bool: """ @abstractmethod - def train(self, data: List[Any]): + def train(self, data: np.ndarray): """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. + data: A full data array used for training. """ diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 1579323e68..4285a29a38 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -71,7 +71,7 @@ def is_trained(self) -> bool: return True - def __call__(self, data: Union[Dict, List[Dict]], **options) -> np.ndarray: + def __call__(self, data: Union[Dict, List[Dict]], **options) -> Tuple[np.ndarray, np.ndarray]: """ Call self on the given datum. This method sequentially calls the stored data actions on the datum. @@ -85,11 +85,14 @@ def __call__(self, data: Union[Dict, List[Dict]], **options) -> np.ndarray: Returns: processed data: The data processed by the data processor. """ - return self._call_internal(data, **options) + # Convert into conventional data format, + # TODO need upgrade of following steps, i.e. curve fitter + processed_data = self._call_internal(data, **options) + return unp.nominal_values(processed_data), unp.std_devs(processed_data) def call_with_history( self, data: Union[Dict, List[Dict]], history_nodes: Set = None - ) -> Tuple[np.ndarray, List]: + ) -> Tuple[np.ndarray, np.ndarray, 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. @@ -106,7 +109,10 @@ def call_with_history( A tuple of (processed data, history), that are the data processed by the processor and its intermediate state in each specified node, respectively. """ - return self._call_internal(data, True, history_nodes) + # Convert into conventional data format, + # TODO need upgrade of following steps, i.e. curve fitter + processed_data, history = self._call_internal(data, True, history_nodes) + return unp.nominal_values(processed_data), unp.std_devs(processed_data), history def _call_internal( self, @@ -144,23 +150,26 @@ def _call_internal( if with_history and (history_nodes is None or index in history_nodes): history.append((node.__class__.__name__, data_to_process, index)) + # Return only first entry if len(data) == 1 + if data_to_process.shape[-1] == 1: + data_to_process = data_to_process[0] + if with_history: return data_to_process, history else: return data_to_process - def train(self, data: List[Dict[str, Any]]): + def train(self, data: Union[Dict, List[Dict]]): """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 isinstance(node, TrainableDataAction): if not node.is_trained: # Process the data up to the untrained node. - node.train(self._call_internal(data, call_up_to_node=index)[0]) + node.train(self._call_internal(data, call_up_to_node=index)) def _data_extraction(self, data: Union[Dict, List[Dict]]) -> np.ndarray: """Extracts the data on which to run the nodes. diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 8ef5940386..218ec140cb 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -14,7 +14,7 @@ from abc import abstractmethod from numbers import Number -from typing import Any, List, Optional, Tuple, Union, Sequence +from typing import List, Union, Sequence import numpy as np from uncertainties import unumpy as unp, ufloat @@ -56,30 +56,26 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: return data def _process(self, data: np.ndarray) -> np.ndarray: - """Average the data. + r"""Average the data. Args: data: A full data array to format. Notes: - The error propagation is computed if any error values is already involved. - Otherwise error is computed by the standard error of the mean, - i.e. the standard deviation of the datum divided by :math:`sqrt{N}` + Error is computed by 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. + Standard error computed by the previous node will be discarded. Returns: Arrays with one less dimension than the given data. """ - errors = unp.std_devs(data) - - # Error is not defined or no error. Newly compute variance from nominal values. - if np.isnan(errors).all() or not errors.any(): - nominals = unp.nominal_values(data) - errors = np.std(nominals, axis=self._axis) / np.sqrt(nominals.shape[self._axis]) - return unp.uarray(nominal_values=nominals, std_devs=errors) - - # Compute average with error propagation - return np.mean(data, axis=self._axis) + nominals = unp.nominal_values(data) + errors = np.std(nominals, axis=self._axis) / np.sqrt(nominals.shape[self._axis]) + return unp.uarray( + nominal_values=np.average(nominals, axis=self._axis), + std_devs=errors, + ) class MinMaxNormalize(DataAction): @@ -94,9 +90,10 @@ def _process(self, data: np.ndarray) -> np.ndarray: Returns: The data that has been processed. """ - min_y, max_y = np.min(data), np.max(data) + # Drop uncertainty of min max values. This is just mix-max scaling. + nominal_values = unp.nominal_values(data) + min_y, max_y = np.min(nominal_values), np.max(nominal_values) - # Uncertainty of min_y and max_y are also considered. return (data - min_y) / (max_y - min_y) @@ -118,28 +115,22 @@ def __init__(self, validate: bool = True): self._n_slots = 0 self._n_iq = 0 - def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, Any]: + def _format_data(self, data: np.ndarray) -> np.ndarray: """Check that the IQ data is 2D and convert it to a numpy array. Args: - datum: All IQ data. This data has different dimensions depending on whether + data: All IQ data. This data has different dimensions depending on whether single-shot or averaged data is being processed. Single-shot data is four dimensional, i.e., ``[#circuits, #shots, #slots, 2]``, while averaged IQ data is three dimensional, i.e., ``[#circuits, #slots, 2]``. Here, ``#slots`` is the number of classical registers used in the circuit. - error: Optional, accompanied error. Returns: - datum and any error estimate as a numpy array. + data 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) - self._n_circs = 0 self._n_shots = 0 self._n_slots = 0 @@ -148,11 +139,11 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An # identify shape try: # level1 single-shot data - self._n_circs, self._n_shots, self._n_slots, self._n_iq = datum.shape + self._n_circs, self._n_shots, self._n_slots, self._n_iq = data.shape except ValueError: try: # level1 data averaged over shots - self._n_circs, self._n_slots, self._n_iq = datum.shape + self._n_circs, self._n_slots, self._n_iq = data.shape except ValueError as ex: raise DataProcessorError( f"Data given to {self.__class__.__name__} is not likely level1 data." @@ -165,13 +156,7 @@ def _format_data(self, datum: Any, error: Optional[Any] = None) -> Tuple[Any, An f"(I and Q). Instead, {self._n_iq} dimensions were found." ) - if error is not None and error.shape != datum.shape: - raise DataProcessorError( - f"IQ data error given to {self.__class__.__name__} must be a 2D array." - f"Instead, a {len(error.shape)}D array was given." - ) - - return datum, error + return data @property def axis(self) -> List[np.array]: @@ -208,15 +193,11 @@ def is_trained(self) -> bool: """ return self._main_axes is not None - def _process( - self, datum: np.array, error: Optional[np.array] = None - ) -> Tuple[np.array, np.array]: + def _process(self, data: np.ndarray) -> np.ndarray: """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]. + data: All IQ data to be processed. Returns: A Tuple of 1D arrays of the result of the SVD and the associated error. Each entry @@ -236,37 +217,19 @@ def _process( # level1 single mode dims = self._n_circs, self._n_shots, self._n_slots - processed_data = np.zeros(dims, dtype=float) - error_vals = np.zeros(dims, dtype=float) + projected_data = np.zeros(dims, dtype=object) for idx in range(self._n_slots): scale = self.scales[idx] + # error propagation is computed from data if any std error exists centered = np.array( - [datum[..., idx, iq] - self.means(qubit=idx, iq_index=iq) for iq in [0, 1]] + [data[..., idx, iq] - self.means(qubit=idx, iq_index=iq) for iq in [0, 1]] ) - processed_data[..., idx] = (self._main_axes[idx] @ centered) / scale - - if error is not None: - angle = np.arctan(self._main_axes[idx][1] / self._main_axes[idx][0]) - error_vals[..., idx] = ( - np.sqrt( - (error[..., idx, 0] * np.cos(angle)) ** 2 - + (error[..., idx, 1] * np.sin(angle)) ** 2 - ) - / scale - ) - - if self._n_circs == 1: - if error is None: - return processed_data[0], None - else: - return processed_data[0], error_vals[0] + projected_data[..., idx] = (self._main_axes[idx] @ centered) / scale - if error is None: - return processed_data, None - return processed_data, error_vals + return projected_data - def train(self, data: List[Any]): + def train(self, data: np.ndarray): """Train the SVD on the given data. Each element of the given data will be converted to a 2D array of dimension @@ -277,19 +240,20 @@ def train(self, data: List[Any]): 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. + data: A full array of IQ data to be trained. """ if data is None: return - data, _ = self._format_data(data) + # TODO do not remove standard error. Currently svd is not supported. + data = unp.nominal_values(self._format_data(data)) self._main_axes = [] self._scales = [] self._means = [] - for qubit_idx in range(self._n_slots): - datums = np.vstack([datum[qubit_idx] for datum in data]).T + for idx in range(self._n_slots): + datums = np.vstack([datum[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, :]) diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index a4771a159a..59dd433d46 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -81,11 +81,11 @@ def test_empty_processor(self): data_processor = DataProcessor("counts") datum, error = data_processor(self.exp_data_lvl2.data(0)) - self.assertEqual(datum, [{"00": 4, "10": 6}]) + self.assertEqual(datum, {"00": 4, "10": 6}) self.assertIsNone(error) datum, error, history = data_processor.call_with_history(self.exp_data_lvl2.data(0)) - self.assertEqual(datum, [{"00": 4, "10": 6}]) + self.assertEqual(datum, {"00": 4, "10": 6}) self.assertEqual(history, []) def test_to_real(self): diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 1c4392f528..19de4747ea 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -15,6 +15,7 @@ # pylint: disable=unbalanced-tuple-unpacking import numpy as np +from uncertainties import unumpy as unp from qiskit.test import QiskitTestCase from qiskit_experiments.data_processing.nodes import ( @@ -33,17 +34,31 @@ class TestAveraging(BaseDataProcessorTest): def test_simple(self): """Simple test of averaging.""" - datum = np.array([[1, 2], [3, 4], [5, 6]]) node = AverageData(axis=1) - self.assertTrue(np.allclose(node(datum)[0], np.array([1.5, 3.5, 5.5]))) - self.assertTrue(np.allclose(node(datum)[1], np.array([0.5, 0.5, 0.5]) / np.sqrt(2))) + processed_data = node(data=datum) + + np.testing.assert_array_almost_equal( + unp.nominal_values(processed_data), + np.array([1.5, 3.5, 5.5]), + ) + np.testing.assert_array_almost_equal( + unp.std_devs(processed_data), + np.array([0.5, 0.5, 0.5]) / np.sqrt(2), + ) node = AverageData(axis=0) - self.assertTrue(np.allclose(node(datum)[0], np.array([3.0, 4.0]))) - std = np.std([1, 3, 5]) - self.assertTrue(np.allclose(node(datum)[1], np.array([std, std]) / np.sqrt(3))) + processed_data = node(data=datum) + + np.testing.assert_array_almost_equal( + unp.nominal_values(processed_data), + np.array([3.0, 4.0]), + ) + np.testing.assert_array_almost_equal( + unp.std_devs(processed_data), + np.array([1.632993161855452, 1.632993161855452]) / np.sqrt(3), + ) def test_iq_averaging(self): """Test averaging of IQ-data.""" @@ -64,17 +79,23 @@ def test_iq_averaging(self): self.create_experiment(iq_data, single_shot=True) avg_iq = AverageData(axis=0) - - avg_datum, error = avg_iq(self.iq_experiment.data(0)["memory"]) + processed_data = avg_iq(data=np.asarray(self.iq_experiment.data(0)["memory"])) 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)) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed_data), + expected_avg, + decimal=-8, + ) + np.testing.assert_array_almost_equal( + unp.std_devs(processed_data), + expected_std, + decimal=-8, + ) class TestNormalize(QiskitTestCase): @@ -91,9 +112,21 @@ def test_simple(self): node = MinMaxNormalize() - self.assertTrue(np.allclose(node(data)[0], expected_data)) - self.assertTrue(np.allclose(node(data, error)[0], expected_data)) - self.assertTrue(np.allclose(node(data, error)[1], expected_error)) + processed_data = node(data=data) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed_data), + expected_data, + ) + + processed_data = node(data=unp.uarray(nominal_values=data, std_devs=error)) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed_data), + expected_data, + ) + np.testing.assert_array_almost_equal( + unp.std_devs(processed_data), + expected_error, + ) class TestSVD(BaseDataProcessorTest): @@ -110,27 +143,36 @@ def test_simple_data(self): self.create_experiment(iq_data) iq_svd = SVD() - iq_svd.train([datum["memory"] for datum in self.iq_experiment.data()]) + iq_svd.train( + np.asarray([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))) + np.testing.assert_array_almost_equal(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))) + np.testing.assert_array_almost_equal(iq_svd._main_axes[1], np.array([-1, 1]) / np.sqrt(2)) # Note: input data shape [n_circs, n_slots, n_iq] for avg mode simulation - processed, _ = iq_svd(np.array([[[1, 1], [1, -1]]])) - expected = np.array([-1, -1]) / np.sqrt(2) - self.assertTrue(np.allclose(processed, expected)) + processed_data = iq_svd(np.array([[[1, 1], [1, -1]]])) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed_data), + np.array([[-1, -1]]) / np.sqrt(2), + ) - processed, _ = iq_svd(np.array([[[2, 2], [2, -2]]])) - self.assertTrue(np.allclose(processed, expected * 2)) + processed_data = iq_svd(np.array([[[2, 2], [2, -2]]])) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed_data), + 2 * np.array([[-1, -1]]) / np.sqrt(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)) + processed_data = iq_svd(np.array([[[1, -1], [1, 1]]])) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed_data), + np.array([[0, 0]]), + ) def test_svd(self): """Use IQ data gathered from the hardware.""" @@ -154,10 +196,16 @@ def test_svd(self): self.create_experiment(iq_data) iq_svd = SVD() - iq_svd.train([datum["memory"] for datum in self.iq_experiment.data()]) + iq_svd.train( + np.asarray([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]))) + np.testing.assert_array_almost_equal( + iq_svd._main_axes[0], np.array([-0.99633018, -0.08559302]) + ) + np.testing.assert_array_almost_equal( + iq_svd._main_axes[1], np.array([-0.99627747, -0.0862044]) + ) def test_svd_error(self): """Test the error formula of the SVD.""" @@ -168,23 +216,30 @@ def test_svd_error(self): 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])) + processed_data = iq_svd( + unp.uarray(nominal_values=[[[1.0, 0.2]]], std_devs=[[[0.2, 0.1]]]) + ) + self.assertEqual(unp.nominal_values(processed_data), np.array([1.0])) + self.assertEqual(unp.std_devs(processed_data), 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])) + processed_data = iq_svd( + unp.uarray(nominal_values=[[[1.0, 0.2]]], std_devs=[[[0.2, 0.3]]]) + ) + self.assertEqual(unp.nominal_values(processed_data), np.array([1.0])) + self.assertEqual(unp.std_devs(processed_data), np.array([0.2])) # Tilt 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]]]) + + processed_data = iq_svd( + unp.uarray(nominal_values=[[[1.0, 0.0]]], std_devs=[[[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_])) + self.assertEqual(unp.nominal_values(processed_data), np.array([cos_])) expected_error = np.sqrt((0.2 * cos_) ** 2 + (0.3 * sin_) ** 2) - self.assertEqual(error, np.array([expected_error])) + self.assertEqual(unp.std_devs(processed_data), np.array([expected_error])) def test_train_svd_processor(self): """Test that we can train a DataProcessor with an SVD.""" @@ -217,14 +272,14 @@ def test_variance_not_zero(self): node = Probability(outcome="1") data = {"1": 1024, "0": 0} - mode, stderr = node(data) - self.assertGreater(stderr, 0.0) - self.assertLessEqual(mode, 1.0) + processed_data = node(data=np.asarray([data])) + self.assertGreater(unp.std_devs(processed_data), 0.0) + self.assertLessEqual(unp.nominal_values(processed_data), 1.0) data = {"1": 0, "0": 1024} - mode, stderr = node(data) - self.assertGreater(stderr, 0.0) - self.assertGreaterEqual(mode, 0.0) + processed_data = node(data=np.asarray([data])) + self.assertGreater(unp.std_devs(processed_data), 0.0) + self.assertGreater(unp.nominal_values(processed_data), 0.0) def test_probability_balanced(self): """Test if p=0.5 is returned when counts are balanced and prior is flat.""" @@ -232,5 +287,5 @@ def test_probability_balanced(self): # balanced counts with a flat prior will yield p = 0.5 data = {"1": 512, "0": 512} - mode, _ = node(data) - self.assertAlmostEqual(mode, 0.5) + processed_data = node(data=np.asarray([data])) + self.assertAlmostEqual(unp.nominal_values(processed_data), 0.5) From 1507c45e86ab1f427d3ac13b9d6f8007645cba5c Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 3 Nov 2021 05:45:51 +0900 Subject: [PATCH 11/55] fix processor logic --- .../data_processing/data_processor.py | 92 ++++++++++++------- qiskit_experiments/data_processing/nodes.py | 4 +- 2 files changed, 62 insertions(+), 34 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 4285a29a38..ef49c13fa2 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -12,11 +12,11 @@ """Actions done on the data to bring it in a usable form.""" +from typing import Dict, List, Set, Tuple, Union + import numpy as np from uncertainties import unumpy as unp -from typing import Any, Dict, List, Set, Tuple, Union - from qiskit_experiments.data_processing.data_action import DataAction, TrainableDataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -85,10 +85,21 @@ def __call__(self, data: Union[Dict, List[Dict]], **options) -> Tuple[np.ndarray Returns: processed data: The data processed by the data processor. """ - # Convert into conventional data format, + # Convert into conventional data format, This is temporary code block. + # Once following steps support ufloat array, data will be returned as-is. # TODO need upgrade of following steps, i.e. curve fitter processed_data = self._call_internal(data, **options) - return unp.nominal_values(processed_data), unp.std_devs(processed_data) + try: + nominals = unp.nominal_values(processed_data) + stdevs = unp.std_devs(processed_data) + if np.isnan(stdevs).all(): + stdevs = None + except TypeError: + # unprocessed data, can be count dict array. + nominals = processed_data + stdevs = None + + return nominals, stdevs def call_with_history( self, data: Union[Dict, List[Dict]], history_nodes: Set = None @@ -109,10 +120,21 @@ def call_with_history( A tuple of (processed data, history), that are the data processed by the processor and its intermediate state in each specified node, respectively. """ - # Convert into conventional data format, + # Convert into conventional data format, This is temporary code block. + # Once following steps support ufloat array, data will be returned as-is. # TODO need upgrade of following steps, i.e. curve fitter processed_data, history = self._call_internal(data, True, history_nodes) - return unp.nominal_values(processed_data), unp.std_devs(processed_data), history + try: + nominals = unp.nominal_values(processed_data) + stdevs = unp.std_devs(processed_data) + if np.isnan(stdevs).all(): + stdevs = None + except TypeError: + # unprocessed data, can be count dict array. + nominals = processed_data + stdevs = None + + return nominals, stdevs, history def _call_internal( self, @@ -141,23 +163,30 @@ def _call_internal( if call_up_to_node is None: call_up_to_node = len(self._nodes) - data_to_process = self._data_extraction(data) + data = self._data_extraction(data) history = [] for index, node in enumerate(self._nodes[:call_up_to_node]): - data_to_process = node(data_to_process) + data = node(data) if with_history and (history_nodes is None or index in history_nodes): - history.append((node.__class__.__name__, data_to_process, index)) + history.append( + ( + node.__class__.__name__, + unp.nominal_values(data), + unp.std_devs(data), + index, + ) + ) # Return only first entry if len(data) == 1 - if data_to_process.shape[-1] == 1: - data_to_process = data_to_process[0] + if data.shape[-1] == 1: + data = data[0] if with_history: - return data_to_process, history + return data, history else: - return data_to_process + return data def train(self, data: Union[Dict, List[Dict]]): """Train the nodes of the data processor. @@ -209,29 +238,25 @@ def _data_extraction(self, data: Union[Dict, List[Dict]]) -> np.ndarray: f"The input key {self._input_key} was not found in the input datum." ) from error - if self._input_key == "counts": - # asarray(dict) returns zero-dim array since dict is also iterable. - # outcome should be array-like. - outcome = [outcome] - outcome = np.asarray(outcome) - - # Validate data shape - if dims is None: - dims = outcome.shape - else: - # This is because each data node creates full array of all result data. - # Jagged array cannot be numerically operated with numpy array. - if outcome.shape != dims: - raise DataProcessorError( - "Input data is likely a mixture of job results with different measurement " - "configuration. Data processor doesn't support jagged array." - ) - + if self._input_key != "counts": + outcome = np.asarray(outcome) + # Validate data shape + if dims is None: + dims = outcome.shape + else: + # This is because each data node creates full array of all result data. + # Jagged array cannot be numerically operated with numpy array. + if outcome.shape != dims: + raise DataProcessorError( + "Input data is likely a mixture of job results with different " + "measurement setup. Data processor doesn't support jagged array." + ) data_to_process.append(outcome) try: # Likely level1 or below. Return ufloat array with un-computed std_dev. - # This will also return a standard ndarray with dtype=object. + # The output data format is a standard ndarray with dtype=object with + # arbitrary shape [n_circuits, ...] depending on the measurement setup. nominal_values = np.asarray(data_to_process, float) return unp.uarray( nominal_values=nominal_values, @@ -239,6 +264,9 @@ def _data_extraction(self, data: Union[Dict, List[Dict]]) -> np.ndarray: ) except TypeError: # Likely level2 counts or level2 memory data. Cannot be typecasted to ufloat. + # The output data format is a standard ndarray with dtype=object with + # shape [n_circuits] or [n_circuits, n_memory_slot_size]. + # No error value is bound. return np.asarray(data_to_process, dtype=object) def __repr__(self): diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 218ec140cb..d8a925eca1 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -91,8 +91,8 @@ def _process(self, data: np.ndarray) -> np.ndarray: The data that has been processed. """ # Drop uncertainty of min max values. This is just mix-max scaling. - nominal_values = unp.nominal_values(data) - min_y, max_y = np.min(nominal_values), np.max(nominal_values) + nominals = unp.nominal_values(data) + min_y, max_y = np.min(nominals), np.max(nominals) return (data - min_y) / (max_y - min_y) From 02e71266d19ef4f99ebf92dcc9fb94d6573778b6 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 3 Nov 2021 06:30:20 +0900 Subject: [PATCH 12/55] documentation --- .../data_processing/data_processor.py | 39 +++++++++++++++++-- test/data_processing/test_nodes.py | 20 +++------- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index ef49c13fa2..a40b876cdc 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -10,7 +10,39 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Actions done on the data to bring it in a usable form.""" +"""Actions done on the data to bring it in a usable form. + +Data processor is an object that represents a chain of data processing steps to +transform arbitrary input data, e.g. IQ data, into expected format, e.g. population. + +Such transform may take multiple steps, such as kerneling, discrimination, ..., +and each step is implemented as :class:`~qiskit_experiments.data_processing.\ +data_action.DataAction`, namely, `nodes`. + +The processor implements :meth:`__call__` method, thus once its instance is initialized, +this object can be used as if a standard python function: + +.. code-block:: python + + processor = DataProcessor(input_key="memory", [Node1(), Node2(), ...]) + out_data = processor(in_data) + +Usually this is the step beyond detailed data analysis. The data input to the processor is +a sequence of dictionary representing a result of single circuit execution, +and output from the processor is arbitrary numpy array, with shape and data type +depending on the combination of processing nodes. + +The uncertainty arises from quantum measurement or finite sampling may be taken into account +in a specific node, and generated standard error there may be propagated through +numerical operations within the stack. +In Qiskit Experiments, this uncertainty propagation computation is offloaded to +``uncertainties`` package, that offers a python float and numpy-array compatible number +representation that naively supports standard error and computation with it. + +.. _uncertainties: +https://pypi.org/project/uncertainties/ + +""" from typing import Dict, List, Set, Tuple, Union @@ -89,6 +121,7 @@ def __call__(self, data: Union[Dict, List[Dict]], **options) -> Tuple[np.ndarray # Once following steps support ufloat array, data will be returned as-is. # TODO need upgrade of following steps, i.e. curve fitter processed_data = self._call_internal(data, **options) + try: nominals = unp.nominal_values(processed_data) stdevs = unp.std_devs(processed_data) @@ -179,8 +212,8 @@ def _call_internal( ) ) - # Return only first entry if len(data) == 1 - if data.shape[-1] == 1: + # Return only first entry if len(data) == 1, e.g. [[0, 1]] -> [0, 1] + if data.shape[0] == 1: data = data[0] if with_history: diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 19de4747ea..84f1d0d9af 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -143,9 +143,7 @@ def test_simple_data(self): self.create_experiment(iq_data) iq_svd = SVD() - iq_svd.train( - np.asarray([datum["memory"] for datum in self.iq_experiment.data()]) - ) + iq_svd.train(np.asarray([datum["memory"] for datum in self.iq_experiment.data()])) # qubit 0 IQ data is oriented along (1,1) np.testing.assert_array_almost_equal(iq_svd._main_axes[0], np.array([-1, -1]) / np.sqrt(2)) @@ -196,9 +194,7 @@ def test_svd(self): self.create_experiment(iq_data) iq_svd = SVD() - iq_svd.train( - np.asarray([datum["memory"] for datum in self.iq_experiment.data()]) - ) + iq_svd.train(np.asarray([datum["memory"] for datum in self.iq_experiment.data()])) np.testing.assert_array_almost_equal( iq_svd._main_axes[0], np.array([-0.99633018, -0.08559302]) @@ -216,25 +212,19 @@ def test_svd_error(self): iq_svd._means = [[0.0, 0.0]] # Since the axis is along the real part the imaginary error is irrelevant. - processed_data = iq_svd( - unp.uarray(nominal_values=[[[1.0, 0.2]]], std_devs=[[[0.2, 0.1]]]) - ) + processed_data = iq_svd(unp.uarray(nominal_values=[[[1.0, 0.2]]], std_devs=[[[0.2, 0.1]]])) self.assertEqual(unp.nominal_values(processed_data), np.array([1.0])) self.assertEqual(unp.std_devs(processed_data), np.array([0.2])) # Since the axis is along the real part the imaginary error is irrelevant. - processed_data = iq_svd( - unp.uarray(nominal_values=[[[1.0, 0.2]]], std_devs=[[[0.2, 0.3]]]) - ) + processed_data = iq_svd(unp.uarray(nominal_values=[[[1.0, 0.2]]], std_devs=[[[0.2, 0.3]]])) self.assertEqual(unp.nominal_values(processed_data), np.array([1.0])) self.assertEqual(unp.std_devs(processed_data), np.array([0.2])) # Tilt the axis to an angle of 36.9... degrees iq_svd._main_axes = np.array([[0.8, 0.6]]) - processed_data = iq_svd( - unp.uarray(nominal_values=[[[1.0, 0.0]]], std_devs=[[[0.2, 0.3]]]) - ) + processed_data = iq_svd(unp.uarray(nominal_values=[[[1.0, 0.0]]], std_devs=[[[0.2, 0.3]]])) cos_ = np.cos(np.arctan(0.6 / 0.8)) sin_ = np.sin(np.arctan(0.6 / 0.8)) self.assertEqual(unp.nominal_values(processed_data), np.array([cos_])) From 8eaedafc864414621a6df0ecbe43ffe5eef8f999 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 3 Nov 2021 06:33:11 +0900 Subject: [PATCH 13/55] lint --- qiskit_experiments/data_processing/nodes.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index d8a925eca1..ea10368abc 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -45,6 +45,9 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: Returns: The data that has been validated and formatted. + + Raises: + DataProcessorError: When the specified axis does not exist in given array. """ if self._validate: if len(data.shape) <= self._axis: @@ -306,6 +309,9 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: Returns: The data that has been validated and formatted. + + Raises: + DataProcessorError: When input data is not likely IQ data. """ if self._validate: if data.shape[-1] != 2: @@ -438,7 +444,7 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: Raises: DataProcessorError: if the data is not a counts dict or a list of counts dicts. """ - VALID_COUNT = int, float, np.integer + valid_count_type = int, float, np.integer if self._validate: for datum in data: @@ -456,7 +462,7 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: # This variance may be generated by readout error mitigation when # A-matrix is computed with some error. datum[bit_str] = count.nominal_value - if not isinstance(count, VALID_COUNT): + if not isinstance(count, valid_count_type): raise DataProcessorError( f"Count {bit_str} is not a valid value in {self.__class__.__name__}." ) From ef5ae6b6f68d448a68715a0eebcf3912440134ff Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 3 Nov 2021 07:16:42 +0900 Subject: [PATCH 14/55] add reno use np.testing --- .../data_processing/data_processor.py | 2 +- ...grade-data-processor-30204e10e1958c30.yaml | 6 ++++ test/data_processing/test_nodes.py | 35 ++++++++++++------- 3 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 releasenotes/notes/upgrade-data-processor-30204e10e1958c30.yaml diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index a40b876cdc..635657b628 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -298,7 +298,7 @@ def _data_extraction(self, data: Union[Dict, List[Dict]]) -> np.ndarray: except TypeError: # Likely level2 counts or level2 memory data. Cannot be typecasted to ufloat. # The output data format is a standard ndarray with dtype=object with - # shape [n_circuits] or [n_circuits, n_memory_slot_size]. + # shape [n_circuits] or [n_circuits, n_shots]. # No error value is bound. return np.asarray(data_to_process, dtype=object) diff --git a/releasenotes/notes/upgrade-data-processor-30204e10e1958c30.yaml b/releasenotes/notes/upgrade-data-processor-30204e10e1958c30.yaml new file mode 100644 index 0000000000..94de41cc90 --- /dev/null +++ b/releasenotes/notes/upgrade-data-processor-30204e10e1958c30.yaml @@ -0,0 +1,6 @@ +--- +developer: + - | + Data format used in the :py:class:`~qiskit_experiments.data_processing.data_processor.\ + DataProcessor` is upgraded from `Tuple[Any, Any]` to `np.ndarray`. Uncertainty propagation + computation is offloaded to uncertainties package. See module documentation for details. diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 84f1d0d9af..afa7a9f43c 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -137,7 +137,6 @@ 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], [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) @@ -151,8 +150,8 @@ def test_simple_data(self): # qubit 1 IQ data is oriented along (1, -1) np.testing.assert_array_almost_equal(iq_svd._main_axes[1], np.array([-1, 1]) / np.sqrt(2)) - # Note: input data shape [n_circs, n_slots, n_iq] for avg mode simulation - + # This is n_circuit = 1, n_slot = 2, the input shape should be [1, 2, 2] + # Then the output shape will be [1, 2] by reducing the last dimension processed_data = iq_svd(np.array([[[1, 1], [1, -1]]])) np.testing.assert_array_almost_equal( unp.nominal_values(processed_data), @@ -174,7 +173,6 @@ def test_simple_data(self): def test_svd(self): """Use IQ data gathered from the hardware.""" - # This data is primarily oriented along the real axis with a slight tilt. # There is a large offset in the imaginary dimension when comparing qubits # 0 and 1. @@ -205,6 +203,8 @@ def test_svd(self): def test_svd_error(self): """Test the error formula of the SVD.""" + # This is n_circuit = 1, n_slot = 1, the input shape should be [1, 1, 2] + # Then the output shape will be [1, 1] by reducing the last dimension iq_svd = SVD() iq_svd._main_axes = np.array([[1.0, 0.0]]) @@ -213,13 +213,13 @@ def test_svd_error(self): # Since the axis is along the real part the imaginary error is irrelevant. processed_data = iq_svd(unp.uarray(nominal_values=[[[1.0, 0.2]]], std_devs=[[[0.2, 0.1]]])) - self.assertEqual(unp.nominal_values(processed_data), np.array([1.0])) - self.assertEqual(unp.std_devs(processed_data), np.array([0.2])) + np.testing.assert_array_equal(unp.nominal_values(processed_data), np.array([[1.0]])) + np.testing.assert_array_equal(unp.std_devs(processed_data), np.array([[0.2]])) # Since the axis is along the real part the imaginary error is irrelevant. processed_data = iq_svd(unp.uarray(nominal_values=[[[1.0, 0.2]]], std_devs=[[[0.2, 0.3]]])) - self.assertEqual(unp.nominal_values(processed_data), np.array([1.0])) - self.assertEqual(unp.std_devs(processed_data), np.array([0.2])) + np.testing.assert_array_equal(unp.nominal_values(processed_data), np.array([[1.0]])) + np.testing.assert_array_equal(unp.std_devs(processed_data), np.array([[0.2]])) # Tilt the axis to an angle of 36.9... degrees iq_svd._main_axes = np.array([[0.8, 0.6]]) @@ -227,9 +227,14 @@ def test_svd_error(self): processed_data = iq_svd(unp.uarray(nominal_values=[[[1.0, 0.0]]], std_devs=[[[0.2, 0.3]]])) cos_ = np.cos(np.arctan(0.6 / 0.8)) sin_ = np.sin(np.arctan(0.6 / 0.8)) - self.assertEqual(unp.nominal_values(processed_data), np.array([cos_])) - expected_error = np.sqrt((0.2 * cos_) ** 2 + (0.3 * sin_) ** 2) - self.assertEqual(unp.std_devs(processed_data), np.array([expected_error])) + np.testing.assert_array_equal( + unp.nominal_values(processed_data), + np.array([[cos_]]), + ) + np.testing.assert_array_equal( + unp.std_devs(processed_data), + np.array([[np.sqrt((0.2 * cos_) ** 2 + (0.3 * sin_) ** 2)]]), + ) def test_train_svd_processor(self): """Test that we can train a DataProcessor with an SVD.""" @@ -245,13 +250,17 @@ def test_train_svd_processor(self): self.assertTrue(processor.is_trained) + # This is n_circuit = 1, n_slot = 2, the input shape should be [1, 2, 2] + # Then the output shape will be [1, 2] by reducing the last dimension + # Via processor the first dim is also reduced when data len = 1. + # Thus output shape will be [2] + # Check that we can use the SVD iq_data = [[[2, 2], [2, -2]]] self.create_experiment(iq_data) processed, _ = processor(self.iq_experiment.data(0)) - expected = np.array([-2, -2]) / np.sqrt(2) - self.assertTrue(np.allclose(processed, expected)) + np.testing.assert_array_almost_equal(processed, np.array([-2, -2]) / np.sqrt(2)) class TestProbability(QiskitTestCase): From f397a87e5b2a2c4eefe86af81a99d1d18ed05fb5 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Wed, 3 Nov 2021 20:39:18 +0900 Subject: [PATCH 15/55] fix test --- test/data_processing/test_nodes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index afa7a9f43c..83a58e941a 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -213,13 +213,13 @@ def test_svd_error(self): # Since the axis is along the real part the imaginary error is irrelevant. processed_data = iq_svd(unp.uarray(nominal_values=[[[1.0, 0.2]]], std_devs=[[[0.2, 0.1]]])) - np.testing.assert_array_equal(unp.nominal_values(processed_data), np.array([[1.0]])) - np.testing.assert_array_equal(unp.std_devs(processed_data), np.array([[0.2]])) + np.testing.assert_array_almost_equal(unp.nominal_values(processed_data), np.array([[1.0]])) + np.testing.assert_array_almost_equal(unp.std_devs(processed_data), np.array([[0.2]])) # Since the axis is along the real part the imaginary error is irrelevant. processed_data = iq_svd(unp.uarray(nominal_values=[[[1.0, 0.2]]], std_devs=[[[0.2, 0.3]]])) - np.testing.assert_array_equal(unp.nominal_values(processed_data), np.array([[1.0]])) - np.testing.assert_array_equal(unp.std_devs(processed_data), np.array([[0.2]])) + np.testing.assert_array_almost_equal(unp.nominal_values(processed_data), np.array([[1.0]])) + np.testing.assert_array_almost_equal(unp.std_devs(processed_data), np.array([[0.2]])) # Tilt the axis to an angle of 36.9... degrees iq_svd._main_axes = np.array([[0.8, 0.6]]) @@ -227,11 +227,11 @@ def test_svd_error(self): processed_data = iq_svd(unp.uarray(nominal_values=[[[1.0, 0.0]]], std_devs=[[[0.2, 0.3]]])) cos_ = np.cos(np.arctan(0.6 / 0.8)) sin_ = np.sin(np.arctan(0.6 / 0.8)) - np.testing.assert_array_equal( + np.testing.assert_array_almost_equal( unp.nominal_values(processed_data), np.array([[cos_]]), ) - np.testing.assert_array_equal( + np.testing.assert_array_almost_equal( unp.std_devs(processed_data), np.array([[np.sqrt((0.2 * cos_) ** 2 + (0.3 * sin_) ** 2)]]), ) From f4ab97f2fddefb646733843323f29427498fe911 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 06:00:15 +0900 Subject: [PATCH 16/55] Update qiskit_experiments/data_processing/data_action.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_action.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 9ef3429afe..46b3dcb371 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -36,9 +36,9 @@ def _process(self, data: np.ndarray) -> np.ndarray: """Applies the data processing step to the data. Args: - data: A full data array to process. This is numpy array of arbitrary dtype. - If elements are ufloat objects consisting of nominal value and - standard error values, error propagation is automatically computed. + data: A full data array to process. This is a numpy array of arbitrary type. + If the elements are ufloat objects consisting of a nominal value and + a standard error, then the error propagation is automatically computed. Returns: The data that has been processed. From fc1cacfaabae6d23e04d5c5658d4d3374b594eda Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 06:00:22 +0900 Subject: [PATCH 17/55] Update qiskit_experiments/data_processing/data_action.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 46b3dcb371..31aba21a0f 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -41,7 +41,7 @@ def _process(self, data: np.ndarray) -> np.ndarray: a standard error, then the error propagation is automatically computed. Returns: - The data that has been processed. + The processed data. """ def _format_data(self, data: np.ndarray) -> np.ndarray: From 4296bbf2b4874679db7b10f2c450117783b06197 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 06:00:34 +0900 Subject: [PATCH 18/55] Update qiskit_experiments/data_processing/data_action.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_action.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 31aba21a0f..8a6f300e03 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -62,9 +62,9 @@ def __call__(self, data: np.ndarray) -> np.ndarray: """Call the data action of this node on the data. Args: - data: A numpy array with arbitrary dtype. If elements are ufloat objects - consisting of nominal value and standard error values, - error propagation is automatically computed. + data: A numpy array with arbitrary dtype. If the elements are ufloat objects + consisting of a nominal value and a standard error, then the error propagation + is done automatically. Returns: The data that has been processed. From 6374a43258518847fa59d7d6ee5b4718b030b49b Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 06:00:40 +0900 Subject: [PATCH 19/55] Update qiskit_experiments/data_processing/data_action.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 8a6f300e03..3f64c4191b 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -67,7 +67,7 @@ def __call__(self, data: np.ndarray) -> np.ndarray: is done automatically. Returns: - The data that has been processed. + The processed data. """ return self._process(self._format_data(data)) From df8bd60d02b19522829512f9c2e51dc06bad7797 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 06:01:02 +0900 Subject: [PATCH 20/55] Update qiskit_experiments/data_processing/data_processor.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 635657b628..974bc5a013 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -12,8 +12,8 @@ """Actions done on the data to bring it in a usable form. -Data processor is an object that represents a chain of data processing steps to -transform arbitrary input data, e.g. IQ data, into expected format, e.g. population. +A data processor is a chain of data processing steps that transform various input data, +e.g. IQ data, into a desired format, e.g. population, which can be analyzed. Such transform may take multiple steps, such as kerneling, discrimination, ..., and each step is implemented as :class:`~qiskit_experiments.data_processing.\ From 2de64ffe608caa1e9b20fa6d2f46d2e4618f5882 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 06:01:36 +0900 Subject: [PATCH 21/55] Update qiskit_experiments/data_processing/data_processor.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_processor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 974bc5a013..6985cb1754 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -15,9 +15,9 @@ A data processor is a chain of data processing steps that transform various input data, e.g. IQ data, into a desired format, e.g. population, which can be analyzed. -Such transform may take multiple steps, such as kerneling, discrimination, ..., -and each step is implemented as :class:`~qiskit_experiments.data_processing.\ -data_action.DataAction`, namely, `nodes`. +These data transformations may consist of multiple steps, such as kerneling and discrimination. +Each step is implemented by a :class:`~qiskit_experiments.data_processing.\ +data_action.DataAction` also called a `node`. The processor implements :meth:`__call__` method, thus once its instance is initialized, this object can be used as if a standard python function: From 25252b6ee4ce5de2e08bb5cd398719450c9799ef Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 06:01:59 +0900 Subject: [PATCH 22/55] Update qiskit_experiments/data_processing/data_processor.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 6985cb1754..cf8d5ea4c2 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -19,8 +19,8 @@ Each step is implemented by a :class:`~qiskit_experiments.data_processing.\ data_action.DataAction` also called a `node`. -The processor implements :meth:`__call__` method, thus once its instance is initialized, -this object can be used as if a standard python function: +The data processor implements the :meth:`__call__` method. Once initialized, it +can thus be used as a standard python function: .. code-block:: python From f7c1455a27a8d8640207e00471cdee5d4c7c561d Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 06:05:04 +0900 Subject: [PATCH 23/55] Update qiskit_experiments/data_processing/data_processor.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_processor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index cf8d5ea4c2..6b3b178527 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -27,10 +27,9 @@ processor = DataProcessor(input_key="memory", [Node1(), Node2(), ...]) out_data = processor(in_data) -Usually this is the step beyond detailed data analysis. The data input to the processor is -a sequence of dictionary representing a result of single circuit execution, -and output from the processor is arbitrary numpy array, with shape and data type -depending on the combination of processing nodes. +The data input to the processor is a sequence of dictionaries each representing the result +of a single circuit. The output of the processor is a numpy array whose shape and data type +depend on the combination of the nodes in the data processor. The uncertainty arises from quantum measurement or finite sampling may be taken into account in a specific node, and generated standard error there may be propagated through From d1bc09e79d49240c58fa87398cb85a371eb55db4 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 07:34:04 +0900 Subject: [PATCH 24/55] Update qiskit_experiments/data_processing/data_processor.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_processor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 6b3b178527..cfc0469869 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -31,12 +31,11 @@ of a single circuit. The output of the processor is a numpy array whose shape and data type depend on the combination of the nodes in the data processor. -The uncertainty arises from quantum measurement or finite sampling may be taken into account -in a specific node, and generated standard error there may be propagated through -numerical operations within the stack. -In Qiskit Experiments, this uncertainty propagation computation is offloaded to +Uncertainties that arise from quantum measurements or finite sampling can be taken into account +in the nodes: a standard error can be generated in a node and can be propagated through the subsequent +nodes in the data processor. In Qiskit Experiments, this uncertainty propagation is offloaded to the ``uncertainties`` package, that offers a python float and numpy-array compatible number -representation that naively supports standard error and computation with it. +representation that natively supports standard errors and their propagation. .. _uncertainties: https://pypi.org/project/uncertainties/ From 74de5c046f1e3bc2db87ef2197bed339e6592357 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 08:06:24 +0900 Subject: [PATCH 25/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/nodes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index ea10368abc..1457787102 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -316,8 +316,8 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: if self._validate: if data.shape[-1] != 2: raise DataProcessorError( - f"IQ data given to {self.__class__.__name__} must be multi-dimensional array" - "of [d0, d1, ..., 2] in which the last dimension corresponds to IQ elements." + f"IQ data given to {self.__class__.__name__} must be a multi-dimensional array" + "of dimension [d0, d1, ..., 2] in which the last dimension corresponds to IQ elements." f"Input data contains element with length {data.shape[-1]} != 2." ) From 11c3a16f9dd987b1ae8f113384cb7fd0c0a33ddb Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 13 Nov 2021 08:28:49 +0900 Subject: [PATCH 26/55] add description for correlation --- .../data_processing/__init__.py | 28 +++++++++++++-- .../data_processing/data_processor.py | 34 +++++++------------ 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/qiskit_experiments/data_processing/__init__.py b/qiskit_experiments/data_processing/__init__.py index adbbbd716d..6f312470b6 100644 --- a/qiskit_experiments/data_processing/__init__.py +++ b/qiskit_experiments/data_processing/__init__.py @@ -17,9 +17,31 @@ .. currentmodule:: qiskit_experiments.data_processing Data processing is the act of taking taking the data returned by the backend and -converting it into a format that can be analyzed. For instance, counts can be -converted to a probability while two-dimensional IQ data may be converted to a -one-dimensional signal. +converting it into a format that can be analyzed. +It is implemented as a chain of data processing steps that transform various input data, +e.g. IQ data, into a desired format, e.g. population, which can be analyzed. + +These data transformations may consist of multiple steps, such as kerneling and discrimination. +Each step is implemented by a :class:`~qiskit_experiments.data_processing.data_action.DataAction` +also called a `node`. + +The data processor implements the :meth:`__call__` method. Once initialized, it +can thus be used as a standard python function: + +.. code-block:: python + + processor = DataProcessor(input_key="memory", [Node1(), Node2(), ...]) + out_data = processor(in_data) + +The data input to the processor is a sequence of dictionaries each representing the result +of a single circuit. The output of the processor is a numpy array whose shape and data type +depend on the combination of the nodes in the data processor. + +Uncertainties that arise from quantum measurements or finite sampling can be taken into account +in the nodes: a standard error can be generated in a node and can be propagated +through the subsequent nodes in the data processor. +Correlation between computed values is also considered. + Classes ======= diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index cfc0469869..368b0bfcc4 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -10,32 +10,24 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Actions done on the data to bring it in a usable form. +r"""Actions done on the data to bring it in a usable form. -A data processor is a chain of data processing steps that transform various input data, -e.g. IQ data, into a desired format, e.g. population, which can be analyzed. - -These data transformations may consist of multiple steps, such as kerneling and discrimination. -Each step is implemented by a :class:`~qiskit_experiments.data_processing.\ -data_action.DataAction` also called a `node`. - -The data processor implements the :meth:`__call__` method. Once initialized, it -can thus be used as a standard python function: +In Qiskit Experiments, uncertainty propagation is offloaded to the ``uncertainties`` +package, that offers a python float and numpy-array compatible number +representation that natively supports standard errors and their propagation. -.. code-block:: python +Given values :math:`a` and :math:`b` have finite uncertainty, the error propagation +in the function :math:`f` will be computed with derivatives - processor = DataProcessor(input_key="memory", [Node1(), Node2(), ...]) - out_data = processor(in_data) +.. math: -The data input to the processor is a sequence of dictionaries each representing the result -of a single circuit. The output of the processor is a numpy array whose shape and data type -depend on the combination of the nodes in the data processor. + \sigma_f^2 \sim \left| \frac{\partial f}{\partial a} \right|^2 \sigma_a^2 + + \left| \frac{\partial f}{\partial b} \right|^2 \sigma_b^2 + + 2 \frac{\partial f}{\partial a} \frac{\partial f}{\partial b} \sigma_{ab} -Uncertainties that arise from quantum measurements or finite sampling can be taken into account -in the nodes: a standard error can be generated in a node and can be propagated through the subsequent -nodes in the data processor. In Qiskit Experiments, this uncertainty propagation is offloaded to the -``uncertainties`` package, that offers a python float and numpy-array compatible number -representation that natively supports standard errors and their propagation. +where :math:`sigma_a` and :math:`sigma_b` are uncertainty of values and +:math:`sigma_{ab}` is the correlation between them. +You can refer to the ``uncertainties`` package documentation for details. .. _uncertainties: https://pypi.org/project/uncertainties/ From 458e76174bc5c1c8065b485b396644e4b801434a Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 13 Nov 2021 08:30:52 +0900 Subject: [PATCH 27/55] remove typehint for trainable node --- qiskit_experiments/data_processing/data_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 368b0bfcc4..3a4eb4204d 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -61,7 +61,7 @@ class DataProcessor: def __init__( self, input_key: str, - data_actions: List[Union[DataAction, TrainableDataAction]] = None, + data_actions: List[DataAction] = None, ): """Create a chain of data processing actions. @@ -74,7 +74,7 @@ def __init__( self._input_key = input_key self._nodes = data_actions if data_actions else [] - def append(self, node: Union[DataAction, TrainableDataAction]): + def append(self, node: DataAction): """ Append new data action node to this data processor. From e4914694683ac8cadb5d61ccbe36f834651f85be Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 13 Nov 2021 08:42:48 +0900 Subject: [PATCH 28/55] update return doc --- qiskit_experiments/data_processing/data_processor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 3a4eb4204d..e2689bfe02 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -181,7 +181,8 @@ def _call_internal( Returns: When ``with_history`` is ``False`` it returns an numpy array of processed data. - Otherwise it returns a tuple of above with a list of intermediate data at each step. + Otherwise it returns a tuple of (processed data, history) in which the `history` + is a list of intermediate data at each step. """ if call_up_to_node is None: call_up_to_node = len(self._nodes) From a1389e92366b65b06c34bb1a47aa9fd0d2621b30 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 13 Nov 2021 08:45:31 +0900 Subject: [PATCH 29/55] add type check --- qiskit_experiments/data_processing/data_processor.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index e2689bfe02..fe590e38c3 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -194,11 +194,19 @@ def _call_internal( data = node(data) if with_history and (history_nodes is None or index in history_nodes): + try: + nominals = unp.nominal_values(data) + stdevs = unp.std_devs(data) + if np.isnan(stdevs).all(): + stdevs = None + except TypeError: + nominals = data + stdevs = None history.append( ( node.__class__.__name__, - unp.nominal_values(data), - unp.std_devs(data), + nominals, + stdevs, index, ) ) From 811e87f0c5800b79e2627ebb946cec49f77d30c8 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 13 Nov 2021 09:02:36 +0900 Subject: [PATCH 30/55] detail for full array --- .../data_processing/data_action.py | 6 ++++-- qiskit_experiments/data_processing/nodes.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 3f64c4191b..499bcebfec 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -36,7 +36,8 @@ def _process(self, data: np.ndarray) -> np.ndarray: """Applies the data processing step to the data. Args: - data: A full data array to process. This is a numpy array of arbitrary type. + data: A data array to process. This is a single numpy array containing + all circuit results input to the data processor. If the elements are ufloat objects consisting of a nominal value and a standard error, then the error propagation is automatically computed. @@ -51,7 +52,8 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: additionally change the data type, e.g. converting a list to a numpy array. Args: - data: A full data array to format. + data: A data array to format. This is a single numpy array containing + all circuit results input to the data processor. Returns: The data that has been validated and formatted. diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 1457787102..74d6c8dab8 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -41,7 +41,7 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: """Format the data into numpy arrays. Args: - data: A full data array to format. + data: An all-result data array to format. Returns: The data that has been validated and formatted. @@ -62,7 +62,7 @@ def _process(self, data: np.ndarray) -> np.ndarray: r"""Average the data. Args: - data: A full data array to format. + data: An all-result data array to format. Notes: Error is computed by the standard error of the mean, @@ -88,7 +88,7 @@ def _process(self, data: np.ndarray) -> np.ndarray: """Normalize the data to the interval [0, 1]. Args: - data: A full data array to process. + data: An all-result data array to process. Returns: The data that has been processed. @@ -243,7 +243,7 @@ def train(self, data: np.ndarray): qubit so that future data points can be projected onto the axis. Args: - data: A full array of IQ data to be trained. + data: An all-result array of IQ data to be trained. """ if data is None: return @@ -292,7 +292,7 @@ def _process(self, data: np.ndarray) -> np.ndarray: The last dimension of the array should correspond to [real, imaginary] part of data. Args: - data: A full data array to process. + data: An all-result data array to process. Returns: The data that has been processed. @@ -305,7 +305,7 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: additionally change the data type, e.g. converting a list to a numpy array. Args: - data: A full data array to format. + data: An all-result data array to format. Returns: The data that has been validated and formatted. @@ -506,7 +506,7 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: """Format and validate the input. Args: - data: A full data array to format. + data: An all-result data array to format. Returns: The data that has been validated and formatted. @@ -526,7 +526,7 @@ def _process(self, data: np.ndarray) -> np.ndarray: """Compute basis eigenvalue. Args: - data: A full data array to process. + data: An all-result data array to process. Returns: The data that has been processed. From 7ad5f19ddddca3e66a67b288b538fac6e445d2f3 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 09:08:47 +0900 Subject: [PATCH 31/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/nodes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 74d6c8dab8..2bee37d11d 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -65,10 +65,10 @@ def _process(self, data: np.ndarray) -> np.ndarray: data: An all-result data array to format. Notes: - Error is computed by the standard error of the mean, + The error is computed by 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. - Standard error computed by the previous node will be discarded. + Standard errors computed by the previous node are discarded. Returns: Arrays with one less dimension than the given data. From 0290ca775697b9c9e81c5e19429d1d130daf869b Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Sat, 13 Nov 2021 09:09:59 +0900 Subject: [PATCH 32/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- 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 2bee37d11d..bd3ddf9500 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -91,7 +91,7 @@ def _process(self, data: np.ndarray) -> np.ndarray: data: An all-result data array to process. Returns: - The data that has been processed. + The normalized data. """ # Drop uncertainty of min max values. This is just mix-max scaling. nominals = unp.nominal_values(data) From 09403306e8762d97c155723191deded0f38601ab Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 13 Nov 2021 09:12:37 +0900 Subject: [PATCH 33/55] add comment --- 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 bd3ddf9500..c680104f6b 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -92,8 +92,11 @@ def _process(self, data: np.ndarray) -> np.ndarray: Returns: The normalized data. + + Notes: + This doesn't consider the uncertainties of the minimum or maximum values. + Input data array is just scaled by the data range. """ - # Drop uncertainty of min max values. This is just mix-max scaling. nominals = unp.nominal_values(data) min_y, max_y = np.min(nominals), np.max(nominals) From 4177cc8132596dd76922728ca0ee08e7be45c54d Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 13 Nov 2021 09:17:53 +0900 Subject: [PATCH 34/55] fix document --- qiskit_experiments/data_processing/nodes.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index c680104f6b..ab5ccbc3cb 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -304,9 +304,6 @@ def _process(self, data: np.ndarray) -> np.ndarray: def _format_data(self, data: np.ndarray) -> np.ndarray: """Format and validate the input. - Check that the given data has the correct structure. This method may - additionally change the data type, e.g. converting a list to a numpy array. - Args: data: An all-result data array to format. @@ -320,7 +317,8 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: if data.shape[-1] != 2: raise DataProcessorError( f"IQ data given to {self.__class__.__name__} must be a multi-dimensional array" - "of dimension [d0, d1, ..., 2] in which the last dimension corresponds to IQ elements." + "of dimension [d0, d1, ..., 2] in which the last dimension " + "corresponds to IQ elements." f"Input data contains element with length {data.shape[-1]} != 2." ) From 9cab07d77c1ed4defa976a9ceea9ee57bfa276b0 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 13 Nov 2021 09:31:25 +0900 Subject: [PATCH 35/55] black --- qiskit_experiments/data_processing/data_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 499bcebfec..b1efa6ea62 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -65,7 +65,7 @@ def __call__(self, data: np.ndarray) -> np.ndarray: Args: data: A numpy array with arbitrary dtype. If the elements are ufloat objects - consisting of a nominal value and a standard error, then the error propagation + consisting of a nominal value and a standard error, then the error propagation is done automatically. Returns: From 4159d7aa1245920925a26765c2f3651f17147d20 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 16 Nov 2021 05:33:49 +0900 Subject: [PATCH 36/55] Update qiskit_experiments/data_processing/__init__.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/__init__.py b/qiskit_experiments/data_processing/__init__.py index 6f312470b6..2042ca1ce6 100644 --- a/qiskit_experiments/data_processing/__init__.py +++ b/qiskit_experiments/data_processing/__init__.py @@ -16,7 +16,7 @@ .. currentmodule:: qiskit_experiments.data_processing -Data processing is the act of taking taking the data returned by the backend and +Data processing is the act of taking the data returned by the backend and converting it into a format that can be analyzed. It is implemented as a chain of data processing steps that transform various input data, e.g. IQ data, into a desired format, e.g. population, which can be analyzed. From 571b30b3b1aef47eb68c9f195516229fc46f57cd Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 16 Nov 2021 05:34:56 +0900 Subject: [PATCH 37/55] Update qiskit_experiments/data_processing/data_processor.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index fe590e38c3..78fdda71f7 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -16,8 +16,8 @@ package, that offers a python float and numpy-array compatible number representation that natively supports standard errors and their propagation. -Given values :math:`a` and :math:`b` have finite uncertainty, the error propagation -in the function :math:`f` will be computed with derivatives +Given values :math:`a` and :math:`b` with a finite uncertainty, the error propagation +in the function :math:`f` is computed with derivatives .. math: From 248cf256c5f825799ce25f2519cf5be7938bba09 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 16 Nov 2021 05:35:18 +0900 Subject: [PATCH 38/55] Update qiskit_experiments/data_processing/data_processor.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_processor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 78fdda71f7..72eaf8495e 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -25,9 +25,9 @@ + \left| \frac{\partial f}{\partial b} \right|^2 \sigma_b^2 + 2 \frac{\partial f}{\partial a} \frac{\partial f}{\partial b} \sigma_{ab} -where :math:`sigma_a` and :math:`sigma_b` are uncertainty of values and -:math:`sigma_{ab}` is the correlation between them. -You can refer to the ``uncertainties`` package documentation for details. +where :math:`sigma_a` and :math:`sigma_b` are the uncertainties of :math:`a` and :math:`b` while +:math:`sigma_{ab}` is the correlation between :math:`a` and :math:`b`. +Please refer to the ``uncertainties`` package documentation for additional details. .. _uncertainties: https://pypi.org/project/uncertainties/ From 1b3aea08e18fe0b2e017aee5a7cce00e891f8c83 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 16 Nov 2021 06:38:15 +0900 Subject: [PATCH 39/55] update docs --- qiskit_experiments/data_processing/nodes.py | 39 ++++++++++++++------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index ab5ccbc3cb..6fef5f8f7a 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -41,7 +41,8 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: """Format the data into numpy arrays. Args: - data: An all-result data array to format. + data: A data array to format. This is a single numpy array containing + all circuit results input to the data processor. Returns: The data that has been validated and formatted. @@ -62,7 +63,8 @@ def _process(self, data: np.ndarray) -> np.ndarray: r"""Average the data. Args: - data: An all-result data array to format. + data: A data array to process. This is a single numpy array containing + all circuit results input to the data processor. Notes: The error is computed by the standard error of the mean, @@ -88,7 +90,8 @@ def _process(self, data: np.ndarray) -> np.ndarray: """Normalize the data to the interval [0, 1]. Args: - data: An all-result data array to process. + data: A data array to process. This is a single numpy array containing + all circuit results input to the data processor. Returns: The normalized data. @@ -125,7 +128,9 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: """Check that the IQ data is 2D and convert it to a numpy array. Args: - data: All IQ data. This data has different dimensions depending on whether + data: A data array to format. This is a single numpy array containing + all circuit results input to the data processor. + This data has different dimensions depending on whether single-shot or averaged data is being processed. Single-shot data is four dimensional, i.e., ``[#circuits, #shots, #slots, 2]``, while averaged IQ data is three dimensional, i.e., ``[#circuits, #slots, 2]``. @@ -203,7 +208,8 @@ def _process(self, data: np.ndarray) -> np.ndarray: """Project the IQ data onto the axis defined by an SVD and scale it. Args: - data: All IQ data to be processed. + data: A data array to process. This is a single numpy array containing + all circuit results input to the data processor. Returns: A Tuple of 1D arrays of the result of the SVD and the associated error. Each entry @@ -246,7 +252,8 @@ def train(self, data: np.ndarray): qubit so that future data points can be projected onto the axis. Args: - data: An all-result array of IQ data to be trained. + data: A data array to be trained. This is a single numpy array containing + all circuit results input to the data processor. """ if data is None: return @@ -295,7 +302,8 @@ def _process(self, data: np.ndarray) -> np.ndarray: The last dimension of the array should correspond to [real, imaginary] part of data. Args: - data: An all-result data array to process. + data: A data array to process. This is a single numpy array containing + all circuit results input to the data processor. Returns: The data that has been processed. @@ -305,7 +313,8 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: """Format and validate the input. Args: - data: An all-result data array to format. + data: A data array to format. This is a single numpy array containing + all circuit results input to the data processor. Returns: The data that has been validated and formatted. @@ -437,7 +446,8 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: Checks that the given data has a counts format. Args: - data: An object data array containing count dictionaries. + data: A data array to format. This is a single numpy array containing + all circuit results input to the data processor. Returns: The ``data`` as given. @@ -475,7 +485,10 @@ def _process(self, data: np.ndarray) -> np.ndarray: """Compute mean and standard error from the beta distribution. Args: - data: A object data array of counts dictionary. + data: A data array to process. This is a single numpy array containing + all circuit results input to the data processor. + This is usually object data type containing Python dictionaries of + count data keyed on the measured bitstring. Returns: The data that has been processed. @@ -507,7 +520,8 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: """Format and validate the input. Args: - data: An all-result data array to format. + data: A data array to format. This is a single numpy array containing + all circuit results input to the data processor. Returns: The data that has been validated and formatted. @@ -527,7 +541,8 @@ def _process(self, data: np.ndarray) -> np.ndarray: """Compute basis eigenvalue. Args: - data: An all-result data array to process. + data: A data array to process. This is a single numpy array containing + all circuit results input to the data processor. Returns: The data that has been processed. From ff19191c968c3e6ebb8879bd0c574aa62d047f4d Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 16 Nov 2021 06:42:46 +0900 Subject: [PATCH 40/55] update docs --- qiskit_experiments/data_processing/data_action.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index b1efa6ea62..8a075ef58f 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -99,5 +99,7 @@ def train(self, data: np.ndarray): Certain data processing nodes, such as a SVD, require data to first train. Args: - data: A full data array used for training. + data: A data array for training. This is a single numpy array containing + all circuit results input to the data processor :meth:`~qiskit_experiments.\ + data_processing.data_processor.DataProcessor#train` method. """ From 45003dc5f78c90c3ce5b10238bf6f079b9ee50f0 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 16 Nov 2021 06:54:56 +0900 Subject: [PATCH 41/55] restrict count validation --- qiskit_experiments/data_processing/nodes.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 6fef5f8f7a..ae4bcfe326 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -448,14 +448,18 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: Args: data: A data array to format. This is a single numpy array containing all circuit results input to the data processor. + This is usually object data type containing Python dictionaries of + count data keyed on the measured bitstring. + Count value should be a discrete quantity representing the frequency of event, + and no uncertainty should be associated with the value. Returns: The ``data`` as given. Raises: - DataProcessorError: if the data is not a counts dict or a list of counts dicts. + DataProcessorError: If the data is not a counts dict or a list of counts dicts. """ - valid_count_type = int, float, np.integer + valid_count_type = int, np.integer if self._validate: for datum in data: @@ -468,17 +472,14 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: raise DataProcessorError( f"Key {bit_str} is not a valid count key in {self.__class__.__name__}." ) - if isinstance(count, Variable): - # This code eliminates variance in count values. - # This variance may be generated by readout error mitigation when - # A-matrix is computed with some error. - datum[bit_str] = count.nominal_value if not isinstance(count, valid_count_type): raise DataProcessorError( - f"Count {bit_str} is not a valid value in {self.__class__.__name__}." + f"Count {bit_str} is not a valid value in {self.__class__.__name__}. " + "The uncertainty of probability is computed based on sampling error, " + "thus the count should be an error-free discrete quantity " + "representing the frequency of event." ) - # This is bare ndarray. No error will be attached to data. return data def _process(self, data: np.ndarray) -> np.ndarray: From 006964f7890da91aab513af28b904f6e767a892f Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 20 Nov 2021 16:33:58 +0900 Subject: [PATCH 42/55] update return type of data processor --- .../curve_analysis/curve_analysis.py | 24 +- .../data_processing/data_processor.py | 63 ++---- test/data_processing/test_data_processing.py | 207 ++++++++++++------ test/data_processing/test_nodes.py | 26 --- 4 files changed, 168 insertions(+), 152 deletions(-) diff --git a/qiskit_experiments/curve_analysis/curve_analysis.py b/qiskit_experiments/curve_analysis/curve_analysis.py index 28468eac0e..815e7ac9c3 100644 --- a/qiskit_experiments/curve_analysis/curve_analysis.py +++ b/qiskit_experiments/curve_analysis/curve_analysis.py @@ -21,6 +21,7 @@ import warnings from abc import ABC from typing import Any, Dict, List, Tuple, Callable, Union, Optional +from uncertainties import unumpy as unp import numpy as np from qiskit.providers import Backend @@ -577,18 +578,22 @@ def _is_target_series(datum, **filters): x_key = self._get_option("x_key") try: - x_values = [datum["metadata"][x_key] for datum in data] + x_values = np.asarray([datum["metadata"][x_key] for datum in data], dtype=float) except KeyError as ex: raise DataProcessorError( f"X value key {x_key} is not defined in circuit metadata." ) from ex if isinstance(data_processor, DataProcessor): - y_values, y_sigmas = data_processor(data) - if y_sigmas is None: - y_sigmas = np.full(y_values.shape, np.nan) + y_data = data_processor(data) + + y_nominals = unp.nominal_values(y_data) + y_stderrs = unp.std_devs(y_data) else: - y_values, y_sigmas = zip(*map(data_processor, data)) + y_nominals, y_stderrs = zip(*map(data_processor, data)) + + y_nominals = np.asarray(y_nominals, dtype=float) + y_stderrs = np.asarray(y_stderrs, dtype=float) # Store metadata metadata = np.asarray([datum["metadata"] for datum in data], dtype=object) @@ -596,11 +601,6 @@ def _is_target_series(datum, **filters): # Store shots shots = np.asarray([datum.get("shots", np.nan) for datum in data]) - # Format data - x_values = np.asarray(x_values, dtype=float) - y_values = np.asarray(y_values, dtype=float) - y_sigmas = np.asarray(y_sigmas, dtype=float) - # Find series (invalid data is labeled as -1) data_index = np.full(x_values.size, -1, dtype=int) for idx, series_def in enumerate(self.__series__): @@ -613,8 +613,8 @@ def _is_target_series(datum, **filters): raw_data = CurveData( label="raw_data", x=x_values, - y=y_values, - y_err=y_sigmas, + y=y_nominals, + y_err=y_stderrs, shots=shots, data_index=data_index, metadata=metadata, diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 72eaf8495e..cea23968a2 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -93,7 +93,7 @@ def is_trained(self) -> bool: return True - def __call__(self, data: Union[Dict, List[Dict]], **options) -> Tuple[np.ndarray, np.ndarray]: + def __call__(self, data: Union[Dict, List[Dict]], **options) -> np.ndarray: """ Call self on the given datum. This method sequentially calls the stored data actions on the datum. @@ -105,28 +105,14 @@ def __call__(self, data: Union[Dict, List[Dict]], **options) -> Tuple[np.ndarray 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. + The data processed by the data processor. This is arbitrary numpy array that + may contain standard error as ufloat object. """ - # Convert into conventional data format, This is temporary code block. - # Once following steps support ufloat array, data will be returned as-is. - # TODO need upgrade of following steps, i.e. curve fitter - processed_data = self._call_internal(data, **options) - - try: - nominals = unp.nominal_values(processed_data) - stdevs = unp.std_devs(processed_data) - if np.isnan(stdevs).all(): - stdevs = None - except TypeError: - # unprocessed data, can be count dict array. - nominals = processed_data - stdevs = None - - return nominals, stdevs + return self._call_internal(data, **options) def call_with_history( self, data: Union[Dict, List[Dict]], history_nodes: Set = None - ) -> Tuple[np.ndarray, np.ndarray, List]: + ) -> Tuple[np.ndarray, 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. @@ -143,21 +129,7 @@ def call_with_history( A tuple of (processed data, history), that are the data processed by the processor and its intermediate state in each specified node, respectively. """ - # Convert into conventional data format, This is temporary code block. - # Once following steps support ufloat array, data will be returned as-is. - # TODO need upgrade of following steps, i.e. curve fitter - processed_data, history = self._call_internal(data, True, history_nodes) - try: - nominals = unp.nominal_values(processed_data) - stdevs = unp.std_devs(processed_data) - if np.isnan(stdevs).all(): - stdevs = None - except TypeError: - # unprocessed data, can be count dict array. - nominals = processed_data - stdevs = None - - return nominals, stdevs, history + return self._call_internal(data, True, history_nodes) def _call_internal( self, @@ -194,31 +166,28 @@ def _call_internal( data = node(data) if with_history and (history_nodes is None or index in history_nodes): - try: - nominals = unp.nominal_values(data) - stdevs = unp.std_devs(data) - if np.isnan(stdevs).all(): - stdevs = None - except TypeError: - nominals = data - stdevs = None + if data.shape[0] == 1: + cache_data = data[0] + else: + cache_data = data history.append( ( node.__class__.__name__, - nominals, - stdevs, + cache_data, index, ) ) # Return only first entry if len(data) == 1, e.g. [[0, 1]] -> [0, 1] if data.shape[0] == 1: - data = data[0] + out_data = data[0] + else: + out_data = data if with_history: - return data, history + return out_data, history else: - return data + return out_data def train(self, data: Union[Dict, List[Dict]]): """Train the nodes of the data processor. diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 59dd433d46..2e62a357b6 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -17,6 +17,7 @@ from test.fake_experiment import FakeExperiment import numpy as np +from uncertainties import unumpy as unp from qiskit.result.models import ExperimentResultData, ExperimentResult from qiskit.result import Result @@ -80,11 +81,10 @@ def test_empty_processor(self): """Check that a DataProcessor without steps does nothing.""" data_processor = DataProcessor("counts") - datum, error = data_processor(self.exp_data_lvl2.data(0)) + datum = data_processor(self.exp_data_lvl2.data(0)) self.assertEqual(datum, {"00": 4, "10": 6}) - self.assertIsNone(error) - datum, error, history = data_processor.call_with_history(self.exp_data_lvl2.data(0)) + datum, history = data_processor.call_with_history(self.exp_data_lvl2.data(0)) self.assertEqual(datum, {"00": 4, "10": 6}) self.assertEqual(history, []) @@ -96,7 +96,7 @@ def test_to_real(self): exp_data.add_data(self.result_lvl1) # Test to real on a single datum - new_data, error = processor(exp_data.data(0)) + new_data = processor(exp_data.data(0)) expected_old = { "memory": [ @@ -113,20 +113,29 @@ def test_to_real(self): expected_new = np.array([[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]]) self.assertEqual(exp_data.data(0), expected_old) - self.assertTrue(np.allclose(new_data, expected_new)) - self.assertIsNone(error) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + expected_new, + ) + self.assertTrue(np.isnan(unp.std_devs(new_data)).all()) # Test that we can call with history. - new_data, error, history = processor.call_with_history(exp_data.data(0)) + new_data, 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)) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + expected_new, + ) self.assertEqual(history[0][0], "ToReal") - self.assertTrue(np.allclose(history[0][1], expected_new)) + np.testing.assert_array_almost_equal( + unp.nominal_values(history[0][1]), + expected_new, + ) # Test to real on more than one datum - new_data, error = processor(exp_data.data()) + new_data = processor(exp_data.data()) expected_new = np.array( [ @@ -134,8 +143,10 @@ def test_to_real(self): [[5131.962, 4438.87], [3415.985, 2942.458], [5199.964, 4030.843]], ] ) - - self.assertTrue(np.allclose(new_data, expected_new)) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + expected_new, + ) def test_to_imag(self): """Test that we can average the data.""" @@ -145,7 +156,7 @@ def test_to_imag(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - new_data, error = processor(exp_data.data(0)) + new_data = processor(exp_data.data(0)) expected_old = { "memory": [ @@ -168,19 +179,28 @@ def test_to_imag(self): ) self.assertEqual(exp_data.data(0), expected_old) - self.assertTrue(np.allclose(new_data, expected_new)) - self.assertIsNone(error) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + expected_new, + ) + self.assertTrue(np.isnan(unp.std_devs(new_data)).all()) # Test that we can call with history. - new_data, error, history = processor.call_with_history(exp_data.data(0)) + new_data, 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)) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + expected_new, + ) self.assertEqual(history[0][0], "ToImag") - self.assertTrue(np.allclose(history[0][1], expected_new)) + np.testing.assert_array_almost_equal( + unp.nominal_values(history[0][1]), + expected_new, + ) # Test to imaginary on more than one datum - new_data, error = processor(exp_data.data()) + new_data = processor(exp_data.data()) expected_new = np.array( [ @@ -189,7 +209,10 @@ def test_to_imag(self): ] ) - self.assertTrue(np.allclose(new_data, expected_new)) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + expected_new, + ) def test_populations(self): """Test that counts are properly converted to a population.""" @@ -198,14 +221,17 @@ def test_populations(self): processor.append(Probability("00", alpha_prior=1.0)) # Test on a single datum. - new_data, error = processor(self.exp_data_lvl2.data(0)) + new_data = processor(self.exp_data_lvl2.data(0)) - self.assertAlmostEqual(float(new_data), 0.41666667) - self.assertAlmostEqual(float(error), 0.13673544235706114) + self.assertAlmostEqual(float(unp.nominal_values(new_data)), 0.41666667) + self.assertAlmostEqual(float(unp.std_devs(new_data)), 0.13673544235706114) # Test on all the data - new_data, error = processor(self.exp_data_lvl2.data()) - np.testing.assert_array_almost_equal(new_data, np.array([0.41666667, 0.25])) + new_data = processor(self.exp_data_lvl2.data()) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + np.array([0.41666667, 0.25]), + ) def test_validation(self): """Test the validation mechanism.""" @@ -267,7 +293,7 @@ def test_avg_and_single(self): to_imag = DataProcessor("memory", [ToImag(scale=1)]) # Test the real single shot node - new_data, error = to_real(self.exp_data_single.data(0)) + new_data = to_real(self.exp_data_single.data(0)) expected = np.array( [ [-56470872.0, -53407256.0], @@ -278,11 +304,14 @@ def test_avg_and_single(self): [51426688.0, 34330920.0], ] ) - self.assertTrue(np.allclose(new_data, expected)) - self.assertIsNone(error) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + expected, + ) + self.assertTrue(np.isnan(unp.std_devs(new_data)).all()) # Test the imaginary single shot node - new_data, error = to_imag(self.exp_data_single.data(0)) + new_data = to_imag(self.exp_data_single.data(0)) expected = np.array( [ [-136691568.0, -176278624.0], @@ -293,15 +322,24 @@ def test_avg_and_single(self): [-142703104.0, -185572592.0], ] ) - self.assertTrue(np.allclose(new_data, expected)) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + expected, + ) # Test the real average node - new_data, error = to_real(self.exp_data_avg.data(0)) - self.assertTrue(np.allclose(new_data, np.array([-539698.0, 5541283.0]))) + new_data = to_real(self.exp_data_avg.data(0)) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + np.array([-539698.0, 5541283.0]), + ) # Test the imaginary average node - new_data, error = to_imag(self.exp_data_avg.data(0)) - self.assertTrue(np.allclose(new_data, np.array([-153030784.0, -160369600.0]))) + new_data = to_imag(self.exp_data_avg.data(0)) + np.testing.assert_array_almost_equal( + unp.nominal_values(new_data), + np.array([-153030784.0, -160369600.0]), + ) class TestAveragingAndSVD(BaseDataProcessorTest): @@ -401,18 +439,28 @@ def test_averaging(self): processor = DataProcessor("memory", [AverageData(axis=1)]) # 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]]) / 2.0 - self.assertTrue(np.allclose(processed, expected_avg)) - self.assertTrue(np.allclose(error, expected_std)) + processed = processor(self.data.data(0)) + + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + np.array([[1.0, 1.0], [-1.0, 1.0]]), + ) + np.testing.assert_array_almost_equal( + unp.std_devs(processed), + np.array([[0.15811388300841894, 0.1], [0.15811388300841894, 0.0]]) / 2.0, + ) # 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]]) / 2.0 - self.assertTrue(np.allclose(processed, expected_avg)) - self.assertTrue(np.allclose(error, expected_std)) + processed = processor(self.data.data(1)) + + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + np.array([[-1.0, -1.0], [1.0, -1.0]]), + ) + np.testing.assert_array_almost_equal( + unp.std_devs(processed), + np.array([[0.15811388300841894, 0.1], [0.15811388300841894, 0.0]]) / 2.0, + ) def test_averaging_and_svd(self): """Test averaging followed by a SVD.""" @@ -425,23 +473,40 @@ def test_averaging_and_svd(self): self.assertTrue(processor.is_trained) # Test the excited state - processed, error = processor(self.data.data(0)) - self.assertTrue(np.allclose(processed, self._sig_es)) + processed = processor(self.data.data(0)) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + self._sig_es, + ) # Test the ground state - processed, error = processor(self.data.data(1)) - self.assertTrue(np.allclose(processed, self._sig_gs)) + processed = processor(self.data.data(1)) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + self._sig_gs, + ) # Test the x90p rotation - processed, error = processor(self.data.data(2)) - self.assertTrue(np.allclose(processed, self._sig_x90)) - self.assertTrue(np.allclose(error, np.array([0.25, 0.25]))) + processed = processor(self.data.data(2)) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + self._sig_x90, + ) + np.testing.assert_array_almost_equal( + unp.std_devs(processed), + np.array([0.25, 0.25]), + ) # Test the x45p rotation - processed, error = processor(self.data.data(3)) - expected_std = np.array([np.std([1, 1, 1, -1]) / np.sqrt(4.0) / 2] * 2) - self.assertTrue(np.allclose(processed, self._sig_x45)) - self.assertTrue(np.allclose(error, expected_std)) + processed = processor(self.data.data(3)) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + self._sig_x45, + ) + np.testing.assert_array_almost_equal( + unp.std_devs(processed), + np.array([np.std([1, 1, 1, -1]) / np.sqrt(4.0) / 2] * 2), + ) def test_process_all_data(self): """Test that we can process all data at once.""" @@ -463,13 +528,19 @@ def test_process_all_data(self): ) # Test processing of all data - processed = processor(self.data.data())[0] - self.assertTrue(np.allclose(processed, all_expected)) + processed = processor(self.data.data()) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + all_expected, + ) # Test processing of each datum individually for idx, expected in enumerate([self._sig_es, self._sig_gs, self._sig_x90, self._sig_x45]): - processed = processor(self.data.data(idx))[0] - self.assertTrue(np.allclose(processed, expected)) + processed = processor(self.data.data(idx)) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + expected, + ) def test_normalize(self): """Test that by adding a normalization node we get a signal between 1 and 1.""" @@ -480,11 +551,12 @@ def test_normalize(self): processor.train([self.data.data(idx) for idx in [0, 1]]) self.assertTrue(processor.is_trained) - all_expected = np.array([[0.0, 1.0], [1.0, 0.0], [0.5, 0.5], [0.75, 0.25]]) - # Test processing of all data - processed = processor(self.data.data())[0] - self.assertTrue(np.allclose(processed, all_expected)) + processed = processor(self.data.data()) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + np.array([[0.0, 1.0], [1.0, 0.0], [0.5, 0.5], [0.75, 0.25]]), + ) class TestAvgDataAndSVD(BaseDataProcessorTest): @@ -559,8 +631,9 @@ def test_normalize(self): processor.train([self.data.data(idx) for idx in [0, 1]]) self.assertTrue(processor.is_trained) - all_expected = np.array([[0.0, 1.0], [1.0, 0.0], [0.5, 0.5], [0.75, 0.25]]) - # Test processing of all data - processed = processor(self.data.data())[0] - self.assertTrue(np.allclose(processed, all_expected)) + processed = processor(self.data.data()) + np.testing.assert_array_almost_equal( + unp.nominal_values(processed), + np.array([[0.0, 1.0], [1.0, 0.0], [0.5, 0.5], [0.75, 0.25]]), + ) diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 83a58e941a..48125b5ba4 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -236,32 +236,6 @@ def test_svd_error(self): np.array([[np.sqrt((0.2 * cos_) ** 2 + (0.3 * sin_) ** 2)]]), ) - def test_train_svd_processor(self): - """Test that we can train a DataProcessor with an SVD.""" - - processor = DataProcessor("memory", [SVD()]) - - self.assertFalse(processor.is_trained) - - 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()) - - self.assertTrue(processor.is_trained) - - # This is n_circuit = 1, n_slot = 2, the input shape should be [1, 2, 2] - # Then the output shape will be [1, 2] by reducing the last dimension - # Via processor the first dim is also reduced when data len = 1. - # Thus output shape will be [2] - - # Check that we can use the SVD - iq_data = [[[2, 2], [2, -2]]] - self.create_experiment(iq_data) - - processed, _ = processor(self.iq_experiment.data(0)) - np.testing.assert_array_almost_equal(processed, np.array([-2, -2]) / np.sqrt(2)) - class TestProbability(QiskitTestCase): """Test probability computation.""" From 452bf9035f116d2851cc241a560f5f47b294accd Mon Sep 17 00:00:00 2001 From: knzwnao Date: Sat, 20 Nov 2021 17:29:01 +0900 Subject: [PATCH 43/55] lint --- qiskit_experiments/data_processing/nodes.py | 1 - test/data_processing/test_nodes.py | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index ae4bcfe326..c9ccbd9266 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -18,7 +18,6 @@ import numpy as np from uncertainties import unumpy as unp, ufloat -from uncertainties.core import Variable from qiskit_experiments.data_processing.data_action import DataAction, TrainableDataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 48125b5ba4..de4ffeeb00 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -15,17 +15,15 @@ # pylint: disable=unbalanced-tuple-unpacking import numpy as np +from qiskit.test import QiskitTestCase from uncertainties import unumpy as unp -from qiskit.test import QiskitTestCase from qiskit_experiments.data_processing.nodes import ( SVD, AverageData, MinMaxNormalize, Probability, ) -from qiskit_experiments.data_processing.data_processor import DataProcessor - from . import BaseDataProcessorTest From 504c3717165e585fc4ef487fa7ff8b4161eeb484 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Fri, 26 Nov 2021 09:14:04 +0900 Subject: [PATCH 44/55] add svd test --- test/data_processing/test_data_processing.py | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 2e62a357b6..318b88b5e6 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -558,6 +558,30 @@ def test_normalize(self): np.array([[0.0, 1.0], [1.0, 0.0], [0.5, 0.5], [0.75, 0.25]]), ) + def test_distorted_iq_data(self): + """Test if uncertainty can consider correlation. + + SVD projects IQ data onto I-axis, and input different data sets that + have the same mean and same variance but squeezed along different axis. + """ + svd_node = SVD() + svd_node._scales = [1.0] + svd_node._main_axes = [np.array([1, 0])] + svd_node._means = [(0.0, 0.0)] + + processor = DataProcessor("memory", [AverageData(axis=1), svd_node]) + + dist_i_axis = {"memory": [[[-1, 0]], [[-0.5, 0]], [[0.0, 0]], [[0.5, 0]], [[1, 0]]]} + dist_q_axis = {"memory": [[[0, -1]], [[0, -0.5]], [[0, 0.0]], [[0, 0.5]], [[0, 1]]]} + + out_i = processor(dist_i_axis) + self.assertAlmostEqual(out_i[0].nominal_value, 0.0) + self.assertAlmostEqual(out_i[0].std_dev, 0.31622776601683794) + + out_q = processor(dist_q_axis) + self.assertAlmostEqual(out_q[0].nominal_value, 0.0) + self.assertAlmostEqual(out_q[0].std_dev, 0.0) + class TestAvgDataAndSVD(BaseDataProcessorTest): """Test the SVD and normalization on averaged IQ data.""" From 19ff8f1f2a7c246931a6a789d9d327fd85e1e7c7 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Fri, 26 Nov 2021 12:46:50 +0900 Subject: [PATCH 45/55] update average node --- qiskit_experiments/data_processing/nodes.py | 35 ++++++---- test/data_processing/test_nodes.py | 72 ++++++++++++++++----- 2 files changed, 76 insertions(+), 31 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index c9ccbd9266..734a4bc3a6 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -27,11 +27,18 @@ class AverageData(DataAction): """A node to average data representable as numpy arrays.""" def __init__(self, axis: int, validate: bool = True): - """Initialize a data averaging node. + r"""Initialize a data averaging node. Args: axis: The axis along which to average. validate: If set to False the DataAction will not validate its input. + + Notes: + If standard error of input array is not populated, this node will compute + 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. + Otherwise standard error is computed by quadratic sum of the errors of input data + divided by the number of data points, as usual error propagation. """ super().__init__(validate) self._axis = axis @@ -59,27 +66,27 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: return data def _process(self, data: np.ndarray) -> np.ndarray: - r"""Average the data. + """Average the data. Args: data: A data array to process. This is a single numpy array containing all circuit results input to the data processor. - Notes: - The error is computed by 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. - Standard errors computed by the previous node are discarded. - Returns: Arrays with one less dimension than the given data. """ - nominals = unp.nominal_values(data) - errors = np.std(nominals, axis=self._axis) / np.sqrt(nominals.shape[self._axis]) - return unp.uarray( - nominal_values=np.average(nominals, axis=self._axis), - std_devs=errors, - ) + ax = self._axis + + reduced_array = np.mean(data, axis=ax) + nominals = unp.nominal_values(reduced_array) + errors = unp.std_devs(reduced_array) + + if np.any(np.isnan(errors)): + # replace empty elements with SEM + sem = np.std(unp.nominal_values(data), axis=ax) / np.sqrt(data.shape[ax]) + errors = np.where(np.isnan(errors), sem, errors) + + return unp.uarray(nominals, errors) class MinMaxNormalize(DataAction): diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index 37e1e47418..e16e3086e6 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -12,10 +12,9 @@ """Data processor tests.""" -# pylint: disable=unbalanced-tuple-unpacking from test.base import QiskitExperimentsTestCase + import numpy as np -from qiskit.test import QiskitTestCase from uncertainties import unumpy as unp from qiskit_experiments.data_processing.nodes import ( @@ -31,8 +30,8 @@ class TestAveraging(BaseDataProcessorTest): """Test the averaging nodes.""" def test_simple(self): - """Simple test of averaging.""" - datum = np.array([[1, 2], [3, 4], [5, 6]]) + """Simple test of averaging. Standard error of mean is generated.""" + datum = unp.uarray([[1, 2], [3, 4], [5, 6]], np.full((3, 2), np.nan)) node = AverageData(axis=1) processed_data = node(data=datum) @@ -58,23 +57,62 @@ def test_simple(self): np.array([1.632993161855452, 1.632993161855452]) / np.sqrt(3), ) + def test_with_error(self): + """Compute error propagation. This is quadratic sum divided by samples.""" + datum = unp.uarray( + [[1, 2, 3, 4, 5, 6]], + [[0.1, 0.2, 0.3, 0.4, 0.5, 0.6]], + ) + + node = AverageData(axis=1) + processed_data = node(data=datum) + + self.assertAlmostEqual(processed_data[0].nominal_value, 3.5) + self.assertAlmostEqual(processed_data[0].std_dev, 0.15898986690282427) + + def test_with_error_partly_non_error(self): + """Compute error propagation. Some elements have no error.""" + datum = unp.uarray( + [ + [1, 2, 3, 4, 5, 6], + [1, 2, 3, 4, 5, 6], + ], + [ + [0.1, 0.2, 0.3, 0.4, 0.5, 0.6], + [np.nan, 0.2, 0.3, 0.4, 0.5, 0.6], + ], + ) + + node = AverageData(axis=1) + processed_data = node(data=datum) + + self.assertAlmostEqual(processed_data[0].nominal_value, 3.5) + self.assertAlmostEqual(processed_data[0].std_dev, 0.15898986690282427) + + self.assertAlmostEqual(processed_data[1].nominal_value, 3.5) + self.assertAlmostEqual(processed_data[1].std_dev, 0.6972166887783964) + def test_iq_averaging(self): """Test averaging of IQ-data.""" - iq_data = [ - [[-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]], - ] + iq_data = np.array( + [ + [[-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]], + ], + dtype=float, + ) + iq_std = np.full_like(iq_data, np.nan) - self.create_experiment(iq_data, single_shot=True) + self.create_experiment(unp.uarray(iq_data, iq_std), single_shot=True) avg_iq = AverageData(axis=0) processed_data = avg_iq(data=np.asarray(self.iq_experiment.data(0)["memory"])) From 350f724dcd75fd51619ccf1d87b4b0436a3d98f4 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Nov 2021 01:00:21 +0900 Subject: [PATCH 46/55] Update qiskit_experiments/data_processing/data_processor.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/data_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index cea23968a2..ce9523b9f1 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -105,8 +105,8 @@ def __call__(self, data: Union[Dict, List[Dict]], **options) -> np.ndarray: options: Run-time options given as keyword arguments that will be passed to the nodes. Returns: - The data processed by the data processor. This is arbitrary numpy array that - may contain standard error as ufloat object. + The data processed by the data processor. This is an arbitrary numpy array that + may contain standard errors as a ufloat object. """ return self._call_internal(data, **options) From 417c21888fce93c34377c2cde76d7aecf23774af Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Nov 2021 01:00:29 +0900 Subject: [PATCH 47/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- 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 734a4bc3a6..e84367885b 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -34,11 +34,11 @@ def __init__(self, axis: int, validate: bool = True): validate: If set to False the DataAction will not validate its input. Notes: - If standard error of input array is not populated, this node will compute + If the input array has no standard error, then this node will compute 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. - Otherwise standard error is computed by quadratic sum of the errors of input data - divided by the number of data points, as usual error propagation. + Otherwise the standard error is given by the square root of :math:`N^{-1}` times + the sum of the squared errors. """ super().__init__(validate) self._axis = axis From 0a983097f97e1252c968a47146c85be2d21adb1f Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Nov 2021 01:00:35 +0900 Subject: [PATCH 48/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- 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 e84367885b..0b980f5128 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -454,7 +454,7 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: Args: data: A data array to format. This is a single numpy array containing all circuit results input to the data processor. - This is usually object data type containing Python dictionaries of + This is usually an object data type containing Python dictionaries of count data keyed on the measured bitstring. Count value should be a discrete quantity representing the frequency of event, and no uncertainty should be associated with the value. From aa414a715b5468af7833f17e6aaf13931c9355dd Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Nov 2021 01:00:42 +0900 Subject: [PATCH 49/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit_experiments/data_processing/nodes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 0b980f5128..50d5830c47 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -456,8 +456,8 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: all circuit results input to the data processor. This is usually an object data type containing Python dictionaries of count data keyed on the measured bitstring. - Count value should be a discrete quantity representing the frequency of event, - and no uncertainty should be associated with the value. + A count value is a discrete quantity representing the frequency of an event. + Therefore, count values do not have an uncertainty. Returns: The ``data`` as given. From 151e135cf190b8e1ac8b5ac968f64d88218b7a10 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Nov 2021 01:00:48 +0900 Subject: [PATCH 50/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- 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 50d5830c47..2fde8cb239 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -480,7 +480,7 @@ def _format_data(self, data: np.ndarray) -> np.ndarray: ) if not isinstance(count, valid_count_type): raise DataProcessorError( - f"Count {bit_str} is not a valid value in {self.__class__.__name__}. " + f"Count {bit_str} is not a valid count for {self.__class__.__name__}. " "The uncertainty of probability is computed based on sampling error, " "thus the count should be an error-free discrete quantity " "representing the frequency of event." From a3272e81a5fa568264446584064f36c17dee0f86 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 30 Nov 2021 01:00:54 +0900 Subject: [PATCH 51/55] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- 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 2fde8cb239..7dba79f53f 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -494,7 +494,7 @@ def _process(self, data: np.ndarray) -> np.ndarray: Args: data: A data array to process. This is a single numpy array containing all circuit results input to the data processor. - This is usually object data type containing Python dictionaries of + This is usually an object data type containing Python dictionaries of count data keyed on the measured bitstring. Returns: From 5f49f5f41dfe0f8c10a258292dcf42905df7007e Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 30 Nov 2021 01:17:54 +0900 Subject: [PATCH 52/55] add comment --- test/data_processing/test_nodes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/data_processing/test_nodes.py b/test/data_processing/test_nodes.py index e16e3086e6..2fb520c188 100644 --- a/test/data_processing/test_nodes.py +++ b/test/data_processing/test_nodes.py @@ -68,6 +68,7 @@ def test_with_error(self): processed_data = node(data=datum) self.assertAlmostEqual(processed_data[0].nominal_value, 3.5) + # sqrt(0.1**2 + 0.2**2 + ... + 0.6**2) / 6 self.assertAlmostEqual(processed_data[0].std_dev, 0.15898986690282427) def test_with_error_partly_non_error(self): @@ -87,9 +88,11 @@ def test_with_error_partly_non_error(self): processed_data = node(data=datum) self.assertAlmostEqual(processed_data[0].nominal_value, 3.5) + # sqrt(0.1**2 + 0.2**2 + ... + 0.6**2) / 6 self.assertAlmostEqual(processed_data[0].std_dev, 0.15898986690282427) self.assertAlmostEqual(processed_data[1].nominal_value, 3.5) + # sqrt((0.1 - 0.35)**2 + (0.2 - 0.35)**2 + ... + (0.6 - 0.35)**2) / 6 self.assertAlmostEqual(processed_data[1].std_dev, 0.6972166887783964) def test_iq_averaging(self): From a3b731f78ef81b04735f98676b5d0248d86bd8f1 Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 30 Nov 2021 11:59:46 +0900 Subject: [PATCH 53/55] add handling for level2 memory --- .../data_processing/data_processor.py | 6 +- test/data_processing/test_data_processing.py | 135 +++++++++++++++++- 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index ce9523b9f1..7781c2176d 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -254,7 +254,9 @@ def _data_extraction(self, data: Union[Dict, List[Dict]]) -> np.ndarray: ) data_to_process.append(outcome) - try: + data_to_process = np.asarray(data_to_process) + + if data_to_process.dtype in (float, int): # Likely level1 or below. Return ufloat array with un-computed std_dev. # The output data format is a standard ndarray with dtype=object with # arbitrary shape [n_circuits, ...] depending on the measurement setup. @@ -263,7 +265,7 @@ def _data_extraction(self, data: Union[Dict, List[Dict]]) -> np.ndarray: nominal_values=nominal_values, std_devs=np.full_like(nominal_values, np.nan, dtype=float), ) - except TypeError: + else: # Likely level2 counts or level2 memory data. Cannot be typecasted to ufloat. # The output data format is a standard ndarray with dtype=object with # shape [n_circuits] or [n_circuits, n_shots]. diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 318b88b5e6..c40b4f0b06 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -17,7 +17,7 @@ from test.fake_experiment import FakeExperiment import numpy as np -from uncertainties import unumpy as unp +from uncertainties import unumpy as unp, ufloat from qiskit.result.models import ExperimentResultData, ExperimentResult from qiskit.result import Result @@ -77,6 +77,139 @@ def setUp(self): self.exp_data_lvl2 = ExperimentData(FakeExperiment()) self.exp_data_lvl2.add_data(Result(results=[res1, res2], **self.base_result_args)) + def test_data_prep_level1_memory_single(self): + """Format meas_level=1 meas_return=single.""" + # slots = 3, shots = 2, circuits = 2 + data_raw = [ + { + "memory": [ + [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + ], + }, + { + "memory": [ + [[0.7, 0.8], [0.9, 1.0], [1.1, 1.2]], + [[0.7, 0.8], [0.9, 1.0], [1.1, 1.2]], + ], + }, + ] + formatted_data = DataProcessor("memory", [])._data_extraction(data_raw) + + ref_data = np.array( + [ + [ + [ + [ufloat(0.1, np.nan), ufloat(0.2, np.nan)], + [ufloat(0.3, np.nan), ufloat(0.4, np.nan)], + [ufloat(0.5, np.nan), ufloat(0.6, np.nan)], + ], + [ + [ufloat(0.1, np.nan), ufloat(0.2, np.nan)], + [ufloat(0.3, np.nan), ufloat(0.4, np.nan)], + [ufloat(0.5, np.nan), ufloat(0.6, np.nan)], + ], + ], + [ + [ + [ufloat(0.7, np.nan), ufloat(0.8, np.nan)], + [ufloat(0.9, np.nan), ufloat(1.0, np.nan)], + [ufloat(1.1, np.nan), ufloat(1.2, np.nan)], + ], + [ + [ufloat(0.7, np.nan), ufloat(0.8, np.nan)], + [ufloat(0.9, np.nan), ufloat(1.0, np.nan)], + [ufloat(1.1, np.nan), ufloat(1.2, np.nan)], + ], + ], + ] + ) + + self.assertTupleEqual(formatted_data.shape, ref_data.shape) + np.testing.assert_array_equal(unp.nominal_values(formatted_data), unp.nominal_values(ref_data)) + # note that np.nan cannot be evaluated by "==" + self.assertTrue(np.isnan(unp.std_devs(formatted_data)).all()) + + def test_data_prep_level1_memory_average(self): + """Format meas_level=1 meas_return=avg.""" + # slots = 3, circuits = 2 + data_raw = [ + { + "memory": [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], + }, + { + "memory": [[0.7, 0.8], [0.9, 1.0], [1.1, 1.2]], + }, + ] + formatted_data = DataProcessor("memory", [])._data_extraction(data_raw) + + ref_data = np.array( + [ + [ + [ufloat(0.1, np.nan), ufloat(0.2, np.nan)], + [ufloat(0.3, np.nan), ufloat(0.4, np.nan)], + [ufloat(0.5, np.nan), ufloat(0.6, np.nan)], + ], + [ + [ufloat(0.7, np.nan), ufloat(0.8, np.nan)], + [ufloat(0.9, np.nan), ufloat(1.0, np.nan)], + [ufloat(1.1, np.nan), ufloat(1.2, np.nan)], + ], + ] + ) + + self.assertTupleEqual(formatted_data.shape, ref_data.shape) + np.testing.assert_array_equal(unp.nominal_values(formatted_data), unp.nominal_values(ref_data)) + # note that np.nan cannot be evaluated by "==" + self.assertTrue(np.isnan(unp.std_devs(formatted_data)).all()) + + def test_data_prep_level2_counts(self): + """Format meas_level=2.""" + # slots = 2, shots=10, circuits = 2 + data_raw = [ + { + "counts": {"00": 2, "01": 3, "10": 1, "11": 4}, + }, + { + "counts": {"00": 3, "01": 3, "10": 2, "11": 2}, + }, + ] + formatted_data = DataProcessor("counts", [])._data_extraction(data_raw) + + ref_data = np.array( + [ + {"00": 2, "01": 3, "10": 1, "11": 4}, + {"00": 3, "01": 3, "10": 2, "11": 2}, + ], + dtype=object, + ) + + np.testing.assert_array_equal(formatted_data, ref_data) + + def test_data_prep_level2_counts_memory(self): + # slots = 2, shots=10, circuits = 2 + data_raw = [ + { + "counts": {"00": 2, "01": 3, "10": 1, "11": 4}, + "memory": ["00", "01", "01", "10", "11", "11", "00", "01", "11", "11"], + }, + { + "counts": {"00": 3, "01": 3, "10": 2, "11": 2}, + "memory": ["00", "00", "01", "00", "10", "01", "01", "11", "10", "11"], + }, + ] + formatted_data = DataProcessor("memory", [])._data_extraction(data_raw) + + ref_data = np.array( + [ + ["00", "01", "01", "10", "11", "11", "00", "01", "11", "11"], + ["00", "00", "01", "00", "10", "01", "01", "11", "10", "11"], + ], + dtype=object, + ) + + np.testing.assert_array_equal(formatted_data, ref_data) + def test_empty_processor(self): """Check that a DataProcessor without steps does nothing.""" data_processor = DataProcessor("counts") From 10ed78127f4821b6212132cb9912b02fd49108ac Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 30 Nov 2021 12:02:58 +0900 Subject: [PATCH 54/55] black --- qiskit_experiments/data_processing/nodes.py | 2 +- test/data_processing/test_data_processing.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 7dba79f53f..7fb6f7e991 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -37,7 +37,7 @@ def __init__(self, axis: int, validate: bool = True): If the input array has no standard error, then this node will compute 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. - Otherwise the standard error is given by the square root of :math:`N^{-1}` times + Otherwise the standard error is given by the square root of :math:`N^{-1}` times the sum of the squared errors. """ super().__init__(validate) diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index c40b4f0b06..cc6f3503aa 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -126,7 +126,9 @@ def test_data_prep_level1_memory_single(self): ) self.assertTupleEqual(formatted_data.shape, ref_data.shape) - np.testing.assert_array_equal(unp.nominal_values(formatted_data), unp.nominal_values(ref_data)) + np.testing.assert_array_equal( + unp.nominal_values(formatted_data), unp.nominal_values(ref_data) + ) # note that np.nan cannot be evaluated by "==" self.assertTrue(np.isnan(unp.std_devs(formatted_data)).all()) @@ -159,7 +161,9 @@ def test_data_prep_level1_memory_average(self): ) self.assertTupleEqual(formatted_data.shape, ref_data.shape) - np.testing.assert_array_equal(unp.nominal_values(formatted_data), unp.nominal_values(ref_data)) + np.testing.assert_array_equal( + unp.nominal_values(formatted_data), unp.nominal_values(ref_data) + ) # note that np.nan cannot be evaluated by "==" self.assertTrue(np.isnan(unp.std_devs(formatted_data)).all()) From 4cd8c742ae431450a282986680eedc3fdec88fee Mon Sep 17 00:00:00 2001 From: knzwnao Date: Tue, 30 Nov 2021 12:19:06 +0900 Subject: [PATCH 55/55] lint --- test/data_processing/test_data_processing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index cc6f3503aa..98aad43e54 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -191,6 +191,7 @@ def test_data_prep_level2_counts(self): np.testing.assert_array_equal(formatted_data, ref_data) def test_data_prep_level2_counts_memory(self): + """Format meas_level=2 with having memory set.""" # slots = 2, shots=10, circuits = 2 data_raw = [ {