From 3d25f40486b565f551548179e121a1a92402da43 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 26 Mar 2021 09:12:49 +0100 Subject: [PATCH 01/52] * Carved out data processor from PR #20 Co-authored-by: Naoki Kanazawa --- .../data_processing/__init__.py | 27 +++ qiskit_experiments/data_processing/base.py | 138 ++++++++++++ .../data_processing/data_processor.py | 120 ++++++++++ .../data_processing/exceptions.py | 28 +++ qiskit_experiments/data_processing/nodes.py | 205 ++++++++++++++++++ test/data_processing/__init__.py | 0 test/data_processing/test_data_processing.py | 194 +++++++++++++++++ 7 files changed, 712 insertions(+) create mode 100644 qiskit_experiments/data_processing/__init__.py create mode 100644 qiskit_experiments/data_processing/base.py create mode 100644 qiskit_experiments/data_processing/data_processor.py create mode 100644 qiskit_experiments/data_processing/exceptions.py create mode 100644 qiskit_experiments/data_processing/nodes.py create mode 100644 test/data_processing/__init__.py create mode 100644 test/data_processing/test_data_processing.py diff --git a/qiskit_experiments/data_processing/__init__.py b/qiskit_experiments/data_processing/__init__.py new file mode 100644 index 0000000000..d200f22fe9 --- /dev/null +++ b/qiskit_experiments/data_processing/__init__.py @@ -0,0 +1,27 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Qiskit experiments calibration data processing roots.""" + +from .base import DataAction +from .nodes import ( + # data acquisition node + Discriminator, + Kernel, + + # value formatter node + Population, + ToImag, + ToReal +) + +from .data_processor import DataProcessor diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py new file mode 100644 index 0000000000..8ee0c5b467 --- /dev/null +++ b/qiskit_experiments/data_processing/base.py @@ -0,0 +1,138 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Defines the steps that can be used to analyse data.""" + +from abc import ABCMeta, abstractmethod +from enum import Enum +from typing import Any, Dict + +from qiskit_experiments.data_processing.exceptions import DataProcessorError + + +class DataAction(metaclass=ABCMeta): + """ + Abstract action which is a single action done on measured data to process it. + Each subclass of DataAction must define the type of data that it accepts as input + using decorators. + """ + + node_type = None + prev_node = () + + def __init__(self): + """Create new data analysis routine.""" + self._child = None + + @property + def child(self) -> 'DataAction': + """Return the child of this data processing step.""" + return self._child + + def append(self, component: 'DataAction'): + """Add new data processing routine. + + Args: + component: New data processing routine. + + Raises: + DataProcessorError: If the previous node is None (i.e. a root node) + """ + if not component.prev_node: + raise DataProcessorError(f'Analysis routine {component.__class__.__name__} is a root' + f'node. This routine cannot be appended to another node.') + + if self._child is None: + if isinstance(self, component.prev_node): + self._child = component + else: + raise DataProcessorError(f'Analysis routine {component.__class__.__name__} ' + f'cannot be appended after {self.__class__.__name__}') + else: + self._child.append(component) + + @abstractmethod + def process(self, data: Dict[str, Any]): + """ + Applies the data processing step to the data. + + Args: + data: the data to which the data processing step will be applied. + """ + + def format_data(self, data: Dict[str, Any]): + """ + Apply the data action of this node and call the child node's format_data method. + + Args: + data: A dict containing the data. The action nodes in the data + processor will raise errors if the data does not contain the + appropriate data. + """ + processed_data = self.process(data) + + if self._child: + self._child.format_data(processed_data) + + +class NodeType(Enum): + """Type of node that can be supported by the analysis steps.""" + KERNEL = 1 + DISCRIMINATOR = 2 + IQDATA = 3 + COUNTS = 4 + POPULATION = 5 + + +def kernel(cls: DataAction): + """A decorator to give kernel attribute to node.""" + cls.node_type = NodeType.KERNEL + return cls + + +def discriminator(cls: DataAction): + """A decorator to give discriminator attribute to node.""" + cls.node_type = NodeType.DISCRIMINATOR + return cls + + +def iq_data(cls: DataAction): + """A decorator to give iqdata attribute to node.""" + cls.node_type = NodeType.IQDATA + return cls + + +def counts(cls: DataAction): + """A decorator to give counts attribute to node.""" + cls.node_type = NodeType.COUNTS + return cls + + +def population(cls: DataAction): + """A decorator to give population attribute to node.""" + cls.node_type = NodeType.POPULATION + return cls + + +def prev_node(*nodes: DataAction): + """A decorator to specify the available previous nodes.""" + + try: + nodes = list(nodes) + except TypeError: + nodes = [nodes] + + def add_nodes(cls: DataAction): + cls.prev_node = tuple(nodes) + return cls + + return add_nodes diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py new file mode 100644 index 0000000000..cc126881c9 --- /dev/null +++ b/qiskit_experiments/data_processing/data_processor.py @@ -0,0 +1,120 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Class that ties together actions on the data.""" + +from typing import Any, Dict, Union + +from qiskit.qobj.utils import MeasLevel +from qiskit_experiments.data_processing.nodes import Kernel, Discriminator +from qiskit_experiments.data_processing.base import NodeType, DataAction + + +class DataProcessor: + """ + Defines the actions done on the measured data to bring it in a form usable + by the calibration analysis classes. + """ + + def __init__(self): + """Create an empty chain of data ProcessingSteps.""" + self._root_node = None + + def append(self, node: DataAction): + """ + Append new data action node to this data processor. + + Args: + node: A DataAction that will process the data. + """ + if self._root_node: + self._root_node.append(node) + else: + self._root_node = node + + def meas_level(self) -> MeasLevel: + """ + Returns: + measurement level: MeasLevel.CLASSIFIED is returned if the end data is discriminated, + MeasLevel.KERNELED is returned if a kernel is defined but no discriminator, and + MeasLevel.RAW is returned is no kernel is defined. + """ + kernel = DataProcessor.check_kernel(self._root_node) + if kernel and isinstance(kernel, Kernel): + discriminator = DataProcessor.check_discriminator(self._root_node) + if discriminator and isinstance(discriminator, Discriminator): + + # classified level if both system kernel and discriminator are defined + return MeasLevel.CLASSIFIED + + # kerneled level if only system kernel is defined + return MeasLevel.KERNELED + + # otherwise raw level is requested + return MeasLevel.RAW + + def output_key(self) -> str: + """Return the key to look for in the data output by the processor.""" + if self._root_node: + node = self._root_node + while node.child: + node = node.child + + if node.node_type in [NodeType.KERNEL, NodeType.IQDATA]: + return 'memory' + if node.node_type == NodeType.DISCRIMINATOR: + return 'counts' + if node.node_type == NodeType.POPULATION: + return 'populations' + + return 'counts' + + def format_data(self, data: Dict[str, Any]): + """ + Format Qiskit result data. + + This method sequentially calls stored child data processing nodes + with its `format_data` methods. Once all child nodes have called, + input data is converted into expected data format. + + Args: + data: The data, typically from an ExperimentData instance, that needs to + be processed. This dict also contains the metadata of each experiment. + """ + if self._root_node: + self._root_node.format_data(data) + + @classmethod + def check_kernel(cls, node: DataAction) -> Union[None, DataAction]: + """Return the stored kernel in the workflow.""" + if not node: + return None + + if node.node_type == NodeType.KERNEL: + return node + else: + if not node.child: + return None + return cls.check_kernel(node.child) + + @classmethod + def check_discriminator(cls, node: DataAction): + """Return stored discriminator in the workflow.""" + if not node: + return None + + if node.node_type == NodeType.DISCRIMINATOR: + return node + else: + if not node.child: + return None + return cls.check_discriminator(node.child) diff --git a/qiskit_experiments/data_processing/exceptions.py b/qiskit_experiments/data_processing/exceptions.py new file mode 100644 index 0000000000..34700c0e6e --- /dev/null +++ b/qiskit_experiments/data_processing/exceptions.py @@ -0,0 +1,28 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Exceptions for data processing.""" + +from qiskit.exceptions import QiskitError + + +class DataProcessorError(QiskitError): + """Errors raised by the calibration module.""" + + def __init__(self, *message): + """Set the error message.""" + super().__init__(*message) + self.message = ' '.join(message) + + def __str__(self): + """Return the message.""" + return repr(self.message) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py new file mode 100644 index 0000000000..aab3aea83a --- /dev/null +++ b/qiskit_experiments/data_processing/nodes.py @@ -0,0 +1,205 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Different data analysis steps.""" + +from typing import Any, Dict, Optional +import numpy as np + +from qiskit_experiments.data_processing.exceptions import DataProcessorError +from qiskit_experiments.data_processing.base import (DataAction, iq_data, kernel, + discriminator, prev_node, population) + + +@kernel +@prev_node() +class Kernel(DataAction): + """User provided kernel.""" + + def __init__(self, kernel_, name: Optional[str] = None): + """ + Args: + kernel_: Kernel to kernel the data. + name: Optional name for the node. + """ + self.kernel = kernel_ + self.name = name + super().__init__() + + def process(self, data: Dict[str, Any]): + """ + Args: + data: The data dictionary to process. + + Raises: + DataProcessorError: if the data has no memory. + """ + if 'memory' not in data: + raise DataProcessorError(f'Data does not have memory. ' + f'Cannot apply {self.__class__.__name__}') + + data['memory'] = self.kernel.kernel(np.array(data['memory'])) + + +@discriminator +@prev_node(Kernel) +class Discriminator(DataAction): + """Backend system discriminator.""" + + def __init__(self, discriminator_, name: Optional[str] = None): + """ + Args: + discriminator_: The discriminator used to transform the data to counts. + For example, transform IQ data to counts. + name: Optional name for the node. + """ + self.discriminator = discriminator_ + self.name = name + super().__init__() + + def process(self, data: Dict[str, Any]): + """ + Discriminate the data to transform it into counts. + + Args: + data: The data in a format that can be understood by the discriminator. + + Raises: + DataProcessorError: if the data does not contain memory. + """ + if 'memory' not in data: + raise DataProcessorError(f'Data does not have memory. ' + f'Cannot apply {self.__class__.__name__}') + + data['counts'] = self.discriminator.discriminate(np.array(data['memory'])) + + +@iq_data +@prev_node(Kernel) +class ToReal(DataAction): + """IQ data post-processing. This returns real part of IQ data.""" + + def __init__(self, scale: Optional[float] = 1.0, average: bool = False): + """ + Args: + scale: scale by which to multiply the real part of the data. + average: if True the single-shots are averaged. + """ + self.scale = scale + self.average = average + super().__init__() + + def process(self, data: Dict[str, Any]): + """ + Modifies the data inplace by taking the real part of the memory and + scaling it by the given factor. + + Args: + data: The data dict. IQ data is stored under memory. + + Raises: + DataProcessorError: if the data does not contain memory. + """ + if 'memory' not in data: + raise DataProcessorError(f'Data does not have memory. ' + f'Cannot apply {self.__class__.__name__}') + + # Single shot data + if isinstance(data['memory'][0][0], list): + new_mem = [] + for shot in data['memory']: + new_mem.append([self.scale*_[0] for _ in shot]) + + if self.average: + new_mem = list(np.mean(np.array(new_mem), axis=0)) + + # Averaged data + else: + new_mem = [self.scale*_[0] for _ in data['memory']] + + data['memory'] = new_mem + +@iq_data +@prev_node(Kernel) +class ToImag(DataAction): + """IQ data post-processing. This returns imaginary part of IQ data.""" + + def __init__(self, scale: Optional[float] = 1.0, average: bool = False): + """ + Args: + scale: scale by which to multiply the imaginary part of the data. + """ + self.scale = scale + self.average = average + super().__init__() + + def process(self, data: Dict[str, Any]): + """ + Scales the imaginary part of IQ data. + + Args: + data: The data dict. IQ data is stored under memory. + + Raises: + DataProcessorError: if the data does not contain memory. + """ + if 'memory' not in data: + raise DataProcessorError(f'Data does not have memory. ' + f'Cannot apply {self.__class__.__name__}') + + # Single shot data + if isinstance(data['memory'][0][0], list): + new_mem = [] + for shot in data['memory']: + new_mem.append([self.scale*_[1] for _ in shot]) + + if self.average: + new_mem = list(np.mean(np.array(new_mem), axis=0)) + + # Averaged data + else: + new_mem = [self.scale*_[0] for _ in data['memory']] + + data['memory'] = new_mem + + +@population +@prev_node(Discriminator) +class Population(DataAction): + """Count data post processing. This returns population.""" + + def process(self, data: Dict[str, Any]): + """ + Args: + data: The data dictionary. This will modify the dict in place, + taking the data under counts and adding the corresponding + populations. + + Raises: + DataProcessorError: if counts are not in the given data. + """ + if 'counts' not in data: + raise DataProcessorError(f'Data does not have counts. ' + f'Cannot apply {self.__class__.__name__}') + + counts = data.get('counts') + + populations = np.zeros(len(list(counts.keys())[0])) + + shots = 0 + for bit_str, count in counts.items(): + shots += 1 + for ind, bit in enumerate(bit_str): + if bit == '1': + populations[ind] += count + + data['populations'] = populations / shots diff --git a/test/data_processing/__init__.py b/test/data_processing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py new file mode 100644 index 0000000000..c44c47f6a0 --- /dev/null +++ b/test/data_processing/test_data_processing.py @@ -0,0 +1,194 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Data processor tests.""" + +from qiskit.result.models import ExperimentResultData, ExperimentResult +from qiskit.result import Result +from qiskit.test import QiskitTestCase +from qiskit.qobj.utils import MeasLevel +from qiskit.qobj.common import QobjExperimentHeader +from qiskit_experiments import ExperimentData +from qiskit_experiments.base_experiment import BaseExperiment +from qiskit_experiments.data_processing.data_processor import DataProcessor +from qiskit_experiments.data_processing.nodes import (Kernel, Discriminator, + ToReal, ToImag, Population) + + +class FakeKernel: + """Fake kernel to test the data chain.""" + + def kernel(self, data): + """Fake kernel method""" + return data + +class FakeExperiment(BaseExperiment): + """Fake experiment class for testing.""" + + def __init__(self): + """Initialise the fake experiment.""" + self._type = None + super().__init__((0, ), 'fake_test_experiment') + + def circuits(self, backend=None, **circuit_options): + """Fake circuits.""" + return [] + + +class DataProcessorTest(QiskitTestCase): + """Class to test DataProcessor.""" + + def setUp(self): + """Setup variables used for testing.""" + self.base_result_args = dict(backend_name='test_backend', + backend_version='1.0.0', + qobj_id='id-123', + job_id='job-123', + success=True) + + mem1 = ExperimentResultData(memory=[[[1103260.0, -11378508.0], [2959012.0, -16488753.0]], + [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], + [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]]]) + + mem2 = ExperimentResultData(memory=[[[5131962.0, -16630257.0], [4438870.0, -13752518.0]], + [[3415985.0, -16031913.0], [2942458.0, -15840465.0]], + [[5199964.0, -14955998.0], [4030843.0, -14538923.0]]]) + + header1 = QobjExperimentHeader(clbit_labels=[['meas', 0], ['meas', 1]], + creg_sizes=[['meas', 2]], global_phase=0.0, memory_slots=2, + metadata={'experiment_type': 'fake_test_experiment', + 'x_values': 0.0}) + + header2 = QobjExperimentHeader(clbit_labels=[['meas', 0], ['meas', 1]], + creg_sizes=[['meas', 2]], global_phase=0.0, memory_slots=2, + metadata={'experiment_type': 'fake_test_experiment', + 'x_values': 1.0}) + + res1 = ExperimentResult(shots=3, success=True, meas_level=1, data=mem1, header=header1) + res2 = ExperimentResult(shots=3, success=True, meas_level=1, data=mem2, header=header2) + + self.result_lvl1 = Result(results=[res1, res2], **self.base_result_args) + + super().setUp() + + def test_empty_processor(self): + """Check that a DataProcessor without steps does nothing.""" + + raw_counts = {'0x0': 4, '0x2': 5} + data = ExperimentResultData(counts=dict(**raw_counts)) + header = QobjExperimentHeader(metadata={'experiment_type': 'fake_test_experiment'}) + result_ = ExperimentResult(shots=9, success=True, meas_level=2, data=data, header=header) + + result = Result(results=[result_], **self.base_result_args) + + exp_data = ExperimentData(FakeExperiment()) + exp_data.add_data(result) + + data_processor = DataProcessor() + data_processor.format_data(exp_data.data) + self.assertEqual(exp_data.data[0]['counts']['0'], 4) + self.assertEqual(exp_data.data[0]['counts']['10'], 5) + + def test_append_kernel(self): + """Tests that we can add a kernel and a discriminator.""" + processor = DataProcessor() + self.assertEqual(processor.meas_level(), MeasLevel.RAW) + + processor.append(Kernel(FakeKernel)) + self.assertEqual(processor.meas_level(), MeasLevel.KERNELED) + + processor.append(Discriminator(None)) + self.assertEqual(processor.meas_level(), MeasLevel.CLASSIFIED) + + def test_output_key(self): + """Test that we can properly get the output key from the node.""" + processor = DataProcessor() + self.assertEqual(processor.output_key(), 'counts') + + processor.append(Kernel(FakeKernel())) + self.assertEqual(processor.output_key(), 'memory') + + processor.append(ToReal()) + self.assertEqual(processor.output_key(), 'memory') + + processor = DataProcessor() + processor.append(Kernel(FakeKernel())) + processor.append(Discriminator(None)) + self.assertEqual(processor.output_key(), 'counts') + + processor = DataProcessor() + processor.append(Population()) + self.assertEqual(processor.output_key(), 'populations') + + def test_to_real(self): + """Test scaling and conversion to real part.""" + processor = DataProcessor() + processor.append(ToReal(scale=1e-3)) + self.assertEqual(processor.output_key(), 'memory') + + exp_data = ExperimentData(FakeExperiment()) + exp_data.add_data(self.result_lvl1) + + processor.format_data(exp_data.data[0]) + + expected = {'memory': [[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]], + 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0}} + + self.assertEqual(exp_data.data[0], expected) + + # Test that we can average single-shots + processor = DataProcessor() + processor.append(ToReal(scale=1e-3, average=True)) + self.assertEqual(processor.output_key(), 'memory') + + exp_data = ExperimentData(FakeExperiment()) + exp_data.add_data(self.result_lvl1) + + processor.format_data(exp_data.data[0]) + + expected = {'memory': [1520.6480000000001, -1908.3846666666666], + 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0}} + + self.assertEqual(exp_data.data[0], expected) + + def test_to_imag(self): + """Test that we can average the data.""" + processor = DataProcessor() + processor.append(ToImag(scale=1e-3)) + self.assertEqual(processor.output_key(), 'memory') + + exp_data = ExperimentData(FakeExperiment()) + exp_data.add_data(self.result_lvl1) + + processor.format_data(exp_data.data[0]) + + expected = {'memory': [[-11378.508, -16488.753], + [-19283.206000000002, -15339.630000000001], + [-14548.009, -16743.348]], + 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0}} + + self.assertEqual(exp_data.data[0], expected) + + # Test that we can average single-shots + processor = DataProcessor() + processor.append(ToImag(scale=1e-3, average=True)) + self.assertEqual(processor.output_key(), 'memory') + + exp_data = ExperimentData(FakeExperiment()) + exp_data.add_data(self.result_lvl1) + + processor.format_data(exp_data.data[0]) + + expected = {'memory': [-15069.907666666666, -16190.577], + 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0}} + + self.assertEqual(exp_data.data[0], expected) From 375caa0f5ef997c5da140057cf4b34acbb8ce71f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 26 Mar 2021 11:29:10 +0100 Subject: [PATCH 02/52] * Added node_output property. --- qiskit_experiments/data_processing/base.py | 5 ++ .../data_processing/data_processor.py | 7 +-- qiskit_experiments/data_processing/nodes.py | 36 ++++++++++++-- test/data_processing/test_data_processing.py | 49 +++++++++++++------ 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py index 8ee0c5b467..20383ad148 100644 --- a/qiskit_experiments/data_processing/base.py +++ b/qiskit_experiments/data_processing/base.py @@ -60,6 +60,11 @@ def append(self, component: 'DataAction'): else: self._child.append(component) + @property + @abstractmethod + def node_output(self) -> str: + """Returns the key in the data dict where the DataAction added the processed data.""" + @abstractmethod def process(self, data: Dict[str, Any]): """ diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index cc126881c9..04ee96ea05 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -69,12 +69,7 @@ def output_key(self) -> str: while node.child: node = node.child - if node.node_type in [NodeType.KERNEL, NodeType.IQDATA]: - return 'memory' - if node.node_type == NodeType.DISCRIMINATOR: - return 'counts' - if node.node_type == NodeType.POPULATION: - return 'populations' + return node.node_output return 'counts' diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index aab3aea83a..dc31fed5b1 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -35,6 +35,12 @@ def __init__(self, kernel_, name: Optional[str] = None): self.name = name super().__init__() + + @property + def node_output(self) -> str: + """Key under which Kernel stores the data.""" + return 'memory' + def process(self, data: Dict[str, Any]): """ Args: @@ -47,7 +53,7 @@ def process(self, data: Dict[str, Any]): raise DataProcessorError(f'Data does not have memory. ' f'Cannot apply {self.__class__.__name__}') - data['memory'] = self.kernel.kernel(np.array(data['memory'])) + data[self.node_output] = self.kernel.kernel(np.array(data['memory'])) @discriminator @@ -66,6 +72,11 @@ def __init__(self, discriminator_, name: Optional[str] = None): self.name = name super().__init__() + @property + def node_output(self) -> str: + """Key under which Discriminator stores the data.""" + return 'counts' + def process(self, data: Dict[str, Any]): """ Discriminate the data to transform it into counts. @@ -80,7 +91,7 @@ def process(self, data: Dict[str, Any]): raise DataProcessorError(f'Data does not have memory. ' f'Cannot apply {self.__class__.__name__}') - data['counts'] = self.discriminator.discriminate(np.array(data['memory'])) + data[self.node_output] = self.discriminator.discriminate(np.array(data['memory'])) @iq_data @@ -98,6 +109,11 @@ def __init__(self, scale: Optional[float] = 1.0, average: bool = False): self.average = average super().__init__() + @property + def node_output(self) -> str: + """Key under which ToReal stores the data.""" + return 'memory_real' + def process(self, data: Dict[str, Any]): """ Modifies the data inplace by taking the real part of the memory and @@ -126,7 +142,7 @@ def process(self, data: Dict[str, Any]): else: new_mem = [self.scale*_[0] for _ in data['memory']] - data['memory'] = new_mem + data[self.node_output] = new_mem @iq_data @prev_node(Kernel) @@ -142,6 +158,11 @@ def __init__(self, scale: Optional[float] = 1.0, average: bool = False): self.average = average super().__init__() + @property + def node_output(self) -> str: + """Key under which ToImag stores the data.""" + return 'memory_imag' + def process(self, data: Dict[str, Any]): """ Scales the imaginary part of IQ data. @@ -169,7 +190,7 @@ def process(self, data: Dict[str, Any]): else: new_mem = [self.scale*_[0] for _ in data['memory']] - data['memory'] = new_mem + data[self.node_output] = new_mem @population @@ -177,6 +198,11 @@ def process(self, data: Dict[str, Any]): class Population(DataAction): """Count data post processing. This returns population.""" + @property + def node_output(self) -> str: + """Key under which Population stores the data.""" + return 'populations' + def process(self, data: Dict[str, Any]): """ Args: @@ -202,4 +228,4 @@ def process(self, data: Dict[str, Any]): if bit == '1': populations[ind] += count - data['populations'] = populations / shots + data[self.node_output] = populations / shots diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index c44c47f6a0..31869cd763 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -118,7 +118,7 @@ def test_output_key(self): self.assertEqual(processor.output_key(), 'memory') processor.append(ToReal()) - self.assertEqual(processor.output_key(), 'memory') + self.assertEqual(processor.output_key(), 'memory_real') processor = DataProcessor() processor.append(Kernel(FakeKernel())) @@ -133,30 +133,39 @@ def test_to_real(self): """Test scaling and conversion to real part.""" processor = DataProcessor() processor.append(ToReal(scale=1e-3)) - self.assertEqual(processor.output_key(), 'memory') + self.assertEqual(processor.output_key(), 'memory_real') exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) processor.format_data(exp_data.data[0]) - expected = {'memory': [[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]], - 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0}} + expected = { + 'memory': [[[1103260.0, -11378508.0], [2959012.0, -16488753.0]], + [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], + [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]]], + 'memory_real': [[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]], + 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0}} self.assertEqual(exp_data.data[0], expected) # Test that we can average single-shots processor = DataProcessor() processor.append(ToReal(scale=1e-3, average=True)) - self.assertEqual(processor.output_key(), 'memory') + self.assertEqual(processor.output_key(), 'memory_real') exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) processor.format_data(exp_data.data[0]) - expected = {'memory': [1520.6480000000001, -1908.3846666666666], - 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0}} + expected = { + 'memory': [[[1103260.0, -11378508.0], [2959012.0, -16488753.0]], + [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], + [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]]], + 'memory_real': [1520.6480000000001, -1908.3846666666666], + 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0} + } self.assertEqual(exp_data.data[0], expected) @@ -164,31 +173,41 @@ def test_to_imag(self): """Test that we can average the data.""" processor = DataProcessor() processor.append(ToImag(scale=1e-3)) - self.assertEqual(processor.output_key(), 'memory') + self.assertEqual(processor.output_key(), 'memory_imag') exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) processor.format_data(exp_data.data[0]) - expected = {'memory': [[-11378.508, -16488.753], - [-19283.206000000002, -15339.630000000001], - [-14548.009, -16743.348]], - 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0}} + expected = { + 'memory': [[[1103260.0, -11378508.0], [2959012.0, -16488753.0]], + [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], + [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]]], + 'memory_imag': [[-11378.508, -16488.753], + [-19283.206000000002, -15339.630000000001], + [-14548.009, -16743.348]], + 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0} + } self.assertEqual(exp_data.data[0], expected) # Test that we can average single-shots processor = DataProcessor() processor.append(ToImag(scale=1e-3, average=True)) - self.assertEqual(processor.output_key(), 'memory') + self.assertEqual(processor.output_key(), 'memory_imag') exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) processor.format_data(exp_data.data[0]) - expected = {'memory': [-15069.907666666666, -16190.577], - 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0}} + expected = { + 'memory': [[[1103260.0, -11378508.0], [2959012.0, -16488753.0]], + [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], + [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]]], + 'memory_imag': [-15069.907666666666, -16190.577], + 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0} + } self.assertEqual(exp_data.data[0], expected) From 999475868395e275cccad5f09f5666b0eb714527 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 26 Mar 2021 11:41:37 +0100 Subject: [PATCH 03/52] * Ran Black. * Fixed unit tests. --- .../data_processing/__init__.py | 3 +- qiskit_experiments/data_processing/base.py | 17 +- .../data_processing/data_processor.py | 2 +- .../data_processing/exceptions.py | 2 +- qiskit_experiments/data_processing/nodes.py | 81 +++++---- test/data_processing/test_data_processing.py | 155 +++++++++++------- 6 files changed, 154 insertions(+), 106 deletions(-) diff --git a/qiskit_experiments/data_processing/__init__.py b/qiskit_experiments/data_processing/__init__.py index d200f22fe9..bdec94f9a4 100644 --- a/qiskit_experiments/data_processing/__init__.py +++ b/qiskit_experiments/data_processing/__init__.py @@ -17,11 +17,10 @@ # data acquisition node Discriminator, Kernel, - # value formatter node Population, ToImag, - ToReal + ToReal, ) from .data_processor import DataProcessor diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py index 20383ad148..ae90fb4c95 100644 --- a/qiskit_experiments/data_processing/base.py +++ b/qiskit_experiments/data_processing/base.py @@ -34,11 +34,11 @@ def __init__(self): self._child = None @property - def child(self) -> 'DataAction': + def child(self) -> "DataAction": """Return the child of this data processing step.""" return self._child - def append(self, component: 'DataAction'): + def append(self, component: "DataAction"): """Add new data processing routine. Args: @@ -48,15 +48,19 @@ def append(self, component: 'DataAction'): DataProcessorError: If the previous node is None (i.e. a root node) """ if not component.prev_node: - raise DataProcessorError(f'Analysis routine {component.__class__.__name__} is a root' - f'node. This routine cannot be appended to another node.') + raise DataProcessorError( + f"Analysis routine {component.__class__.__name__} is a root" + f"node. This routine cannot be appended to another node." + ) if self._child is None: if isinstance(self, component.prev_node): self._child = component else: - raise DataProcessorError(f'Analysis routine {component.__class__.__name__} ' - f'cannot be appended after {self.__class__.__name__}') + raise DataProcessorError( + f"Analysis routine {component.__class__.__name__} " + f"cannot be appended after {self.__class__.__name__}" + ) else: self._child.append(component) @@ -91,6 +95,7 @@ def format_data(self, data: Dict[str, Any]): class NodeType(Enum): """Type of node that can be supported by the analysis steps.""" + KERNEL = 1 DISCRIMINATOR = 2 IQDATA = 3 diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 04ee96ea05..df7b22af7a 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -71,7 +71,7 @@ def output_key(self) -> str: return node.node_output - return 'counts' + return "counts" def format_data(self, data: Dict[str, Any]): """ diff --git a/qiskit_experiments/data_processing/exceptions.py b/qiskit_experiments/data_processing/exceptions.py index 34700c0e6e..018d109e11 100644 --- a/qiskit_experiments/data_processing/exceptions.py +++ b/qiskit_experiments/data_processing/exceptions.py @@ -21,7 +21,7 @@ class DataProcessorError(QiskitError): def __init__(self, *message): """Set the error message.""" super().__init__(*message) - self.message = ' '.join(message) + self.message = " ".join(message) def __str__(self): """Return the message.""" diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index dc31fed5b1..e9e0b2c9f4 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -16,8 +16,14 @@ import numpy as np from qiskit_experiments.data_processing.exceptions import DataProcessorError -from qiskit_experiments.data_processing.base import (DataAction, iq_data, kernel, - discriminator, prev_node, population) +from qiskit_experiments.data_processing.base import ( + DataAction, + iq_data, + kernel, + discriminator, + prev_node, + population, +) @kernel @@ -35,11 +41,10 @@ def __init__(self, kernel_, name: Optional[str] = None): self.name = name super().__init__() - @property def node_output(self) -> str: """Key under which Kernel stores the data.""" - return 'memory' + return "memory" def process(self, data: Dict[str, Any]): """ @@ -49,11 +54,12 @@ def process(self, data: Dict[str, Any]): Raises: DataProcessorError: if the data has no memory. """ - if 'memory' not in data: - raise DataProcessorError(f'Data does not have memory. ' - f'Cannot apply {self.__class__.__name__}') + if "memory" not in data: + raise DataProcessorError( + f"Data does not have memory. " f"Cannot apply {self.__class__.__name__}" + ) - data[self.node_output] = self.kernel.kernel(np.array(data['memory'])) + data[self.node_output] = self.kernel.kernel(np.array(data["memory"])) @discriminator @@ -75,7 +81,7 @@ def __init__(self, discriminator_, name: Optional[str] = None): @property def node_output(self) -> str: """Key under which Discriminator stores the data.""" - return 'counts' + return "counts" def process(self, data: Dict[str, Any]): """ @@ -87,11 +93,12 @@ def process(self, data: Dict[str, Any]): Raises: DataProcessorError: if the data does not contain memory. """ - if 'memory' not in data: - raise DataProcessorError(f'Data does not have memory. ' - f'Cannot apply {self.__class__.__name__}') + if "memory" not in data: + raise DataProcessorError( + f"Data does not have memory. " f"Cannot apply {self.__class__.__name__}" + ) - data[self.node_output] = self.discriminator.discriminate(np.array(data['memory'])) + data[self.node_output] = self.discriminator.discriminate(np.array(data["memory"])) @iq_data @@ -112,7 +119,7 @@ def __init__(self, scale: Optional[float] = 1.0, average: bool = False): @property def node_output(self) -> str: """Key under which ToReal stores the data.""" - return 'memory_real' + return "memory_real" def process(self, data: Dict[str, Any]): """ @@ -125,25 +132,27 @@ def process(self, data: Dict[str, Any]): Raises: DataProcessorError: if the data does not contain memory. """ - if 'memory' not in data: - raise DataProcessorError(f'Data does not have memory. ' - f'Cannot apply {self.__class__.__name__}') + if "memory" not in data: + raise DataProcessorError( + f"Data does not have memory. " f"Cannot apply {self.__class__.__name__}" + ) # Single shot data - if isinstance(data['memory'][0][0], list): + if isinstance(data["memory"][0][0], list): new_mem = [] - for shot in data['memory']: - new_mem.append([self.scale*_[0] for _ in shot]) + for shot in data["memory"]: + new_mem.append([self.scale * _[0] for _ in shot]) if self.average: new_mem = list(np.mean(np.array(new_mem), axis=0)) # Averaged data else: - new_mem = [self.scale*_[0] for _ in data['memory']] + new_mem = [self.scale * _[0] for _ in data["memory"]] data[self.node_output] = new_mem + @iq_data @prev_node(Kernel) class ToImag(DataAction): @@ -161,7 +170,7 @@ def __init__(self, scale: Optional[float] = 1.0, average: bool = False): @property def node_output(self) -> str: """Key under which ToImag stores the data.""" - return 'memory_imag' + return "memory_imag" def process(self, data: Dict[str, Any]): """ @@ -173,22 +182,23 @@ def process(self, data: Dict[str, Any]): Raises: DataProcessorError: if the data does not contain memory. """ - if 'memory' not in data: - raise DataProcessorError(f'Data does not have memory. ' - f'Cannot apply {self.__class__.__name__}') + if "memory" not in data: + raise DataProcessorError( + f"Data does not have memory. " f"Cannot apply {self.__class__.__name__}" + ) # Single shot data - if isinstance(data['memory'][0][0], list): + if isinstance(data["memory"][0][0], list): new_mem = [] - for shot in data['memory']: - new_mem.append([self.scale*_[1] for _ in shot]) + for shot in data["memory"]: + new_mem.append([self.scale * _[1] for _ in shot]) if self.average: new_mem = list(np.mean(np.array(new_mem), axis=0)) # Averaged data else: - new_mem = [self.scale*_[0] for _ in data['memory']] + new_mem = [self.scale * _[0] for _ in data["memory"]] data[self.node_output] = new_mem @@ -201,7 +211,7 @@ class Population(DataAction): @property def node_output(self) -> str: """Key under which Population stores the data.""" - return 'populations' + return "populations" def process(self, data: Dict[str, Any]): """ @@ -213,11 +223,12 @@ def process(self, data: Dict[str, Any]): Raises: DataProcessorError: if counts are not in the given data. """ - if 'counts' not in data: - raise DataProcessorError(f'Data does not have counts. ' - f'Cannot apply {self.__class__.__name__}') + if "counts" not in data: + raise DataProcessorError( + f"Data does not have counts. " f"Cannot apply {self.__class__.__name__}" + ) - counts = data.get('counts') + counts = data.get("counts") populations = np.zeros(len(list(counts.keys())[0])) @@ -225,7 +236,7 @@ def process(self, data: Dict[str, Any]): for bit_str, count in counts.items(): shots += 1 for ind, bit in enumerate(bit_str): - if bit == '1': + if bit == "1": populations[ind] += count data[self.node_output] = populations / shots diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 31869cd763..f2fd87dffe 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -20,8 +20,13 @@ from qiskit_experiments import ExperimentData from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.data_processing.data_processor import DataProcessor -from qiskit_experiments.data_processing.nodes import (Kernel, Discriminator, - ToReal, ToImag, Population) +from qiskit_experiments.data_processing.nodes import ( + Kernel, + Discriminator, + ToReal, + ToImag, + Population, +) class FakeKernel: @@ -31,13 +36,14 @@ def kernel(self, data): """Fake kernel method""" return data + class FakeExperiment(BaseExperiment): """Fake experiment class for testing.""" def __init__(self): """Initialise the fake experiment.""" self._type = None - super().__init__((0, ), 'fake_test_experiment') + super().__init__((0,), "fake_test_experiment") def circuits(self, backend=None, **circuit_options): """Fake circuits.""" @@ -49,29 +55,45 @@ class DataProcessorTest(QiskitTestCase): def setUp(self): """Setup variables used for testing.""" - self.base_result_args = dict(backend_name='test_backend', - backend_version='1.0.0', - qobj_id='id-123', - job_id='job-123', - success=True) - - mem1 = ExperimentResultData(memory=[[[1103260.0, -11378508.0], [2959012.0, -16488753.0]], - [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], - [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]]]) - - mem2 = ExperimentResultData(memory=[[[5131962.0, -16630257.0], [4438870.0, -13752518.0]], - [[3415985.0, -16031913.0], [2942458.0, -15840465.0]], - [[5199964.0, -14955998.0], [4030843.0, -14538923.0]]]) - - header1 = QobjExperimentHeader(clbit_labels=[['meas', 0], ['meas', 1]], - creg_sizes=[['meas', 2]], global_phase=0.0, memory_slots=2, - metadata={'experiment_type': 'fake_test_experiment', - 'x_values': 0.0}) - - header2 = QobjExperimentHeader(clbit_labels=[['meas', 0], ['meas', 1]], - creg_sizes=[['meas', 2]], global_phase=0.0, memory_slots=2, - metadata={'experiment_type': 'fake_test_experiment', - 'x_values': 1.0}) + self.base_result_args = dict( + backend_name="test_backend", + backend_version="1.0.0", + qobj_id="id-123", + job_id="job-123", + success=True, + ) + + mem1 = ExperimentResultData( + memory=[ + [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], + [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], + [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], + ] + ) + + mem2 = ExperimentResultData( + memory=[ + [[5131962.0, -16630257.0], [4438870.0, -13752518.0]], + [[3415985.0, -16031913.0], [2942458.0, -15840465.0]], + [[5199964.0, -14955998.0], [4030843.0, -14538923.0]], + ] + ) + + header1 = QobjExperimentHeader( + clbit_labels=[["meas", 0], ["meas", 1]], + creg_sizes=[["meas", 2]], + global_phase=0.0, + memory_slots=2, + metadata={"experiment_type": "fake_test_experiment", "x_values": 0.0}, + ) + + header2 = QobjExperimentHeader( + clbit_labels=[["meas", 0], ["meas", 1]], + creg_sizes=[["meas", 2]], + global_phase=0.0, + memory_slots=2, + metadata={"experiment_type": "fake_test_experiment", "x_values": 1.0}, + ) res1 = ExperimentResult(shots=3, success=True, meas_level=1, data=mem1, header=header1) res2 = ExperimentResult(shots=3, success=True, meas_level=1, data=mem2, header=header2) @@ -83,9 +105,9 @@ def setUp(self): def test_empty_processor(self): """Check that a DataProcessor without steps does nothing.""" - raw_counts = {'0x0': 4, '0x2': 5} + raw_counts = {"0x0": 4, "0x2": 5} data = ExperimentResultData(counts=dict(**raw_counts)) - header = QobjExperimentHeader(metadata={'experiment_type': 'fake_test_experiment'}) + header = QobjExperimentHeader(metadata={"experiment_type": "fake_test_experiment"}) result_ = ExperimentResult(shots=9, success=True, meas_level=2, data=data, header=header) result = Result(results=[result_], **self.base_result_args) @@ -95,8 +117,8 @@ def test_empty_processor(self): data_processor = DataProcessor() data_processor.format_data(exp_data.data) - self.assertEqual(exp_data.data[0]['counts']['0'], 4) - self.assertEqual(exp_data.data[0]['counts']['10'], 5) + self.assertEqual(exp_data.data[0]["counts"]["0"], 4) + self.assertEqual(exp_data.data[0]["counts"]["10"], 5) def test_append_kernel(self): """Tests that we can add a kernel and a discriminator.""" @@ -112,28 +134,28 @@ def test_append_kernel(self): def test_output_key(self): """Test that we can properly get the output key from the node.""" processor = DataProcessor() - self.assertEqual(processor.output_key(), 'counts') + self.assertEqual(processor.output_key(), "counts") processor.append(Kernel(FakeKernel())) - self.assertEqual(processor.output_key(), 'memory') + self.assertEqual(processor.output_key(), "memory") processor.append(ToReal()) - self.assertEqual(processor.output_key(), 'memory_real') + self.assertEqual(processor.output_key(), "memory_real") processor = DataProcessor() processor.append(Kernel(FakeKernel())) processor.append(Discriminator(None)) - self.assertEqual(processor.output_key(), 'counts') + self.assertEqual(processor.output_key(), "counts") processor = DataProcessor() processor.append(Population()) - self.assertEqual(processor.output_key(), 'populations') + self.assertEqual(processor.output_key(), "populations") def test_to_real(self): """Test scaling and conversion to real part.""" processor = DataProcessor() processor.append(ToReal(scale=1e-3)) - self.assertEqual(processor.output_key(), 'memory_real') + self.assertEqual(processor.output_key(), "memory_real") exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) @@ -141,18 +163,21 @@ def test_to_real(self): processor.format_data(exp_data.data[0]) expected = { - 'memory': [[[1103260.0, -11378508.0], [2959012.0, -16488753.0]], - [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], - [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]]], - 'memory_real': [[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]], - 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0}} + "memory": [ + [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], + [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], + [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], + ], + "memory_real": [[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]], + "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, + } self.assertEqual(exp_data.data[0], expected) # Test that we can average single-shots processor = DataProcessor() processor.append(ToReal(scale=1e-3, average=True)) - self.assertEqual(processor.output_key(), 'memory_real') + self.assertEqual(processor.output_key(), "memory_real") exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) @@ -160,11 +185,13 @@ def test_to_real(self): processor.format_data(exp_data.data[0]) expected = { - 'memory': [[[1103260.0, -11378508.0], [2959012.0, -16488753.0]], - [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], - [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]]], - 'memory_real': [1520.6480000000001, -1908.3846666666666], - 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0} + "memory": [ + [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], + [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], + [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], + ], + "memory_real": [1520.6480000000001, -1908.3846666666666], + "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } self.assertEqual(exp_data.data[0], expected) @@ -173,7 +200,7 @@ def test_to_imag(self): """Test that we can average the data.""" processor = DataProcessor() processor.append(ToImag(scale=1e-3)) - self.assertEqual(processor.output_key(), 'memory_imag') + self.assertEqual(processor.output_key(), "memory_imag") exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) @@ -181,13 +208,17 @@ def test_to_imag(self): processor.format_data(exp_data.data[0]) expected = { - 'memory': [[[1103260.0, -11378508.0], [2959012.0, -16488753.0]], - [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], - [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]]], - 'memory_imag': [[-11378.508, -16488.753], - [-19283.206000000002, -15339.630000000001], - [-14548.009, -16743.348]], - 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0} + "memory": [ + [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], + [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], + [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], + ], + "memory_imag": [ + [-11378.508, -16488.753], + [-19283.206000000002, -15339.630000000001], + [-14548.009, -16743.348], + ], + "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } self.assertEqual(exp_data.data[0], expected) @@ -195,7 +226,7 @@ def test_to_imag(self): # Test that we can average single-shots processor = DataProcessor() processor.append(ToImag(scale=1e-3, average=True)) - self.assertEqual(processor.output_key(), 'memory_imag') + self.assertEqual(processor.output_key(), "memory_imag") exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) @@ -203,11 +234,13 @@ def test_to_imag(self): processor.format_data(exp_data.data[0]) expected = { - 'memory': [[[1103260.0, -11378508.0], [2959012.0, -16488753.0]], - [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], - [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]]], - 'memory_imag': [-15069.907666666666, -16190.577], - 'metadata': {'experiment_type': 'fake_test_experiment', 'x_values': 0.0} + "memory": [ + [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], + [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], + [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], + ], + "memory_imag": [-15069.907666666666, -16190.577], + "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } self.assertEqual(exp_data.data[0], expected) From 0fc66baf1628cb95dbcfd11011e3585235eb5b24 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 26 Mar 2021 11:59:28 +0100 Subject: [PATCH 04/52] * Added a better methodology for checking the input requirements. --- qiskit_experiments/data_processing/base.py | 24 +++++++++- qiskit_experiments/data_processing/nodes.py | 49 +++++++++++---------- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py index ae90fb4c95..32ad07c2a1 100644 --- a/qiskit_experiments/data_processing/base.py +++ b/qiskit_experiments/data_processing/base.py @@ -14,7 +14,7 @@ from abc import ABCMeta, abstractmethod from enum import Enum -from typing import Any, Dict +from typing import Any, Dict, List from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -69,6 +69,11 @@ def append(self, component: "DataAction"): def node_output(self) -> str: """Returns the key in the data dict where the DataAction added the processed data.""" + @property + @abstractmethod + def node_inputs(self) -> List[str]: + """Returns a list of input data that the node can process.""" + @abstractmethod def process(self, data: Dict[str, Any]): """ @@ -78,6 +83,21 @@ def process(self, data: Dict[str, Any]): data: the data to which the data processing step will be applied. """ + def check_required(self, data: Dict[str, Any]): + """Checks that the given data contains the right key. + + Args: + data: The data to check for the correct keys. + + Raises: + DataProcessorError: if the key is not found. + """ + for key in data.keys(): + if key in self.node_inputs: + return + + raise DataProcessorError(f"None of {self.node_inputs} are in the given data.") + def format_data(self, data: Dict[str, Any]): """ Apply the data action of this node and call the child node's format_data method. @@ -87,6 +107,8 @@ def format_data(self, data: Dict[str, Any]): processor will raise errors if the data does not contain the appropriate data. """ + self.check_required(data) + processed_data = self.process(data) if self._child: diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index e9e0b2c9f4..2ab8233f55 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -12,7 +12,7 @@ """Different data analysis steps.""" -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional import numpy as np from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -46,6 +46,11 @@ def node_output(self) -> str: """Key under which Kernel stores the data.""" return "memory" + @property + def node_inputs(self) -> List[str]: + """Returns a list of input data that the node can process.""" + return ["memory"] + def process(self, data: Dict[str, Any]): """ Args: @@ -54,11 +59,6 @@ def process(self, data: Dict[str, Any]): Raises: DataProcessorError: if the data has no memory. """ - if "memory" not in data: - raise DataProcessorError( - f"Data does not have memory. " f"Cannot apply {self.__class__.__name__}" - ) - data[self.node_output] = self.kernel.kernel(np.array(data["memory"])) @@ -83,6 +83,11 @@ def node_output(self) -> str: """Key under which Discriminator stores the data.""" return "counts" + @property + def node_inputs(self) -> List[str]: + """Returns a list of input data that the node can process.""" + return ["memory"] + def process(self, data: Dict[str, Any]): """ Discriminate the data to transform it into counts. @@ -93,11 +98,6 @@ def process(self, data: Dict[str, Any]): Raises: DataProcessorError: if the data does not contain memory. """ - if "memory" not in data: - raise DataProcessorError( - f"Data does not have memory. " f"Cannot apply {self.__class__.__name__}" - ) - data[self.node_output] = self.discriminator.discriminate(np.array(data["memory"])) @@ -121,6 +121,11 @@ def node_output(self) -> str: """Key under which ToReal stores the data.""" return "memory_real" + @property + def node_inputs(self) -> List[str]: + """Returns a list of input data that the node can process.""" + return ["memory"] + def process(self, data: Dict[str, Any]): """ Modifies the data inplace by taking the real part of the memory and @@ -132,10 +137,6 @@ def process(self, data: Dict[str, Any]): Raises: DataProcessorError: if the data does not contain memory. """ - if "memory" not in data: - raise DataProcessorError( - f"Data does not have memory. " f"Cannot apply {self.__class__.__name__}" - ) # Single shot data if isinstance(data["memory"][0][0], list): @@ -172,6 +173,11 @@ def node_output(self) -> str: """Key under which ToImag stores the data.""" return "memory_imag" + @property + def node_inputs(self) -> List[str]: + """Returns a list of input data that the node can process.""" + return ["memory"] + def process(self, data: Dict[str, Any]): """ Scales the imaginary part of IQ data. @@ -182,10 +188,6 @@ def process(self, data: Dict[str, Any]): Raises: DataProcessorError: if the data does not contain memory. """ - if "memory" not in data: - raise DataProcessorError( - f"Data does not have memory. " f"Cannot apply {self.__class__.__name__}" - ) # Single shot data if isinstance(data["memory"][0][0], list): @@ -213,6 +215,11 @@ def node_output(self) -> str: """Key under which Population stores the data.""" return "populations" + @property + def node_inputs(self) -> List[str]: + """Returns a list of input data that the node can process.""" + return ["counts"] + def process(self, data: Dict[str, Any]): """ Args: @@ -223,10 +230,6 @@ def process(self, data: Dict[str, Any]): Raises: DataProcessorError: if counts are not in the given data. """ - if "counts" not in data: - raise DataProcessorError( - f"Data does not have counts. " f"Cannot apply {self.__class__.__name__}" - ) counts = data.get("counts") From a0cbd6581490f9329950ba47434391ad789030f7 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 26 Mar 2021 15:00:45 +0100 Subject: [PATCH 05/52] * Lint fix. --- qiskit_experiments/data_processing/nodes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 2ab8233f55..331d362c31 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -15,7 +15,6 @@ from typing import Any, Dict, List, Optional import numpy as np -from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.data_processing.base import ( DataAction, iq_data, From 839ab97c4b24e33a8cc1f3e95aab23bcac9c66ee Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 29 Mar 2021 16:52:10 +0200 Subject: [PATCH 06/52] * Added population shots fix and corresponding tests. --- qiskit_experiments/data_processing/nodes.py | 2 +- test/data_processing/test_data_processing.py | 40 +++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 331d362c31..f122f5df1a 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -236,7 +236,7 @@ def process(self, data: Dict[str, Any]): shots = 0 for bit_str, count in counts.items(): - shots += 1 + shots += count for ind, bit in enumerate(bit_str): if bit == "1": populations[ind] += count diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index f2fd87dffe..ede926ea73 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -100,25 +100,27 @@ def setUp(self): self.result_lvl1 = Result(results=[res1, res2], **self.base_result_args) + raw_counts = {"0x0": 4, "0x2": 6} + data = ExperimentResultData(counts=dict(**raw_counts)) + header = QobjExperimentHeader( + metadata={"experiment_type": "fake_test_experiment"}, + clbit_labels=[["c", 0], ["c", 1]], + creg_sizes=[["c", 2]], + n_qubits=2, + memory_slots=2, + ) + res = ExperimentResult(shots=9, success=True, meas_level=2, data=data, header=header) + self.exp_data_lvl2 = ExperimentData(FakeExperiment()) + self.exp_data_lvl2.add_data(Result(results=[res], **self.base_result_args)) + super().setUp() def test_empty_processor(self): """Check that a DataProcessor without steps does nothing.""" - - raw_counts = {"0x0": 4, "0x2": 5} - data = ExperimentResultData(counts=dict(**raw_counts)) - header = QobjExperimentHeader(metadata={"experiment_type": "fake_test_experiment"}) - result_ = ExperimentResult(shots=9, success=True, meas_level=2, data=data, header=header) - - result = Result(results=[result_], **self.base_result_args) - - exp_data = ExperimentData(FakeExperiment()) - exp_data.add_data(result) - data_processor = DataProcessor() - data_processor.format_data(exp_data.data) - self.assertEqual(exp_data.data[0]["counts"]["0"], 4) - self.assertEqual(exp_data.data[0]["counts"]["10"], 5) + data_processor.format_data(self.exp_data_lvl2.data) + self.assertEqual(self.exp_data_lvl2.data[0]["counts"]["00"], 4) + self.assertEqual(self.exp_data_lvl2.data[0]["counts"]["10"], 6) def test_append_kernel(self): """Tests that we can add a kernel and a discriminator.""" @@ -244,3 +246,13 @@ def test_to_imag(self): } self.assertEqual(exp_data.data[0], expected) + + def test_populations(self): + """Test that counts are properly converted to a population.""" + + processor = DataProcessor() + processor.append(Population()) + processor.format_data(self.exp_data_lvl2.data[0]) + + self.assertEqual(self.exp_data_lvl2.data[0]["populations"][1], 0.0) + self.assertEqual(self.exp_data_lvl2.data[0]["populations"][0], 0.6) From 38d667545315c6d3771d012fc10e243e5e4badf3 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 29 Mar 2021 17:07:22 +0200 Subject: [PATCH 07/52] * Unified ToReal and ToImag. --- qiskit_experiments/data_processing/nodes.py | 75 ++++++++------------- 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index f122f5df1a..bb6774c96a 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -12,6 +12,7 @@ """Different data analysis steps.""" +from abc import abstractmethod from typing import Any, Dict, List, Optional import numpy as np @@ -102,8 +103,8 @@ def process(self, data: Dict[str, Any]): @iq_data @prev_node(Kernel) -class ToReal(DataAction): - """IQ data post-processing. This returns real part of IQ data.""" +class IQPart(DataAction): + """Abstract class for IQ data post-processing.""" def __init__(self, scale: Optional[float] = 1.0, average: bool = False): """ @@ -115,16 +116,15 @@ def __init__(self, scale: Optional[float] = 1.0, average: bool = False): self.average = average super().__init__() - @property - def node_output(self) -> str: - """Key under which ToReal stores the data.""" - return "memory_real" - @property def node_inputs(self) -> List[str]: """Returns a list of input data that the node can process.""" return ["memory"] + @abstractmethod + def _index(self) -> int: + """Return 0 for real and 1 for imaginary part.""" + def process(self, data: Dict[str, Any]): """ Modifies the data inplace by taking the real part of the memory and @@ -141,67 +141,46 @@ def process(self, data: Dict[str, Any]): if isinstance(data["memory"][0][0], list): new_mem = [] for shot in data["memory"]: - new_mem.append([self.scale * _[0] for _ in shot]) + new_mem.append([self.scale * iq_qubit[self._index()] for iq_qubit in shot]) if self.average: new_mem = list(np.mean(np.array(new_mem), axis=0)) # Averaged data else: - new_mem = [self.scale * _[0] for _ in data["memory"]] + new_mem = [self.scale * iq_qubit[self._index] for iq_qubit in data["memory"]] data[self.node_output] = new_mem @iq_data @prev_node(Kernel) -class ToImag(DataAction): - """IQ data post-processing. This returns imaginary part of IQ data.""" - - def __init__(self, scale: Optional[float] = 1.0, average: bool = False): - """ - Args: - scale: scale by which to multiply the imaginary part of the data. - """ - self.scale = scale - self.average = average - super().__init__() +class ToReal(IQPart): + """IQ data post-processing. Isolate the real part of the IQ data.""" @property def node_output(self) -> str: - """Key under which ToImag stores the data.""" - return "memory_imag" - - @property - def node_inputs(self) -> List[str]: - """Returns a list of input data that the node can process.""" - return ["memory"] - - def process(self, data: Dict[str, Any]): - """ - Scales the imaginary part of IQ data. - - Args: - data: The data dict. IQ data is stored under memory. + """Key under which ToReal stores the data.""" + return "memory_real" - Raises: - DataProcessorError: if the data does not contain memory. - """ + def _index(self) -> int: + """Return 0 for real part.""" + return 0 - # Single shot data - if isinstance(data["memory"][0][0], list): - new_mem = [] - for shot in data["memory"]: - new_mem.append([self.scale * _[1] for _ in shot]) - if self.average: - new_mem = list(np.mean(np.array(new_mem), axis=0)) +@iq_data +@prev_node(Kernel) +class ToImag(IQPart): + """IQ data post-processing. Isolate the imaginary part of the IQ data.""" - # Averaged data - else: - new_mem = [self.scale * _[0] for _ in data["memory"]] + @property + def node_output(self) -> str: + """Key under which ToImag stores the data.""" + return "memory_imag" - data[self.node_output] = new_mem + def _index(self) -> int: + """Return 0 for real part.""" + return 1 @population From 0be6f1f1c3dcbd6cfc0fb00f6e6928208ad21a8c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 Mar 2021 12:27:26 +0200 Subject: [PATCH 08/52] * Reformatted the DataProcessor to a list of nodes rather than pointers. * Removed the node_type and root node. * Amended tests accordingly. --- qiskit_experiments/data_processing/base.py | 115 +++--------------- .../data_processing/data_processor.py | 86 ++++--------- qiskit_experiments/data_processing/nodes.py | 51 ++------ test/data_processing/test_data_processing.py | 17 ++- 4 files changed, 59 insertions(+), 210 deletions(-) diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py index 32ad07c2a1..c234bf7ea2 100644 --- a/qiskit_experiments/data_processing/base.py +++ b/qiskit_experiments/data_processing/base.py @@ -13,7 +13,6 @@ """Defines the steps that can be used to analyse data.""" from abc import ABCMeta, abstractmethod -from enum import Enum from typing import Any, Dict, List from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -26,43 +25,10 @@ class DataAction(metaclass=ABCMeta): using decorators. """ - node_type = None - prev_node = () - def __init__(self): """Create new data analysis routine.""" self._child = None - - @property - def child(self) -> "DataAction": - """Return the child of this data processing step.""" - return self._child - - def append(self, component: "DataAction"): - """Add new data processing routine. - - Args: - component: New data processing routine. - - Raises: - DataProcessorError: If the previous node is None (i.e. a root node) - """ - if not component.prev_node: - raise DataProcessorError( - f"Analysis routine {component.__class__.__name__} is a root" - f"node. This routine cannot be appended to another node." - ) - - if self._child is None: - if isinstance(self, component.prev_node): - self._child = component - else: - raise DataProcessorError( - f"Analysis routine {component.__class__.__name__} " - f"cannot be appended after {self.__class__.__name__}" - ) - else: - self._child.append(component) + self._accepted_inputs = [] @property @abstractmethod @@ -70,9 +36,18 @@ def node_output(self) -> str: """Returns the key in the data dict where the DataAction added the processed data.""" @property - @abstractmethod def node_inputs(self) -> List[str]: """Returns a list of input data that the node can process.""" + return self._accepted_inputs + + def add_accepted_input(self, data_key: str): + """ + Allows users to add an accepted input data format to this DataAction. + + Args: + data_key: The key that the data action will require in the input data dict. + """ + self._accepted_inputs.append(data_key) @abstractmethod def process(self, data: Dict[str, Any]): @@ -108,63 +83,11 @@ def format_data(self, data: Dict[str, Any]): appropriate data. """ self.check_required(data) - - processed_data = self.process(data) - - if self._child: - self._child.format_data(processed_data) - - -class NodeType(Enum): - """Type of node that can be supported by the analysis steps.""" - - KERNEL = 1 - DISCRIMINATOR = 2 - IQDATA = 3 - COUNTS = 4 - POPULATION = 5 - - -def kernel(cls: DataAction): - """A decorator to give kernel attribute to node.""" - cls.node_type = NodeType.KERNEL - return cls - - -def discriminator(cls: DataAction): - """A decorator to give discriminator attribute to node.""" - cls.node_type = NodeType.DISCRIMINATOR - return cls - - -def iq_data(cls: DataAction): - """A decorator to give iqdata attribute to node.""" - cls.node_type = NodeType.IQDATA - return cls - - -def counts(cls: DataAction): - """A decorator to give counts attribute to node.""" - cls.node_type = NodeType.COUNTS - return cls - - -def population(cls: DataAction): - """A decorator to give population attribute to node.""" - cls.node_type = NodeType.POPULATION - return cls - - -def prev_node(*nodes: DataAction): - """A decorator to specify the available previous nodes.""" - - try: - nodes = list(nodes) - except TypeError: - nodes = [nodes] - - def add_nodes(cls: DataAction): - cls.prev_node = tuple(nodes) - return cls - - return add_nodes + self.process(data) + + def __repr__(self): + """String representation of the node.""" + return ( + f"{self.__class__.__name__}(inputs: {self.node_inputs}, " + f"outputs: {self.node_output})" + ) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index df7b22af7a..8f31c6bb26 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -14,9 +14,8 @@ from typing import Any, Dict, Union -from qiskit.qobj.utils import MeasLevel -from qiskit_experiments.data_processing.nodes import Kernel, Discriminator -from qiskit_experiments.data_processing.base import NodeType, DataAction +from qiskit_experiments.data_processing.base import DataAction +from qiskit_experiments.data_processing.exceptions import DataProcessorError class DataProcessor: @@ -26,8 +25,8 @@ class DataProcessor: """ def __init__(self): - """Create an empty chain of data ProcessingSteps.""" - self._root_node = None + """Create an empty chain of data processing actions.""" + self._nodes = [] def append(self, node: DataAction): """ @@ -35,47 +34,32 @@ def append(self, node: DataAction): Args: node: A DataAction that will process the data. - """ - if self._root_node: - self._root_node.append(node) - else: - self._root_node = node - def meas_level(self) -> MeasLevel: - """ - Returns: - measurement level: MeasLevel.CLASSIFIED is returned if the end data is discriminated, - MeasLevel.KERNELED is returned if a kernel is defined but no discriminator, and - MeasLevel.RAW is returned is no kernel is defined. + Raises: + DataProcessorError: if the output of the last node does not match the input required + by the node to be appended. """ - kernel = DataProcessor.check_kernel(self._root_node) - if kernel and isinstance(kernel, Kernel): - discriminator = DataProcessor.check_discriminator(self._root_node) - if discriminator and isinstance(discriminator, Discriminator): - - # classified level if both system kernel and discriminator are defined - return MeasLevel.CLASSIFIED - - # kerneled level if only system kernel is defined - return MeasLevel.KERNELED + if len(self._nodes) == 0: + self._nodes.append(node) + else: + if self._nodes[-1].node_output not in node.node_inputs: + raise DataProcessorError( + f"Output of node {self._nodes[-1]} is not an acceptable " f"input to {node}." + ) - # otherwise raw level is requested - return MeasLevel.RAW + self._nodes.append(node) - def output_key(self) -> str: + def output_key(self) -> Union[str, None]: """Return the key to look for in the data output by the processor.""" - if self._root_node: - node = self._root_node - while node.child: - node = node.child - return node.node_output + if len(self._nodes) > 0: + return self._nodes[-1].node_output - return "counts" + return None def format_data(self, data: Dict[str, Any]): """ - Format Qiskit result data. + Format the given data. This method sequentially calls stored child data processing nodes with its `format_data` methods. Once all child nodes have called, @@ -85,31 +69,5 @@ def format_data(self, data: Dict[str, Any]): data: The data, typically from an ExperimentData instance, that needs to be processed. This dict also contains the metadata of each experiment. """ - if self._root_node: - self._root_node.format_data(data) - - @classmethod - def check_kernel(cls, node: DataAction) -> Union[None, DataAction]: - """Return the stored kernel in the workflow.""" - if not node: - return None - - if node.node_type == NodeType.KERNEL: - return node - else: - if not node.child: - return None - return cls.check_kernel(node.child) - - @classmethod - def check_discriminator(cls, node: DataAction): - """Return stored discriminator in the workflow.""" - if not node: - return None - - if node.node_type == NodeType.DISCRIMINATOR: - return node - else: - if not node.child: - return None - return cls.check_discriminator(node.child) + for node in self._nodes: + node.format_data(data) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index bb6774c96a..6eb350e910 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -13,21 +13,12 @@ """Different data analysis steps.""" from abc import abstractmethod -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional import numpy as np -from qiskit_experiments.data_processing.base import ( - DataAction, - iq_data, - kernel, - discriminator, - prev_node, - population, -) +from qiskit_experiments.data_processing.base import DataAction -@kernel -@prev_node() class Kernel(DataAction): """User provided kernel.""" @@ -40,17 +31,13 @@ def __init__(self, kernel_, name: Optional[str] = None): self.kernel = kernel_ self.name = name super().__init__() + self._accepted_inputs = ["memory"] @property def node_output(self) -> str: """Key under which Kernel stores the data.""" return "memory" - @property - def node_inputs(self) -> List[str]: - """Returns a list of input data that the node can process.""" - return ["memory"] - def process(self, data: Dict[str, Any]): """ Args: @@ -62,8 +49,6 @@ def process(self, data: Dict[str, Any]): data[self.node_output] = self.kernel.kernel(np.array(data["memory"])) -@discriminator -@prev_node(Kernel) class Discriminator(DataAction): """Backend system discriminator.""" @@ -77,17 +62,13 @@ def __init__(self, discriminator_, name: Optional[str] = None): self.discriminator = discriminator_ self.name = name super().__init__() + self._accepted_inputs = ["memory", "memory_real", "memory_imag"] @property def node_output(self) -> str: """Key under which Discriminator stores the data.""" return "counts" - @property - def node_inputs(self) -> List[str]: - """Returns a list of input data that the node can process.""" - return ["memory"] - def process(self, data: Dict[str, Any]): """ Discriminate the data to transform it into counts. @@ -101,8 +82,6 @@ def process(self, data: Dict[str, Any]): data[self.node_output] = self.discriminator.discriminate(np.array(data["memory"])) -@iq_data -@prev_node(Kernel) class IQPart(DataAction): """Abstract class for IQ data post-processing.""" @@ -115,11 +94,7 @@ def __init__(self, scale: Optional[float] = 1.0, average: bool = False): self.scale = scale self.average = average super().__init__() - - @property - def node_inputs(self) -> List[str]: - """Returns a list of input data that the node can process.""" - return ["memory"] + self._accepted_inputs = ["memory"] @abstractmethod def _index(self) -> int: @@ -153,8 +128,6 @@ def process(self, data: Dict[str, Any]): data[self.node_output] = new_mem -@iq_data -@prev_node(Kernel) class ToReal(IQPart): """IQ data post-processing. Isolate the real part of the IQ data.""" @@ -168,8 +141,6 @@ def _index(self) -> int: return 0 -@iq_data -@prev_node(Kernel) class ToImag(IQPart): """IQ data post-processing. Isolate the imaginary part of the IQ data.""" @@ -183,21 +154,19 @@ def _index(self) -> int: return 1 -@population -@prev_node(Discriminator) class Population(DataAction): """Count data post processing. This returns population.""" + def __init__(self): + """Initialize a counts to population data conversion.""" + super().__init__() + self._accepted_inputs = ["counts"] + @property def node_output(self) -> str: """Key under which Population stores the data.""" return "populations" - @property - def node_inputs(self) -> List[str]: - """Returns a list of input data that the node can process.""" - return ["counts"] - def process(self, data: Dict[str, Any]): """ Args: diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index ede926ea73..d57d3dfc76 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -15,11 +15,11 @@ from qiskit.result.models import ExperimentResultData, ExperimentResult from qiskit.result import Result from qiskit.test import QiskitTestCase -from qiskit.qobj.utils import MeasLevel from qiskit.qobj.common import QobjExperimentHeader from qiskit_experiments import ExperimentData from qiskit_experiments.base_experiment import BaseExperiment from qiskit_experiments.data_processing.data_processor import DataProcessor +from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.data_processing.nodes import ( Kernel, Discriminator, @@ -122,21 +122,20 @@ def test_empty_processor(self): self.assertEqual(self.exp_data_lvl2.data[0]["counts"]["00"], 4) self.assertEqual(self.exp_data_lvl2.data[0]["counts"]["10"], 6) - def test_append_kernel(self): + def test_append(self): """Tests that we can add a kernel and a discriminator.""" processor = DataProcessor() - self.assertEqual(processor.meas_level(), MeasLevel.RAW) - - processor.append(Kernel(FakeKernel)) - self.assertEqual(processor.meas_level(), MeasLevel.KERNELED) - + processor.append(Kernel(None)) + processor.append(ToReal(1e-3)) processor.append(Discriminator(None)) - self.assertEqual(processor.meas_level(), MeasLevel.CLASSIFIED) + + with self.assertRaises(DataProcessorError): + processor.append(Kernel(None)) def test_output_key(self): """Test that we can properly get the output key from the node.""" processor = DataProcessor() - self.assertEqual(processor.output_key(), "counts") + self.assertIsNone(processor.output_key()) processor.append(Kernel(FakeKernel())) self.assertEqual(processor.output_key(), "memory") From 3f95b1e84e941149ff963d877f76cd0205dab9b3 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 31 Mar 2021 17:28:52 +0200 Subject: [PATCH 09/52] * Added history functionality to the data processor. --- qiskit_experiments/data_processing/base.py | 12 +++- .../data_processing/data_processor.py | 21 ++++++- qiskit_experiments/data_processing/nodes.py | 28 ++++++--- test/data_processing/test_data_processing.py | 60 +++++++++++++------ 4 files changed, 88 insertions(+), 33 deletions(-) diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py index c234bf7ea2..4725bb010d 100644 --- a/qiskit_experiments/data_processing/base.py +++ b/qiskit_experiments/data_processing/base.py @@ -50,12 +50,15 @@ def add_accepted_input(self, data_key: str): self._accepted_inputs.append(data_key) @abstractmethod - def process(self, data: Dict[str, Any]): + def process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Applies the data processing step to the data. Args: data: the data to which the data processing step will be applied. + + Returns: + processed data: The data that has been processed. """ def check_required(self, data: Dict[str, Any]): @@ -73,7 +76,7 @@ def check_required(self, data: Dict[str, Any]): raise DataProcessorError(f"None of {self.node_inputs} are in the given data.") - def format_data(self, data: Dict[str, Any]): + def format_data(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Apply the data action of this node and call the child node's format_data method. @@ -81,9 +84,12 @@ def format_data(self, data: Dict[str, Any]): data: A dict containing the data. The action nodes in the data processor will raise errors if the data does not contain the appropriate data. + + Returns: + processed data: The output data of the node contained in a dict. """ self.check_required(data) - self.process(data) + return self.process(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 8f31c6bb26..020954d93c 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -12,7 +12,7 @@ """Class that ties together actions on the data.""" -from typing import Any, Dict, Union +from typing import Any, Dict, List, Union from qiskit_experiments.data_processing.base import DataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -57,7 +57,7 @@ def output_key(self) -> Union[str, None]: return None - def format_data(self, data: Dict[str, Any]): + def format_data(self, data: Dict[str, Any], history: bool = False) -> List[Dict[str, Any]]: """ Format the given data. @@ -68,6 +68,21 @@ def format_data(self, data: Dict[str, Any]): Args: data: The data, typically from an ExperimentData instance, that needs to be processed. This dict also contains the metadata of each experiment. + history: If set to true a list of the formatted data at each step is returned. + + Returns: + processed data: The last entry in the list is the end output of the data processor. + If the history of the data actions is required the returned data is a list of + length n+1 where n is the number of data actions in the data processor. """ + data_steps = [] + for node in self._nodes: - node.format_data(data) + if history: + data_steps.append(dict(data)) + + data = node.format_data(data) + + data_steps.append(data) + + return data_steps diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 6eb350e910..f271782357 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -38,15 +38,18 @@ def node_output(self) -> str: """Key under which Kernel stores the data.""" return "memory" - def process(self, data: Dict[str, Any]): + def process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Args: data: The data dictionary to process. + Returns: + processed data: A dict with the data stored under "memory". + Raises: DataProcessorError: if the data has no memory. """ - data[self.node_output] = self.kernel.kernel(np.array(data["memory"])) + return {self.node_output: self.kernel.kernel(np.array(data["memory"]))} class Discriminator(DataAction): @@ -69,17 +72,20 @@ def node_output(self) -> str: """Key under which Discriminator stores the data.""" return "counts" - def process(self, data: Dict[str, Any]): + def process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Discriminate the data to transform it into counts. Args: data: The data in a format that can be understood by the discriminator. + Returns: + processed data: A dict with the data stored under "counts". + Raises: DataProcessorError: if the data does not contain memory. """ - data[self.node_output] = self.discriminator.discriminate(np.array(data["memory"])) + return {self.node_output: self.discriminator.discriminate(np.array(data["memory"]))} class IQPart(DataAction): @@ -100,7 +106,7 @@ def __init__(self, scale: Optional[float] = 1.0, average: bool = False): def _index(self) -> int: """Return 0 for real and 1 for imaginary part.""" - def process(self, data: Dict[str, Any]): + def process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Modifies the data inplace by taking the real part of the memory and scaling it by the given factor. @@ -108,6 +114,9 @@ def process(self, data: Dict[str, Any]): Args: data: The data dict. IQ data is stored under memory. + Returns: + processed data: A dict with the data. + Raises: DataProcessorError: if the data does not contain memory. """ @@ -125,7 +134,7 @@ def process(self, data: Dict[str, Any]): else: new_mem = [self.scale * iq_qubit[self._index] for iq_qubit in data["memory"]] - data[self.node_output] = new_mem + return {self.node_output: new_mem} class ToReal(IQPart): @@ -167,13 +176,16 @@ def node_output(self) -> str: """Key under which Population stores the data.""" return "populations" - def process(self, data: Dict[str, Any]): + def process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Args: data: The data dictionary. This will modify the dict in place, taking the data under counts and adding the corresponding populations. + Returns: + processed data: A dict with the populations. + Raises: DataProcessorError: if counts are not in the given data. """ @@ -189,4 +201,4 @@ def process(self, data: Dict[str, Any]): if bit == "1": populations[ind] += count - data[self.node_output] = populations / shots + return {self.node_output: populations / shots} diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index d57d3dfc76..bfd1babf6b 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -161,19 +161,23 @@ def test_to_real(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - processor.format_data(exp_data.data[0]) + new_data = processor.format_data(exp_data.data[0]) - expected = { + expected_old = { "memory": [ [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], ], - "memory_real": [[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]], "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } - self.assertEqual(exp_data.data[0], expected) + expected_new = { + "memory_real": [[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]], + } + + self.assertEqual(exp_data.data[0], expected_old) + self.assertEqual(new_data[-1], expected_new) # Test that we can average single-shots processor = DataProcessor() @@ -183,19 +187,23 @@ def test_to_real(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - processor.format_data(exp_data.data[0]) + new_data = processor.format_data(exp_data.data[0]) - expected = { + expected_old = { "memory": [ [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], ], - "memory_real": [1520.6480000000001, -1908.3846666666666], "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } - self.assertEqual(exp_data.data[0], expected) + expected_new = { + "memory_real": [1520.6480000000001, -1908.3846666666666], + } + + self.assertEqual(exp_data.data[0], expected_old) + self.assertEqual(new_data[-1], expected_new) def test_to_imag(self): """Test that we can average the data.""" @@ -206,23 +214,27 @@ def test_to_imag(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - processor.format_data(exp_data.data[0]) + new_data = processor.format_data(exp_data.data[0]) - expected = { + expected_old = { "memory": [ [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], ], + "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, + } + + expected_new = { "memory_imag": [ [-11378.508, -16488.753], [-19283.206000000002, -15339.630000000001], [-14548.009, -16743.348], ], - "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } - self.assertEqual(exp_data.data[0], expected) + self.assertEqual(exp_data.data[0], expected_old) + self.assertEqual(new_data[-1], expected_new) # Test that we can average single-shots processor = DataProcessor() @@ -232,26 +244,36 @@ def test_to_imag(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - processor.format_data(exp_data.data[0]) + new_data = processor.format_data(exp_data.data[0]) - expected = { + expected_old = { "memory": [ [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], ], - "memory_imag": [-15069.907666666666, -16190.577], "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } - self.assertEqual(exp_data.data[0], expected) + expected_new = { + "memory_imag": [-15069.907666666666, -16190.577], + } + + self.assertEqual(exp_data.data[0], expected_old) + self.assertEqual(new_data[-1], expected_new) + + # Test the history + new_data = processor.format_data(exp_data.data[0], history=True) + + self.assertEqual(new_data[0], expected_old) + self.assertEqual(new_data[1], expected_new) def test_populations(self): """Test that counts are properly converted to a population.""" processor = DataProcessor() processor.append(Population()) - processor.format_data(self.exp_data_lvl2.data[0]) + new_data = processor.format_data(self.exp_data_lvl2.data[0]) - self.assertEqual(self.exp_data_lvl2.data[0]["populations"][1], 0.0) - self.assertEqual(self.exp_data_lvl2.data[0]["populations"][0], 0.6) + self.assertEqual(new_data[-1]["populations"][1], 0.0) + self.assertEqual(new_data[-1]["populations"][0], 0.6) From 1883a9129968160d7595bcf424de6f0dcb03708c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 1 Apr 2021 10:51:53 +0200 Subject: [PATCH 10/52] * Made history of data processor a property. * Adapted unit tests accordingly. --- qiskit_experiments/data_processing/base.py | 4 ++- .../data_processing/data_processor.py | 26 ++++++++++------ test/data_processing/test_data_processing.py | 30 ++++++++++++------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py index 4725bb010d..ddd51cd870 100644 --- a/qiskit_experiments/data_processing/base.py +++ b/qiskit_experiments/data_processing/base.py @@ -89,7 +89,9 @@ def format_data(self, data: Dict[str, Any]) -> Dict[str, Any]: processed data: The output data of the node contained in a dict. """ self.check_required(data) - return self.process(data) + processed_data = self.process(data) + processed_data["metadata"] = data.get("metadata", {}) + return processed_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 020954d93c..065435aa63 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -12,7 +12,7 @@ """Class that ties together actions on the data.""" -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Tuple, Union from qiskit_experiments.data_processing.base import DataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -27,6 +27,16 @@ class DataProcessor: def __init__(self): """Create an empty chain of data processing actions.""" self._nodes = [] + self._history = [] + + @property + def history(self) -> List[Tuple[str, Dict[str, Any]]]: + """ + Returns: + The history of the data processor. The ith tuple in the history corresponds to the + output of the ith node. Each tuple corresponds to (node name, data dict). + """ + return self._history def append(self, node: DataAction): """ @@ -57,7 +67,7 @@ def output_key(self) -> Union[str, None]: return None - def format_data(self, data: Dict[str, Any], history: bool = False) -> List[Dict[str, Any]]: + def format_data(self, data: Dict[str, Any], save_history: bool = False) -> Dict[str, Any]: """ Format the given data. @@ -68,21 +78,19 @@ def format_data(self, data: Dict[str, Any], history: bool = False) -> List[Dict[ Args: data: The data, typically from an ExperimentData instance, that needs to be processed. This dict also contains the metadata of each experiment. - history: If set to true a list of the formatted data at each step is returned. + save_history: If set to true a list of the formatted data at each step is returned. Returns: processed data: The last entry in the list is the end output of the data processor. If the history of the data actions is required the returned data is a list of length n+1 where n is the number of data actions in the data processor. """ - data_steps = [] + self._history = [] for node in self._nodes: - if history: - data_steps.append(dict(data)) - data = node.format_data(data) - data_steps.append(data) + if save_history: + self._history.append((node.__class__.__name__, dict(data))) - return data_steps + return data diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index bfd1babf6b..c6fd55427b 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -174,10 +174,11 @@ def test_to_real(self): expected_new = { "memory_real": [[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]], + "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } self.assertEqual(exp_data.data[0], expected_old) - self.assertEqual(new_data[-1], expected_new) + self.assertEqual(new_data, expected_new) # Test that we can average single-shots processor = DataProcessor() @@ -187,7 +188,7 @@ def test_to_real(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - new_data = processor.format_data(exp_data.data[0]) + new_data = processor.format_data(exp_data.data[0], save_history=True) expected_old = { "memory": [ @@ -200,10 +201,16 @@ def test_to_real(self): expected_new = { "memory_real": [1520.6480000000001, -1908.3846666666666], + "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } self.assertEqual(exp_data.data[0], expected_old) - self.assertEqual(new_data[-1], expected_new) + self.assertEqual(new_data, expected_new) + + # Check the history + history = processor.history + self.assertEqual(history[0][0], "ToReal") + self.assertEqual(history[0][1], expected_new) def test_to_imag(self): """Test that we can average the data.""" @@ -231,10 +238,11 @@ def test_to_imag(self): [-19283.206000000002, -15339.630000000001], [-14548.009, -16743.348], ], + "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } self.assertEqual(exp_data.data[0], expected_old) - self.assertEqual(new_data[-1], expected_new) + self.assertEqual(new_data, expected_new) # Test that we can average single-shots processor = DataProcessor() @@ -257,16 +265,18 @@ def test_to_imag(self): expected_new = { "memory_imag": [-15069.907666666666, -16190.577], + "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } self.assertEqual(exp_data.data[0], expected_old) - self.assertEqual(new_data[-1], expected_new) + self.assertEqual(new_data, expected_new) + self.assertEqual(processor.history, []) # Test the history - new_data = processor.format_data(exp_data.data[0], history=True) + new_data = processor.format_data(exp_data.data[0], save_history=True) - self.assertEqual(new_data[0], expected_old) - self.assertEqual(new_data[1], expected_new) + self.assertEqual(exp_data.data[0], expected_old) + self.assertEqual(new_data, expected_new) def test_populations(self): """Test that counts are properly converted to a population.""" @@ -275,5 +285,5 @@ def test_populations(self): processor.append(Population()) new_data = processor.format_data(self.exp_data_lvl2.data[0]) - self.assertEqual(new_data[-1]["populations"][1], 0.0) - self.assertEqual(new_data[-1]["populations"][0], 0.6) + self.assertEqual(new_data["populations"][1], 0.0) + self.assertEqual(new_data["populations"][0], 0.6) From 6b6051ce32fa5a34fd5533da47e63d757a6ca484 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 1 Apr 2021 10:56:22 +0200 Subject: [PATCH 11/52] * Fixed docstring. --- 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 065435aa63..5473939a95 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -78,12 +78,11 @@ def format_data(self, data: Dict[str, Any], save_history: bool = False) -> Dict[ Args: data: The data, typically from an ExperimentData instance, that needs to be processed. This dict also contains the metadata of each experiment. - save_history: If set to true a list of the formatted data at each step is returned. + save_history: If set to true the history is saved under the history property. + If set to False the history will be empty. Returns: - processed data: The last entry in the list is the end output of the data processor. - If the history of the data actions is required the returned data is a list of - length n+1 where n is the number of data actions in the data processor. + processed data: The data processed by the data processor.. """ self._history = [] From e32341e8a939363c91a41ccdfedf6c5d13ac9551 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 1 Apr 2021 11:07:13 +0200 Subject: [PATCH 12/52] * Added _process to the IQPart data actions. --- qiskit_experiments/data_processing/nodes.py | 55 ++++++++++++--------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index f271782357..a42ad265ef 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -13,7 +13,7 @@ """Different data analysis steps.""" from abc import abstractmethod -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Tuple import numpy as np from qiskit_experiments.data_processing.base import DataAction @@ -45,9 +45,6 @@ def process(self, data: Dict[str, Any]) -> Dict[str, Any]: Returns: processed data: A dict with the data stored under "memory". - - Raises: - DataProcessorError: if the data has no memory. """ return {self.node_output: self.kernel.kernel(np.array(data["memory"]))} @@ -81,9 +78,6 @@ def process(self, data: Dict[str, Any]) -> Dict[str, Any]: Returns: processed data: A dict with the data stored under "counts". - - Raises: - DataProcessorError: if the data does not contain memory. """ return {self.node_output: self.discriminator.discriminate(np.array(data["memory"]))} @@ -103,8 +97,15 @@ def __init__(self, scale: Optional[float] = 1.0, average: bool = False): self._accepted_inputs = ["memory"] @abstractmethod - def _index(self) -> int: - """Return 0 for real and 1 for imaginary part.""" + def _process(self, point: Tuple[float, float]) -> float: + """Defines how the IQ point will be processed. + + Args: + point: An IQ point as a tuple of two float, i.e. (real, imaginary). + + Returns: + Processed IQ point. + """ def process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ @@ -116,23 +117,20 @@ def process(self, data: Dict[str, Any]) -> Dict[str, Any]: Returns: processed data: A dict with the data. - - Raises: - DataProcessorError: if the data does not contain memory. """ # Single shot data if isinstance(data["memory"][0][0], list): new_mem = [] for shot in data["memory"]: - new_mem.append([self.scale * iq_qubit[self._index()] for iq_qubit in shot]) + new_mem.append([self.scale * self._process(iq_point) for iq_point in shot]) if self.average: new_mem = list(np.mean(np.array(new_mem), axis=0)) # Averaged data else: - new_mem = [self.scale * iq_qubit[self._index] for iq_qubit in data["memory"]] + new_mem = [self.scale * self._process(iq_point) for iq_point in data["memory"]] return {self.node_output: new_mem} @@ -145,9 +143,16 @@ def node_output(self) -> str: """Key under which ToReal stores the data.""" return "memory_real" - def _index(self) -> int: - """Return 0 for real part.""" - return 0 + def _process(self, point: Tuple[float, float]) -> float: + """Defines how the IQ point will be processed. + + Args: + point: An IQ point as a tuple of two float, i.e. (real, imaginary). + + Returns: + The real part of the IQ point. + """ + return point[0] class ToImag(IQPart): @@ -158,9 +163,16 @@ def node_output(self) -> str: """Key under which ToImag stores the data.""" return "memory_imag" - def _index(self) -> int: - """Return 0 for real part.""" - return 1 + def _process(self, point: Tuple[float, float]) -> float: + """Defines how the IQ point will be processed. + + Args: + point: An IQ point as a tuple of two float, i.e. (real, imaginary). + + Returns: + The imaginary part of the IQ point. + """ + return point[1] class Population(DataAction): @@ -185,9 +197,6 @@ def process(self, data: Dict[str, Any]) -> Dict[str, Any]: Returns: processed data: A dict with the populations. - - Raises: - DataProcessorError: if counts are not in the given data. """ counts = data.get("counts") From 681546f481a588ff1d6e99a9be2fd4861fd2c9fe Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 1 Apr 2021 11:26:20 +0200 Subject: [PATCH 13/52] * Changed node_output to a class variable. --- qiskit_experiments/data_processing/base.py | 10 ++--- .../data_processing/data_processor.py | 4 +- qiskit_experiments/data_processing/nodes.py | 39 ++++++------------- 3 files changed, 18 insertions(+), 35 deletions(-) diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py index ddd51cd870..4987845bc4 100644 --- a/qiskit_experiments/data_processing/base.py +++ b/qiskit_experiments/data_processing/base.py @@ -25,16 +25,14 @@ class DataAction(metaclass=ABCMeta): using decorators. """ + # Key under which the node will output the data. + __node_output__ = None + def __init__(self): """Create new data analysis routine.""" self._child = None self._accepted_inputs = [] - @property - @abstractmethod - def node_output(self) -> str: - """Returns the key in the data dict where the DataAction added the processed data.""" - @property def node_inputs(self) -> List[str]: """Returns a list of input data that the node can process.""" @@ -97,5 +95,5 @@ def __repr__(self): """String representation of the node.""" return ( f"{self.__class__.__name__}(inputs: {self.node_inputs}, " - f"outputs: {self.node_output})" + f"outputs: {self.__node_output__})" ) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 5473939a95..b3fc7969cb 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -52,7 +52,7 @@ def append(self, node: DataAction): if len(self._nodes) == 0: self._nodes.append(node) else: - if self._nodes[-1].node_output not in node.node_inputs: + if self._nodes[-1].__node_output__ not in node.node_inputs: raise DataProcessorError( f"Output of node {self._nodes[-1]} is not an acceptable " f"input to {node}." ) @@ -63,7 +63,7 @@ def output_key(self) -> Union[str, None]: """Return the key to look for in the data output by the processor.""" if len(self._nodes) > 0: - return self._nodes[-1].node_output + return self._nodes[-1].__node_output__ return None diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index a42ad265ef..00dd4758e4 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -22,6 +22,8 @@ class Kernel(DataAction): """User provided kernel.""" + __node_output__ = "memory" + def __init__(self, kernel_, name: Optional[str] = None): """ Args: @@ -33,11 +35,6 @@ def __init__(self, kernel_, name: Optional[str] = None): super().__init__() self._accepted_inputs = ["memory"] - @property - def node_output(self) -> str: - """Key under which Kernel stores the data.""" - return "memory" - def process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Args: @@ -46,12 +43,14 @@ def process(self, data: Dict[str, Any]) -> Dict[str, Any]: Returns: processed data: A dict with the data stored under "memory". """ - return {self.node_output: self.kernel.kernel(np.array(data["memory"]))} + return {self.__node_output__: self.kernel.kernel(np.array(data["memory"]))} class Discriminator(DataAction): """Backend system discriminator.""" + __node_output__ = "counts" + def __init__(self, discriminator_, name: Optional[str] = None): """ Args: @@ -64,11 +63,6 @@ def __init__(self, discriminator_, name: Optional[str] = None): super().__init__() self._accepted_inputs = ["memory", "memory_real", "memory_imag"] - @property - def node_output(self) -> str: - """Key under which Discriminator stores the data.""" - return "counts" - def process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Discriminate the data to transform it into counts. @@ -79,7 +73,7 @@ def process(self, data: Dict[str, Any]) -> Dict[str, Any]: Returns: processed data: A dict with the data stored under "counts". """ - return {self.node_output: self.discriminator.discriminate(np.array(data["memory"]))} + return {self.__node_output__: self.discriminator.discriminate(np.array(data["memory"]))} class IQPart(DataAction): @@ -132,16 +126,13 @@ def process(self, data: Dict[str, Any]) -> Dict[str, Any]: else: new_mem = [self.scale * self._process(iq_point) for iq_point in data["memory"]] - return {self.node_output: new_mem} + return {self.__node_output__: new_mem} class ToReal(IQPart): """IQ data post-processing. Isolate the real part of the IQ data.""" - @property - def node_output(self) -> str: - """Key under which ToReal stores the data.""" - return "memory_real" + __node_output__ = "memory_real" def _process(self, point: Tuple[float, float]) -> float: """Defines how the IQ point will be processed. @@ -158,10 +149,7 @@ def _process(self, point: Tuple[float, float]) -> float: class ToImag(IQPart): """IQ data post-processing. Isolate the imaginary part of the IQ data.""" - @property - def node_output(self) -> str: - """Key under which ToImag stores the data.""" - return "memory_imag" + __node_output__ = "memory_imag" def _process(self, point: Tuple[float, float]) -> float: """Defines how the IQ point will be processed. @@ -178,16 +166,13 @@ def _process(self, point: Tuple[float, float]) -> float: class Population(DataAction): """Count data post processing. This returns population.""" + __node_output__ = "populations" + def __init__(self): """Initialize a counts to population data conversion.""" super().__init__() self._accepted_inputs = ["counts"] - @property - def node_output(self) -> str: - """Key under which Population stores the data.""" - return "populations" - def process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Args: @@ -210,4 +195,4 @@ def process(self, data: Dict[str, Any]) -> Dict[str, Any]: if bit == "1": populations[ind] += count - return {self.node_output: populations / shots} + return {self.__node_output__: populations / shots} From 93cdfae692be4cb9d3c4ac603804986d5c5113f5 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 1 Apr 2021 11:31:48 +0200 Subject: [PATCH 14/52] * Added the option to initialize the DataProcessor with given DataActions. --- .../data_processing/data_processor.py | 14 ++++++++++++-- test/data_processing/test_data_processing.py | 3 +-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index b3fc7969cb..1d4b10f7ef 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -24,9 +24,19 @@ class DataProcessor: by the calibration analysis classes. """ - def __init__(self): - """Create an empty chain of data processing actions.""" + def __init__(self, data_actions: List[DataAction] = None): + """Create a chain of data processing actions. + + Args: + data_actions: A list of data processing actions to construct this data processor with. + If None is given an empty DataProcessor will be created. + """ self._nodes = [] + + if data_actions: + for node in data_actions: + self.append(node) + self._history = [] @property diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index c6fd55427b..5e2d15a9e4 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -181,8 +181,7 @@ def test_to_real(self): self.assertEqual(new_data, expected_new) # Test that we can average single-shots - processor = DataProcessor() - processor.append(ToReal(scale=1e-3, average=True)) + processor = DataProcessor([ToReal(scale=1e-3, average=True)]) self.assertEqual(processor.output_key(), "memory_real") exp_data = ExperimentData(FakeExperiment()) From 21374e5eb0a4b6cf164b538be1866362ecdc2de2 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 1 Apr 2021 18:27:39 +0200 Subject: [PATCH 15/52] * Removed Kernel and Discriminator. They will be for a future PR. --- .../data_processing/__init__.py | 4 -- qiskit_experiments/data_processing/nodes.py | 57 ------------------- test/data_processing/test_data_processing.py | 26 +-------- 3 files changed, 3 insertions(+), 84 deletions(-) diff --git a/qiskit_experiments/data_processing/__init__.py b/qiskit_experiments/data_processing/__init__.py index bdec94f9a4..984a6a4cff 100644 --- a/qiskit_experiments/data_processing/__init__.py +++ b/qiskit_experiments/data_processing/__init__.py @@ -14,10 +14,6 @@ from .base import DataAction from .nodes import ( - # data acquisition node - Discriminator, - Kernel, - # value formatter node Population, ToImag, ToReal, diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 00dd4758e4..ac665aedc9 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -19,63 +19,6 @@ from qiskit_experiments.data_processing.base import DataAction -class Kernel(DataAction): - """User provided kernel.""" - - __node_output__ = "memory" - - def __init__(self, kernel_, name: Optional[str] = None): - """ - Args: - kernel_: Kernel to kernel the data. - name: Optional name for the node. - """ - self.kernel = kernel_ - self.name = name - super().__init__() - self._accepted_inputs = ["memory"] - - def process(self, data: Dict[str, Any]) -> Dict[str, Any]: - """ - Args: - data: The data dictionary to process. - - Returns: - processed data: A dict with the data stored under "memory". - """ - return {self.__node_output__: self.kernel.kernel(np.array(data["memory"]))} - - -class Discriminator(DataAction): - """Backend system discriminator.""" - - __node_output__ = "counts" - - def __init__(self, discriminator_, name: Optional[str] = None): - """ - Args: - discriminator_: The discriminator used to transform the data to counts. - For example, transform IQ data to counts. - name: Optional name for the node. - """ - self.discriminator = discriminator_ - self.name = name - super().__init__() - self._accepted_inputs = ["memory", "memory_real", "memory_imag"] - - def process(self, data: Dict[str, Any]) -> Dict[str, Any]: - """ - Discriminate the data to transform it into counts. - - Args: - data: The data in a format that can be understood by the discriminator. - - Returns: - processed data: A dict with the data stored under "counts". - """ - return {self.__node_output__: self.discriminator.discriminate(np.array(data["memory"]))} - - class IQPart(DataAction): """Abstract class for IQ data post-processing.""" diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 5e2d15a9e4..bcb2bbf7c7 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -21,22 +21,12 @@ from qiskit_experiments.data_processing.data_processor import DataProcessor from qiskit_experiments.data_processing.exceptions import DataProcessorError from qiskit_experiments.data_processing.nodes import ( - Kernel, - Discriminator, ToReal, ToImag, Population, ) -class FakeKernel: - """Fake kernel to test the data chain.""" - - def kernel(self, data): - """Fake kernel method""" - return data - - class FakeExperiment(BaseExperiment): """Fake experiment class for testing.""" @@ -123,31 +113,21 @@ def test_empty_processor(self): self.assertEqual(self.exp_data_lvl2.data[0]["counts"]["10"], 6) def test_append(self): - """Tests that we can add a kernel and a discriminator.""" + """Tests that append catches inconsistent data processing chains.""" processor = DataProcessor() - processor.append(Kernel(None)) - processor.append(ToReal(1e-3)) - processor.append(Discriminator(None)) + processor.append(Population()) with self.assertRaises(DataProcessorError): - processor.append(Kernel(None)) + processor.append(ToReal(1e-3)) def test_output_key(self): """Test that we can properly get the output key from the node.""" processor = DataProcessor() self.assertIsNone(processor.output_key()) - processor.append(Kernel(FakeKernel())) - self.assertEqual(processor.output_key(), "memory") - processor.append(ToReal()) self.assertEqual(processor.output_key(), "memory_real") - processor = DataProcessor() - processor.append(Kernel(FakeKernel())) - processor.append(Discriminator(None)) - self.assertEqual(processor.output_key(), "counts") - processor = DataProcessor() processor.append(Population()) self.assertEqual(processor.output_key(), "populations") From 5f7c08210e5ad6099c1fe5838ca9983a09a4615a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 9 Apr 2021 09:13:48 +0200 Subject: [PATCH 16/52] * Added docstring from Will. Co-authored-by: Will Shanks --- .../data_processing/data_processor.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 1d4b10f7ef..255825aa90 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -10,7 +10,20 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Class that ties together actions on the data.""" +""" +A DataProcessor defines a sequence of operations to perform on experimental data. +The DataProcessor.format_data() method applies this sequence on its input argument. +A DataProcessor is created with a list of DataAction objects. Each DataAction +specifies a set of node_inputs that it accepts and a __node_output__ that it +provides. The __node_output__ of each DataAction must be contained in the node_inputs +of the following DataAction in the DataProcessor's list. DataProcessor.format_data() +usually takes in one entry from the data property of an ExperimentData object +(i.e. a dict containing metadata and memory keys and possibly counts, like the +Result.data property) and produces a new dict containing the formatted data. The data +passed to DataProcessor.format_data() is passed to the first DataAction and the +output is passed on in turn to each DataAction. DataProcessor.format_data() returns +the data produced by the last DataAction. +""" from typing import Any, Dict, List, Tuple, Union From 8838f620888696ae5579592bf31442479b36b8a3 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 12 Apr 2021 08:30:25 +0200 Subject: [PATCH 17/52] * Moved docstring. --- .../data_processing/data_processor.py | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 255825aa90..2140580b95 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -10,20 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -""" -A DataProcessor defines a sequence of operations to perform on experimental data. -The DataProcessor.format_data() method applies this sequence on its input argument. -A DataProcessor is created with a list of DataAction objects. Each DataAction -specifies a set of node_inputs that it accepts and a __node_output__ that it -provides. The __node_output__ of each DataAction must be contained in the node_inputs -of the following DataAction in the DataProcessor's list. DataProcessor.format_data() -usually takes in one entry from the data property of an ExperimentData object -(i.e. a dict containing metadata and memory keys and possibly counts, like the -Result.data property) and produces a new dict containing the formatted data. The data -passed to DataProcessor.format_data() is passed to the first DataAction and the -output is passed on in turn to each DataAction. DataProcessor.format_data() returns -the data produced by the last DataAction. -""" +"""Actions done on the data to bring it in a usable form.""" from typing import Any, Dict, List, Tuple, Union @@ -33,8 +20,18 @@ class DataProcessor: """ - Defines the actions done on the measured data to bring it in a form usable - by the calibration analysis classes. + A DataProcessor defines a sequence of operations to perform on experimental data. + The DataProcessor.format_data() method applies this sequence on its input argument. + A DataProcessor is created with a list of DataAction objects. Each DataAction + specifies a set of node_inputs that it accepts and a __node_output__ that it + provides. The __node_output__ of each DataAction must be contained in the node_inputs + of the following DataAction in the DataProcessor's list. DataProcessor.format_data() + usually takes in one entry from the data property of an ExperimentData object + (i.e. a dict containing metadata and memory keys and possibly counts, like the + Result.data property) and produces a new dict containing the formatted data. The data + passed to DataProcessor.format_data() is passed to the first DataAction and the + output is passed on in turn to each DataAction. DataProcessor.format_data() returns + the data produced by the last DataAction. """ def __init__(self, data_actions: List[DataAction] = None): From 77d0bb0ee2ef5003aba626e2f26989854f0441a9 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Mon, 12 Apr 2021 08:32:23 +0200 Subject: [PATCH 18/52] Update qiskit_experiments/data_processing/base.py Co-authored-by: Christopher J. Wood --- qiskit_experiments/data_processing/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py index 4987845bc4..31debb1980 100644 --- a/qiskit_experiments/data_processing/base.py +++ b/qiskit_experiments/data_processing/base.py @@ -48,7 +48,7 @@ def add_accepted_input(self, data_key: str): self._accepted_inputs.append(data_key) @abstractmethod - def process(self, data: Dict[str, Any]) -> Dict[str, Any]: + def _process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Applies the data processing step to the data. From 81ddf2838cbae22f6906f79e0608b55fc50c9173 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Mon, 12 Apr 2021 08:32:36 +0200 Subject: [PATCH 19/52] Update qiskit_experiments/data_processing/base.py Co-authored-by: Christopher J. Wood --- qiskit_experiments/data_processing/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py index 31debb1980..9355b822fb 100644 --- a/qiskit_experiments/data_processing/base.py +++ b/qiskit_experiments/data_processing/base.py @@ -59,7 +59,7 @@ def _process(self, data: Dict[str, Any]) -> Dict[str, Any]: processed data: The data that has been processed. """ - def check_required(self, data: Dict[str, Any]): + def _check_required(self, data: Dict[str, Any]): """Checks that the given data contains the right key. Args: From 6af38d91625a8ee950a7b1e9ea31a59efd8e49b9 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 12 Apr 2021 08:42:32 +0200 Subject: [PATCH 20/52] * Aligned code to _process. --- qiskit_experiments/data_processing/base.py | 4 ++-- qiskit_experiments/data_processing/nodes.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/base.py index 9355b822fb..566c78b62f 100644 --- a/qiskit_experiments/data_processing/base.py +++ b/qiskit_experiments/data_processing/base.py @@ -86,8 +86,8 @@ def format_data(self, data: Dict[str, Any]) -> Dict[str, Any]: Returns: processed data: The output data of the node contained in a dict. """ - self.check_required(data) - processed_data = self.process(data) + self._check_required(data) + processed_data = self._process(data) processed_data["metadata"] = data.get("metadata", {}) return processed_data diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index ac665aedc9..a65aaa01c8 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -34,7 +34,7 @@ def __init__(self, scale: Optional[float] = 1.0, average: bool = False): self._accepted_inputs = ["memory"] @abstractmethod - def _process(self, point: Tuple[float, float]) -> float: + def _process_iq(self, point: Tuple[float, float]) -> float: """Defines how the IQ point will be processed. Args: @@ -44,7 +44,7 @@ def _process(self, point: Tuple[float, float]) -> float: Processed IQ point. """ - def process(self, data: Dict[str, Any]) -> Dict[str, Any]: + def _process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Modifies the data inplace by taking the real part of the memory and scaling it by the given factor. @@ -60,14 +60,14 @@ def process(self, data: Dict[str, Any]) -> Dict[str, Any]: if isinstance(data["memory"][0][0], list): new_mem = [] for shot in data["memory"]: - new_mem.append([self.scale * self._process(iq_point) for iq_point in shot]) + new_mem.append([self.scale * self._process_iq(iq_point) for iq_point in shot]) if self.average: new_mem = list(np.mean(np.array(new_mem), axis=0)) # Averaged data else: - new_mem = [self.scale * self._process(iq_point) for iq_point in data["memory"]] + new_mem = [self.scale * self._process_iq(iq_point) for iq_point in data["memory"]] return {self.__node_output__: new_mem} @@ -77,7 +77,7 @@ class ToReal(IQPart): __node_output__ = "memory_real" - def _process(self, point: Tuple[float, float]) -> float: + def _process_iq(self, point: Tuple[float, float]) -> float: """Defines how the IQ point will be processed. Args: @@ -94,7 +94,7 @@ class ToImag(IQPart): __node_output__ = "memory_imag" - def _process(self, point: Tuple[float, float]) -> float: + def _process_iq(self, point: Tuple[float, float]) -> float: """Defines how the IQ point will be processed. Args: @@ -116,7 +116,7 @@ def __init__(self): super().__init__() self._accepted_inputs = ["counts"] - def process(self, data: Dict[str, Any]) -> Dict[str, Any]: + def _process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Args: data: The data dictionary. This will modify the dict in place, From bd230b53696e30b15b1de81e7c9e6c1d90e23efd Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 12 Apr 2021 12:02:16 +0200 Subject: [PATCH 21/52] * Made data processor callable. --- .../data_processing/data_processor.py | 4 ++-- test/data_processing/test_data_processing.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 2140580b95..ed0a2d66ef 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -87,9 +87,9 @@ def output_key(self) -> Union[str, None]: return None - def format_data(self, data: Dict[str, Any], save_history: bool = False) -> Dict[str, Any]: + def __call__(self, data: Dict[str, Any], save_history: bool = False) -> Dict[str, Any]: """ - Format the given data. + Call self on the given data. This method sequentially calls stored child data processing nodes with its `format_data` methods. Once all child nodes have called, diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index bcb2bbf7c7..3015365a86 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -108,7 +108,7 @@ def setUp(self): def test_empty_processor(self): """Check that a DataProcessor without steps does nothing.""" data_processor = DataProcessor() - data_processor.format_data(self.exp_data_lvl2.data) + data_processor(self.exp_data_lvl2.data) self.assertEqual(self.exp_data_lvl2.data[0]["counts"]["00"], 4) self.assertEqual(self.exp_data_lvl2.data[0]["counts"]["10"], 6) @@ -141,7 +141,7 @@ def test_to_real(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - new_data = processor.format_data(exp_data.data[0]) + new_data = processor(exp_data.data[0]) expected_old = { "memory": [ @@ -167,7 +167,7 @@ def test_to_real(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - new_data = processor.format_data(exp_data.data[0], save_history=True) + new_data = processor(exp_data.data[0], save_history=True) expected_old = { "memory": [ @@ -200,7 +200,7 @@ def test_to_imag(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - new_data = processor.format_data(exp_data.data[0]) + new_data = processor(exp_data.data[0]) expected_old = { "memory": [ @@ -231,7 +231,7 @@ def test_to_imag(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - new_data = processor.format_data(exp_data.data[0]) + new_data = processor(exp_data.data[0]) expected_old = { "memory": [ @@ -252,7 +252,7 @@ def test_to_imag(self): self.assertEqual(processor.history, []) # Test the history - new_data = processor.format_data(exp_data.data[0], save_history=True) + new_data = processor(exp_data.data[0], save_history=True) self.assertEqual(exp_data.data[0], expected_old) self.assertEqual(new_data, expected_new) @@ -262,7 +262,7 @@ def test_populations(self): processor = DataProcessor() processor.append(Population()) - new_data = processor.format_data(self.exp_data_lvl2.data[0]) + new_data = processor(self.exp_data_lvl2.data[0]) self.assertEqual(new_data["populations"][1], 0.0) self.assertEqual(new_data["populations"][0], 0.6) From c9801a7f22d6582ce0a329b81d1bbbb4e70af307 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 12 Apr 2021 12:04:29 +0200 Subject: [PATCH 22/52] * Renamed base.py to data_action.py. --- qiskit_experiments/data_processing/__init__.py | 2 +- qiskit_experiments/data_processing/{base.py => data_action.py} | 0 qiskit_experiments/data_processing/data_processor.py | 2 +- qiskit_experiments/data_processing/nodes.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename qiskit_experiments/data_processing/{base.py => data_action.py} (100%) diff --git a/qiskit_experiments/data_processing/__init__.py b/qiskit_experiments/data_processing/__init__.py index 984a6a4cff..d10c195ec6 100644 --- a/qiskit_experiments/data_processing/__init__.py +++ b/qiskit_experiments/data_processing/__init__.py @@ -12,7 +12,7 @@ """Qiskit experiments calibration data processing roots.""" -from .base import DataAction +from .data_action import DataAction from .nodes import ( Population, ToImag, diff --git a/qiskit_experiments/data_processing/base.py b/qiskit_experiments/data_processing/data_action.py similarity index 100% rename from qiskit_experiments/data_processing/base.py rename to qiskit_experiments/data_processing/data_action.py diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index ed0a2d66ef..c1ddaa8563 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -14,7 +14,7 @@ from typing import Any, Dict, List, Tuple, Union -from qiskit_experiments.data_processing.base import DataAction +from qiskit_experiments.data_processing.data_action import DataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index a65aaa01c8..c936d1b4fe 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -16,7 +16,7 @@ from typing import Any, Dict, Optional, Tuple import numpy as np -from qiskit_experiments.data_processing.base import DataAction +from qiskit_experiments.data_processing.data_action import DataAction class IQPart(DataAction): From ca2d365311ea98ddbe2c1218de291c322443b9c9 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 12 Apr 2021 12:09:29 +0200 Subject: [PATCH 23/52] * Made nodes callable. --- qiskit_experiments/data_processing/data_action.py | 4 ++-- qiskit_experiments/data_processing/data_processor.py | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 566c78b62f..909bcccc7d 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -74,9 +74,9 @@ def _check_required(self, data: Dict[str, Any]): raise DataProcessorError(f"None of {self.node_inputs} are in the given data.") - def format_data(self, data: Dict[str, Any]) -> Dict[str, Any]: + def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]: """ - Apply the data action of this node and call the child node's format_data method. + Call the data action of this node on the data. Args: data: A dict containing the data. The action nodes in the data diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index c1ddaa8563..0c51138a31 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -89,11 +89,8 @@ def output_key(self) -> Union[str, None]: def __call__(self, data: Dict[str, Any], save_history: bool = False) -> Dict[str, Any]: """ - Call self on the given data. - - This method sequentially calls stored child data processing nodes - with its `format_data` methods. Once all child nodes have called, - input data is converted into expected data format. + Call self on the given data. This method sequentially calls the stored data actions + on the data. Args: data: The data, typically from an ExperimentData instance, that needs to @@ -107,7 +104,7 @@ def __call__(self, data: Dict[str, Any], save_history: bool = False) -> Dict[str self._history = [] for node in self._nodes: - data = node.format_data(data) + data = node(data) if save_history: self._history.append((node.__class__.__name__, dict(data))) From 06095d291f8fc35f00672f89dd88438c6233785a Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Mon, 12 Apr 2021 18:17:07 +0200 Subject: [PATCH 24/52] * Removed history property, added call_with_history. --- .../data_processing/data_processor.py | 39 ++++++++++--------- test/data_processing/test_data_processing.py | 8 ++-- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 0c51138a31..a57714fa70 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -49,15 +49,6 @@ def __init__(self, data_actions: List[DataAction] = None): self._history = [] - @property - def history(self) -> List[Tuple[str, Dict[str, Any]]]: - """ - Returns: - The history of the data processor. The ith tuple in the history corresponds to the - output of the ith node. Each tuple corresponds to (node name, data dict). - """ - return self._history - def append(self, node: DataAction): """ Append new data action node to this data processor. @@ -87,7 +78,7 @@ def output_key(self) -> Union[str, None]: return None - def __call__(self, data: Dict[str, Any], save_history: bool = False) -> Dict[str, Any]: + def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Call self on the given data. This method sequentially calls the stored data actions on the data. @@ -95,18 +86,30 @@ def __call__(self, data: Dict[str, Any], save_history: bool = False) -> Dict[str Args: data: The data, typically from an ExperimentData instance, that needs to be processed. This dict also contains the metadata of each experiment. - save_history: If set to true the history is saved under the history property. - If set to False the history will be empty. Returns: - processed data: The data processed by the data processor.. + processed data: The data processed by the data processor. """ - self._history = [] - for node in self._nodes: data = node(data) - if save_history: - self._history.append((node.__class__.__name__, dict(data))) - return data + + def call_with_history(self, data: Dict[str, Any]) -> Tuple[Dict[str, Any], List]: + """ + Process the given data but save each step in a list returned to the user. + + Args: + data: The data, typically from an ExperimentData instance, that needs to + be processed. This dict also contains the metadata of each experiment. + + Returns: + processed data: The data processed by the data processor. + history: The data processed at each node of the data processor. + """ + history = [] + for node in self._nodes: + data = node(data) + history.append((node.__class__.__name__, dict(data))) + + return data, history diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 3015365a86..4303370198 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -167,7 +167,7 @@ def test_to_real(self): exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) - new_data = processor(exp_data.data[0], save_history=True) + new_data, history = processor.call_with_history(exp_data.data[0]) expected_old = { "memory": [ @@ -187,7 +187,6 @@ def test_to_real(self): self.assertEqual(new_data, expected_new) # Check the history - history = processor.history self.assertEqual(history[0][0], "ToReal") self.assertEqual(history[0][1], expected_new) @@ -249,13 +248,14 @@ def test_to_imag(self): self.assertEqual(exp_data.data[0], expected_old) self.assertEqual(new_data, expected_new) - self.assertEqual(processor.history, []) # Test the history - new_data = processor(exp_data.data[0], save_history=True) + new_data, history = processor.call_with_history(exp_data.data[0],) self.assertEqual(exp_data.data[0], expected_old) self.assertEqual(new_data, expected_new) + self.assertEqual(history[0][0], "ToImag") + self.assertEqual(history[0][1], expected_new) def test_populations(self): """Test that counts are properly converted to a population.""" From 22acc21309bff4382a335e64ccd74db999f6e60b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 10:06:39 +0200 Subject: [PATCH 25/52] * Renamed Population to Probability. --- qiskit_experiments/data_processing/__init__.py | 2 +- qiskit_experiments/data_processing/nodes.py | 17 ++++++++--------- test/data_processing/test_data_processing.py | 8 ++++---- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/qiskit_experiments/data_processing/__init__.py b/qiskit_experiments/data_processing/__init__.py index d10c195ec6..d6866829d4 100644 --- a/qiskit_experiments/data_processing/__init__.py +++ b/qiskit_experiments/data_processing/__init__.py @@ -14,7 +14,7 @@ from .data_action import DataAction from .nodes import ( - Population, + Probability, ToImag, ToReal, ) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index c936d1b4fe..01a45a715c 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -106,22 +106,21 @@ def _process_iq(self, point: Tuple[float, float]) -> float: return point[1] -class Population(DataAction): - """Count data post processing. This returns population.""" +class Probability(DataAction): + """Count data post processing. This returns qubit 1 state probabilities.""" __node_output__ = "populations" def __init__(self): - """Initialize a counts to population data conversion.""" + """Initialize a counts to probability data conversion.""" super().__init__() self._accepted_inputs = ["counts"] def _process(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Args: - data: The data dictionary. This will modify the dict in place, - taking the data under counts and adding the corresponding - populations. + data: The data dictionary,taking the data under counts and + adding the corresponding probabilities. Returns: processed data: A dict with the populations. @@ -129,13 +128,13 @@ def _process(self, data: Dict[str, Any]) -> Dict[str, Any]: counts = data.get("counts") - populations = np.zeros(len(list(counts.keys())[0])) + probabilities = np.zeros(len(list(counts.keys())[0])) shots = 0 for bit_str, count in counts.items(): shots += count for ind, bit in enumerate(bit_str): if bit == "1": - populations[ind] += count + probabilities[ind] += count - return {self.__node_output__: populations / shots} + return {self.__node_output__: probabilities / shots} diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 4303370198..d889c3e464 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -23,7 +23,7 @@ from qiskit_experiments.data_processing.nodes import ( ToReal, ToImag, - Population, + Probability, ) @@ -115,7 +115,7 @@ def test_empty_processor(self): def test_append(self): """Tests that append catches inconsistent data processing chains.""" processor = DataProcessor() - processor.append(Population()) + processor.append(Probability()) with self.assertRaises(DataProcessorError): processor.append(ToReal(1e-3)) @@ -129,7 +129,7 @@ def test_output_key(self): self.assertEqual(processor.output_key(), "memory_real") processor = DataProcessor() - processor.append(Population()) + processor.append(Probability()) self.assertEqual(processor.output_key(), "populations") def test_to_real(self): @@ -261,7 +261,7 @@ def test_populations(self): """Test that counts are properly converted to a population.""" processor = DataProcessor() - processor.append(Population()) + processor.append(Probability()) new_data = processor(self.exp_data_lvl2.data[0]) self.assertEqual(new_data["populations"][1], 0.0) From cefcf73eef591e1d62d996d3496a9af29870fd0d Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 10:09:00 +0200 Subject: [PATCH 26/52] * Metadata in processed_data. --- qiskit_experiments/data_processing/data_action.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 909bcccc7d..3e33fc5170 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -88,7 +88,10 @@ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]: """ self._check_required(data) processed_data = self._process(data) - processed_data["metadata"] = data.get("metadata", {}) + + if "metadata" in data: + processed_data["metadata"] = data["metadata"] + return processed_data def __repr__(self): From a0d09030ab691244f3b1c10d2537fdb7b3695243 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 14:22:50 +0200 Subject: [PATCH 27/52] * Refactored _process(Dict[str, Any]) -> Dict[str, Any] to _process(Any) -> Any. --- .../data_processing/data_action.py | 62 ++----- .../data_processing/data_processor.py | 78 ++++----- qiskit_experiments/data_processing/nodes.py | 153 +++++++++++------- test/data_processing/test_data_processing.py | 124 ++++---------- 4 files changed, 180 insertions(+), 237 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 3e33fc5170..a1d421cffd 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -13,9 +13,7 @@ """Defines the steps that can be used to analyse data.""" from abc import ABCMeta, abstractmethod -from typing import Any, Dict, List - -from qiskit_experiments.data_processing.exceptions import DataProcessorError +from typing import Any class DataAction(metaclass=ABCMeta): @@ -25,78 +23,52 @@ class DataAction(metaclass=ABCMeta): using decorators. """ - # Key under which the node will output the data. - __node_output__ = None - def __init__(self): """Create new data analysis routine.""" self._child = None - self._accepted_inputs = [] - - @property - def node_inputs(self) -> List[str]: - """Returns a list of input data that the node can process.""" - return self._accepted_inputs - - def add_accepted_input(self, data_key: str): - """ - Allows users to add an accepted input data format to this DataAction. - - Args: - data_key: The key that the data action will require in the input data dict. - """ - self._accepted_inputs.append(data_key) @abstractmethod - def _process(self, data: Dict[str, Any]) -> Dict[str, Any]: + def _process(self, datum: Any) -> Any: """ Applies the data processing step to the data. Args: - data: the data to which the data processing step will be applied. + datum: A single item of data data which will be processed. Returns: processed data: The data that has been processed. """ - def _check_required(self, data: Dict[str, Any]): - """Checks that the given data contains the right key. + @abstractmethod + def _check_data_format(self, datum: Any) -> Any: + """ + 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: The data to check for the correct keys. + datum: The data instance to check. + + Returns: + datum: The data that was check. Raises: - DataProcessorError: if the key is not found. + DataProcessorError: if the data does not have the proper format. """ - for key in data.keys(): - if key in self.node_inputs: - return - - raise DataProcessorError(f"None of {self.node_inputs} are in the given data.") - def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]: + def __call__(self, data: Any) -> Any: """ Call the data action of this node on the data. Args: - data: A dict containing the data. The action nodes in the data + data: The data to process. The action nodes in the data processor will raise errors if the data does not contain the appropriate data. Returns: processed data: The output data of the node contained in a dict. """ - self._check_required(data) - processed_data = self._process(data) - - if "metadata" in data: - processed_data["metadata"] = data["metadata"] - - return processed_data + return self._process(self._check_data_format(data)) def __repr__(self): """String representation of the node.""" - return ( - f"{self.__class__.__name__}(inputs: {self.node_inputs}, " - f"outputs: {self.__node_output__})" - ) + return f"{self.__class__.__name__}" diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index a57714fa70..9d42a3c39e 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -12,7 +12,7 @@ """Actions done on the data to bring it in a usable form.""" -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Tuple from qiskit_experiments.data_processing.data_action import DataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -34,13 +34,16 @@ class DataProcessor: the data produced by the last DataAction. """ - def __init__(self, data_actions: List[DataAction] = None): + def __init__(self, input_key: str, data_actions: List[DataAction] = None): """Create a chain of data processing actions. Args: + 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. """ + self._input_key = input_key self._nodes = [] if data_actions: @@ -55,61 +58,58 @@ def append(self, node: DataAction): Args: node: A DataAction that will process the data. - - Raises: - DataProcessorError: if the output of the last node does not match the input required - by the node to be appended. """ - if len(self._nodes) == 0: - self._nodes.append(node) - else: - if self._nodes[-1].__node_output__ not in node.node_inputs: - raise DataProcessorError( - f"Output of node {self._nodes[-1]} is not an acceptable " f"input to {node}." - ) - - self._nodes.append(node) - - def output_key(self) -> Union[str, None]: - """Return the key to look for in the data output by the processor.""" + self._nodes.append(node) - if len(self._nodes) > 0: - return self._nodes[-1].__node_output__ - - return None - - def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]: + def __call__(self, datum: Dict[str, Any]) -> Tuple[Any, Any]: """ - Call self on the given data. This method sequentially calls the stored data actions - on the data. + Call self on the given datum. This method sequentially calls the stored data actions + on the datum. Args: - data: The data, typically from an ExperimentData instance, that needs to - be processed. This dict also contains the metadata of each experiment. + datum: A single item of data, typically from an ExperimentData instance, that needs + to be processed. This dict also contains the metadata of each experiment. Returns: processed data: The data processed by the data processor. + + Raises: + DataProcessorError: if no nodes are present. """ + if len(self._nodes) == 0: + raise DataProcessorError("Cannot call an empty data processor.") + + datum_ = datum[self._input_key] + for node in self._nodes: - data = node(data) + datum_ = node(datum_) - return data + return datum_ - def call_with_history(self, data: Dict[str, Any]) -> Tuple[Dict[str, Any], List]: + def call_with_history(self, datum: Dict[str, Any]) -> Tuple[Dict[str, Any], List]: """ - Process the given data but save each step in a list returned to the user. + Call self on the given datum. This method sequentially calls the stored data actions + on the datum and also saves the history of the processed data. Args: - data: The data, typically from an ExperimentData instance, that needs to - be processed. This dict also contains the metadata of each experiment. + datum: A single item of data, typically from an ExperimentData instance, that + needs to be processed. Returns: - processed data: The data processed by the data processor. - history: The data processed at each node of the data processor. + processed data: The datum processed by the data processor. + history: The datum processed at each node of the data processor. + + Raises: + DataProcessorError: if no nodes are present. """ + if len(self._nodes) == 0: + raise DataProcessorError("Cannot call an empty data processor.") + + datum_ = datum[self._input_key] + history = [] for node in self._nodes: - data = node(data) - history.append((node.__class__.__name__, dict(data))) + datum_ = node(datum_) + history.append((node.__class__.__name__, datum_)) - return data, history + return datum_, history diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 01a45a715c..db7ba1026a 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -17,124 +17,165 @@ import numpy as np from qiskit_experiments.data_processing.data_action import DataAction +from qiskit_experiments.data_processing.exceptions import DataProcessorError class IQPart(DataAction): """Abstract class for IQ data post-processing.""" - def __init__(self, scale: Optional[float] = 1.0, average: bool = False): + def __init__(self, scale: Optional[float] = None): """ Args: - scale: scale by which to multiply the real part of the data. - average: if True the single-shots are averaged. + scale: float with which to multiply the IQ data. """ self.scale = scale - self.average = average super().__init__() - self._accepted_inputs = ["memory"] @abstractmethod - def _process_iq(self, point: Tuple[float, float]) -> float: + def _process_iq(self, datum: np.array) -> np.array: """Defines how the IQ point will be processed. Args: - point: An IQ point as a tuple of two float, i.e. (real, imaginary). + datum: a 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. Returns: Processed IQ point. """ - def _process(self, data: Dict[str, Any]) -> Dict[str, Any]: - """ - Modifies the data inplace by taking the real part of the memory and - scaling it by the given factor. + def _check_data_format(self, datum: Any) -> Any: + """Check that the IQ data has the correct format. Args: - data: The data dict. IQ data is stored under memory. + datum: A single item of data which corresponds to single-shot IQ data. It should + have dimension three: shots, qubits, iq-point as [real, imaginary]. Returns: - processed data: A dict with the data. + datum: as a numpy array. + + Raises: + DataProcessorError: if the datum does not have the correct format. """ + if not isinstance(datum, (list, np.ndarray)): + raise DataProcessorError( + f"The IQ data given to {self.__class__.__name__} " f"must be a list or ndarray." + ) - # Single shot data - if isinstance(data["memory"][0][0], list): - new_mem = [] - for shot in data["memory"]: - new_mem.append([self.scale * self._process_iq(iq_point) for iq_point in shot]) + if isinstance(datum, list): + datum = np.asarray(datum) - if self.average: - new_mem = list(np.mean(np.array(new_mem), axis=0)) + if len(datum.shape) != 3: + raise DataProcessorError( + f"Single-shot data given {self.__class__.__name__}" + f"must be a 3D array. Instead, a {len(datum.shape)}D " + f"array was given." + ) - # Averaged data - else: - new_mem = [self.scale * self._process_iq(iq_point) for iq_point in data["memory"]] + return datum - return {self.__node_output__: new_mem} + def _process(self, datum: np.array) -> np.array: + """Wraps _process_iq. + + Args: + datum: A single item of data. + + Returns: + processed data + """ + return self._process_iq(datum) class ToReal(IQPart): """IQ data post-processing. Isolate the real part of the IQ data.""" - __node_output__ = "memory_real" - - def _process_iq(self, point: Tuple[float, float]) -> float: - """Defines how the IQ point will be processed. + def _process_iq(self, datum: np.array) -> np.array: + """Take the real part of the IQ data. Args: - point: An IQ point as a tuple of two float, i.e. (real, imaginary). + datum: a 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. Returns: - The real part of the IQ point. + A 2D array of shots, qubits. Each entry is the real part of the given IQ data. """ - return point[0] + if self.scale is None: + return datum[:, :, 0] + + return datum[:, :, 0] * self.scale class ToImag(IQPart): """IQ data post-processing. Isolate the imaginary part of the IQ data.""" - __node_output__ = "memory_imag" - - def _process_iq(self, point: Tuple[float, float]) -> float: - """Defines how the IQ point will be processed. + def _process_iq(self, datum: np.array) -> np.array: + """Take the imaginary part of the IQ data. Args: - point: An IQ point as a tuple of two float, i.e. (real, imaginary). + datum: a 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. Returns: - The imaginary part of the IQ point. + A 2D array of shots, qubits. Each entry is the imaginary part of the given IQ data. """ - return point[1] + if self.scale is None: + return datum[:, :, 1] + + return datum[:, :, 1] * self.scale class Probability(DataAction): """Count data post processing. This returns qubit 1 state probabilities.""" - __node_output__ = "populations" + def __init__(self, outcome: str): + """Initialize a counts to probability data conversion. - def __init__(self): - """Initialize a counts to probability data conversion.""" + Args: + outcome: The bitstring for which to compute the probability. + """ super().__init__() - self._accepted_inputs = ["counts"] + self._outcome = outcome + + def _check_data_format(self, datum: dict) -> dict: + """ + 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. - def _process(self, data: Dict[str, Any]) -> Dict[str, Any]: + Returns: + The datum as given. + + Raises: + DataProcessorError: if the data is not a counts dict. + """ + if not isinstance(datum, dict): + raise DataProcessorError( + f"Given counts datum {datum} to " + f"{self.__class__.__name__} is not a valid count format." + ) + + 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 for " f"{self.__class__.__name__}." + ) + + if not isinstance(count, (int, float)): + raise DataProcessorError( + f"Count {bit_str} is not a valid count key for" f"{self.__class__.__name__}." + ) + + return datum + + def _process(self, datum: Dict[str, Any]) -> Tuple[float, float]: """ Args: - data: The data dictionary,taking the data under counts and + datum: The data dictionary,taking the data under counts and adding the corresponding probabilities. Returns: processed data: A dict with the populations. """ + shots = sum(datum.values()) + p_mean = datum.get(self._outcome, 0.0) / shots + p_var = shots * p_mean * (1 - p_mean) - counts = data.get("counts") - - probabilities = np.zeros(len(list(counts.keys())[0])) - - shots = 0 - for bit_str, count in counts.items(): - shots += count - for ind, bit in enumerate(bit_str): - if bit == "1": - probabilities[ind] += count - - return {self.__node_output__: probabilities / shots} + return p_mean, p_var diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index d889c3e464..ec5428228a 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -12,6 +12,8 @@ """Data processor tests.""" +import numpy as np + from qiskit.result.models import ExperimentResultData, ExperimentResult from qiskit.result import Result from qiskit.test import QiskitTestCase @@ -107,36 +109,16 @@ def setUp(self): def test_empty_processor(self): """Check that a DataProcessor without steps does nothing.""" - data_processor = DataProcessor() - data_processor(self.exp_data_lvl2.data) - self.assertEqual(self.exp_data_lvl2.data[0]["counts"]["00"], 4) - self.assertEqual(self.exp_data_lvl2.data[0]["counts"]["10"], 6) - - def test_append(self): - """Tests that append catches inconsistent data processing chains.""" - processor = DataProcessor() - processor.append(Probability()) - + data_processor = DataProcessor("counts") with self.assertRaises(DataProcessorError): - processor.append(ToReal(1e-3)) - - def test_output_key(self): - """Test that we can properly get the output key from the node.""" - processor = DataProcessor() - self.assertIsNone(processor.output_key()) + data_processor(self.exp_data_lvl2.data) - processor.append(ToReal()) - self.assertEqual(processor.output_key(), "memory_real") - - processor = DataProcessor() - processor.append(Probability()) - self.assertEqual(processor.output_key(), "populations") + with self.assertRaises(DataProcessorError): + data_processor.call_with_history(self.exp_data_lvl2.data) def test_to_real(self): """Test scaling and conversion to real part.""" - processor = DataProcessor() - processor.append(ToReal(scale=1e-3)) - self.assertEqual(processor.output_key(), "memory_real") + processor = DataProcessor("memory", [ToReal(scale=1e-3)]) exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) @@ -152,49 +134,24 @@ def test_to_real(self): "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } - expected_new = { - "memory_real": [[1103.26, 2959.012], [442.17, -5279.41], [3016.514, -3404.7560]], - "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, - } + 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.assertEqual(new_data, expected_new) - - # Test that we can average single-shots - processor = DataProcessor([ToReal(scale=1e-3, average=True)]) - self.assertEqual(processor.output_key(), "memory_real") - - exp_data = ExperimentData(FakeExperiment()) - exp_data.add_data(self.result_lvl1) + self.assertTrue(np.allclose(new_data, expected_new)) + # Test that we can call with history. new_data, history = processor.call_with_history(exp_data.data[0]) - expected_old = { - "memory": [ - [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], - [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], - [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], - ], - "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, - } - - expected_new = { - "memory_real": [1520.6480000000001, -1908.3846666666666], - "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, - } - self.assertEqual(exp_data.data[0], expected_old) - self.assertEqual(new_data, expected_new) + self.assertTrue(np.allclose(new_data, expected_new)) - # Check the history self.assertEqual(history[0][0], "ToReal") - self.assertEqual(history[0][1], expected_new) + self.assertTrue(np.allclose(history[0][1], expected_new)) def test_to_imag(self): """Test that we can average the data.""" - processor = DataProcessor() + processor = DataProcessor("memory") processor.append(ToImag(scale=1e-3)) - self.assertEqual(processor.output_key(), "memory_imag") exp_data = ExperimentData(FakeExperiment()) exp_data.add_data(self.result_lvl1) @@ -210,59 +167,32 @@ def test_to_imag(self): "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, } - expected_new = { - "memory_imag": [ + expected_new = np.array( + [ [-11378.508, -16488.753], [-19283.206000000002, -15339.630000000001], [-14548.009, -16743.348], - ], - "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, - } + ] + ) self.assertEqual(exp_data.data[0], expected_old) - self.assertEqual(new_data, expected_new) - - # Test that we can average single-shots - processor = DataProcessor() - processor.append(ToImag(scale=1e-3, average=True)) - self.assertEqual(processor.output_key(), "memory_imag") - - exp_data = ExperimentData(FakeExperiment()) - exp_data.add_data(self.result_lvl1) - - new_data = processor(exp_data.data[0]) - - expected_old = { - "memory": [ - [[1103260.0, -11378508.0], [2959012.0, -16488753.0]], - [[442170.0, -19283206.0], [-5279410.0, -15339630.0]], - [[3016514.0, -14548009.0], [-3404756.0, -16743348.0]], - ], - "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, - } - - expected_new = { - "memory_imag": [-15069.907666666666, -16190.577], - "metadata": {"experiment_type": "fake_test_experiment", "x_values": 0.0}, - } + self.assertTrue(np.allclose(new_data, expected_new)) + # Test that we can call with history. + new_data, history = processor.call_with_history(exp_data.data[0]) self.assertEqual(exp_data.data[0], expected_old) - self.assertEqual(new_data, expected_new) + self.assertTrue(np.allclose(new_data, expected_new)) - # Test the history - new_data, history = processor.call_with_history(exp_data.data[0],) - - self.assertEqual(exp_data.data[0], expected_old) - self.assertEqual(new_data, expected_new) self.assertEqual(history[0][0], "ToImag") - self.assertEqual(history[0][1], expected_new) + self.assertTrue(np.allclose(history[0][1], expected_new)) def test_populations(self): """Test that counts are properly converted to a population.""" - processor = DataProcessor() - processor.append(Probability()) + processor = DataProcessor("counts") + processor.append(Probability("00")) + new_data = processor(self.exp_data_lvl2.data[0]) - self.assertEqual(new_data["populations"][1], 0.0) - self.assertEqual(new_data["populations"][0], 0.6) + self.assertEqual(new_data[0], 0.4) + self.assertEqual(new_data[1], 10 * 0.4 * (1 - 0.4)) From 1bc7c83303cb649963ddab3146c31660603b2265 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 13 Apr 2021 14:37:54 +0200 Subject: [PATCH 28/52] * Added option to specifiy which nodes to include in the history. --- .../data_processing/data_processor.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 9d42a3c39e..94904a3b87 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -12,7 +12,7 @@ """Actions done on the data to bring it in a usable form.""" -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Set, Tuple from qiskit_experiments.data_processing.data_action import DataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -86,7 +86,9 @@ def __call__(self, datum: Dict[str, Any]) -> Tuple[Any, Any]: return datum_ - def call_with_history(self, datum: Dict[str, Any]) -> Tuple[Dict[str, Any], List]: + def call_with_history( + self, datum: Dict[str, Any], history_nodes: Set = None + ) -> Tuple[Dict[str, Any], List]: """ Call self on the given datum. This method sequentially calls the stored data actions on the datum and also saves the history of the processed data. @@ -94,6 +96,8 @@ def call_with_history(self, datum: Dict[str, Any]) -> Tuple[Dict[str, Any], List Args: datum: A single item of data, typically from an ExperimentData instance, that needs to be processed. + history_nodes: The nodes, specified by index in the data processing chain, to + include in the history. Returns: processed data: The datum processed by the data processor. @@ -108,8 +112,10 @@ def call_with_history(self, datum: Dict[str, Any]) -> Tuple[Dict[str, Any], List datum_ = datum[self._input_key] history = [] - for node in self._nodes: + for index, node in enumerate(self._nodes): datum_ = node(datum_) - history.append((node.__class__.__name__, datum_)) + + if history_nodes is None or (history_nodes and index in history_nodes): + history.append((node.__class__.__name__, datum_, index)) return datum_, history From 650d9c6852f315390a80282352e3cb31c58f2545 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 16 Apr 2021 13:28:36 +0200 Subject: [PATCH 29/52] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Christopher J. Wood --- 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 db7ba1026a..654fc0a5ea 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -176,6 +176,6 @@ def _process(self, datum: Dict[str, Any]) -> Tuple[float, float]: """ shots = sum(datum.values()) p_mean = datum.get(self._outcome, 0.0) / shots - p_var = shots * p_mean * (1 - p_mean) + p_var = p_mean * (1 - p_mean) / shots return p_mean, p_var From 078fd07b530676f0bc60f9231f1b57b946371b11 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 16 Apr 2021 13:30:51 +0200 Subject: [PATCH 30/52] * Removed __init__ from DataAction. --- qiskit_experiments/data_processing/data_action.py | 4 ---- qiskit_experiments/data_processing/nodes.py | 2 -- 2 files changed, 6 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index a1d421cffd..95f16783fd 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -23,10 +23,6 @@ class DataAction(metaclass=ABCMeta): using decorators. """ - def __init__(self): - """Create new data analysis routine.""" - self._child = None - @abstractmethod def _process(self, datum: Any) -> Any: """ diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index db7ba1026a..333172a589 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -29,7 +29,6 @@ def __init__(self, scale: Optional[float] = None): scale: float with which to multiply the IQ data. """ self.scale = scale - super().__init__() @abstractmethod def _process_iq(self, datum: np.array) -> np.array: @@ -129,7 +128,6 @@ def __init__(self, outcome: str): Args: outcome: The bitstring for which to compute the probability. """ - super().__init__() self._outcome = outcome def _check_data_format(self, datum: dict) -> dict: From 4899bfa826d75cd874d1a1a4d62d80c37586ac1c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 16 Apr 2021 13:50:25 +0200 Subject: [PATCH 31/52] * Added the option to turn of validation. --- .../data_processing/data_action.py | 14 ++++-- qiskit_experiments/data_processing/nodes.py | 47 +++++++++++-------- test/data_processing/test_data_processing.py | 12 ++++- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 95f16783fd..7fdf8eff9a 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -23,6 +23,13 @@ class DataAction(metaclass=ABCMeta): using decorators. """ + def __init__(self, validate: bool = True): + """ + Args: + validate: If set to False the DataAction will not validate its input. + """ + self._validate = validate + @abstractmethod def _process(self, datum: Any) -> Any: """ @@ -36,13 +43,14 @@ def _process(self, datum: Any) -> Any: """ @abstractmethod - def _check_data_format(self, datum: Any) -> Any: + def _format_data(self, datum: Any, validate: bool = True) -> Any: """ 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. + validate: if True the DataAction checks that the format of the datum is valid. Returns: datum: The data that was check. @@ -63,8 +71,8 @@ def __call__(self, data: Any) -> Any: Returns: processed data: The output data of the node contained in a dict. """ - return self._process(self._check_data_format(data)) + return self._process(self._format_data(data, self._validate)) def __repr__(self): """String representation of the node.""" - return f"{self.__class__.__name__}" + return f"{self.__class__.__name__}(validate: {self._validate})" diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index b69bad9007..8311e9a9af 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -23,12 +23,14 @@ class IQPart(DataAction): """Abstract class for IQ data post-processing.""" - def __init__(self, scale: Optional[float] = None): + def __init__(self, scale: Optional[float] = None, validate: bool = True): """ Args: scale: float with which to multiply the IQ data. + validate: If set to False the DataAction will not validate its input. """ self.scale = scale + super().__init__(validate) @abstractmethod def _process_iq(self, datum: np.array) -> np.array: @@ -41,12 +43,13 @@ def _process_iq(self, datum: np.array) -> np.array: Processed IQ point. """ - def _check_data_format(self, datum: Any) -> Any: - """Check that the IQ data has the correct format. + def _format_data(self, datum: Any, validate: bool = True) -> Any: + """Format to np array and check that the IQ data has the correct format. Args: datum: A single item of data which corresponds to single-shot IQ data. It should have dimension three: shots, qubits, iq-point as [real, imaginary]. + validate: if True the DataAction checks that the format of the datum is valid. Returns: datum: as a numpy array. @@ -54,7 +57,7 @@ def _check_data_format(self, datum: Any) -> Any: Raises: DataProcessorError: if the datum does not have the correct format. """ - if not isinstance(datum, (list, np.ndarray)): + if validate and not isinstance(datum, (list, np.ndarray)): raise DataProcessorError( f"The IQ data given to {self.__class__.__name__} " f"must be a list or ndarray." ) @@ -62,7 +65,7 @@ def _check_data_format(self, datum: Any) -> Any: if isinstance(datum, list): datum = np.asarray(datum) - if len(datum.shape) != 3: + if validate and len(datum.shape) != 3: raise DataProcessorError( f"Single-shot data given {self.__class__.__name__}" f"must be a 3D array. Instead, a {len(datum.shape)}D " @@ -122,21 +125,24 @@ def _process_iq(self, datum: np.array) -> np.array: class Probability(DataAction): """Count data post processing. This returns qubit 1 state probabilities.""" - def __init__(self, outcome: str): + def __init__(self, outcome: str, validate: bool = True): """Initialize a counts to probability data conversion. Args: outcome: The bitstring for which to compute the probability. + validate: If set to False the DataAction will not validate its input. """ self._outcome = outcome + super().__init__(validate) - def _check_data_format(self, datum: dict) -> dict: + def _format_data(self, datum: dict, validate: bool = True) -> dict: """ 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. + validate: if True the DataAction checks that the format of the datum is valid. Returns: The datum as given. @@ -144,22 +150,23 @@ def _check_data_format(self, datum: dict) -> dict: Raises: DataProcessorError: if the data is not a counts dict. """ - if not isinstance(datum, dict): - raise DataProcessorError( - f"Given counts datum {datum} to " - f"{self.__class__.__name__} is not a valid count format." - ) - - for bit_str, count in datum.items(): - if not isinstance(bit_str, str): + if validate: + if not isinstance(datum, dict): raise DataProcessorError( - f"Key {bit_str} is not a valid count key for " f"{self.__class__.__name__}." + f"Given counts datum {datum} to " + f"{self.__class__.__name__} is not a valid count format." ) - if not isinstance(count, (int, float)): - raise DataProcessorError( - f"Count {bit_str} is not a valid count key for" f"{self.__class__.__name__}." - ) + 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__}." + ) + + if not isinstance(count, (int, float)): + raise DataProcessorError( + f"Count {bit_str} is not a valid count value in {self.__class__.__name__}." + ) return datum diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index ec5428228a..9a51c75f02 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -195,4 +195,14 @@ def test_populations(self): new_data = processor(self.exp_data_lvl2.data[0]) self.assertEqual(new_data[0], 0.4) - self.assertEqual(new_data[1], 10 * 0.4 * (1 - 0.4)) + self.assertEqual(new_data[1], 0.4 * (1 - 0.4) / 10) + + def test_validation(self): + """Test the validation mechanism.""" + + for validate, error in [(False, AttributeError), (True, DataProcessorError)]: + processor = DataProcessor("counts") + processor.append(Probability("00", validate=validate)) + + with self.assertRaises(error): + processor({"counts": [0, 1, 2]}) From b06e3eb7217a89e6c03361bb4a971504e84113b3 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 16 Apr 2021 13:52:55 +0200 Subject: [PATCH 32/52] Update qiskit_experiments/data_processing/data_processor.py Co-authored-by: Christopher J. Wood --- qiskit_experiments/data_processing/data_processor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 94904a3b87..23b875f9bc 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -50,7 +50,6 @@ def __init__(self, input_key: str, data_actions: List[DataAction] = None): for node in data_actions: self.append(node) - self._history = [] def append(self, node: DataAction): """ From 597d60c25c8c2a0d0bb740f3571fda528f371e5b Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 16 Apr 2021 13:58:53 +0200 Subject: [PATCH 33/52] * Simplified validation of IQ data. --- qiskit_experiments/data_processing/nodes.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 8311e9a9af..e71349eb22 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -57,13 +57,7 @@ def _format_data(self, datum: Any, validate: bool = True) -> Any: Raises: DataProcessorError: if the datum does not have the correct format. """ - if validate and not isinstance(datum, (list, np.ndarray)): - raise DataProcessorError( - f"The IQ data given to {self.__class__.__name__} " f"must be a list or ndarray." - ) - - if isinstance(datum, list): - datum = np.asarray(datum) + datum = np.asarray(datum, dtype=complex) if validate and len(datum.shape) != 3: raise DataProcessorError( From 055d6cc1762e8501c3b10d6ddc227e40d9cf0bba Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 16 Apr 2021 14:01:44 +0200 Subject: [PATCH 34/52] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Christopher J. Wood --- 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 e71349eb22..42d717cf5a 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -83,7 +83,7 @@ def _process(self, datum: np.array) -> np.array: class ToReal(IQPart): """IQ data post-processing. Isolate the real part of the IQ data.""" - def _process_iq(self, datum: np.array) -> np.array: + def _process(self, datum: np.array) -> np.array: """Take the real part of the IQ data. Args: From a498bf0796b67c4bf8c3a6707e47b6b1ef53db94 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 16 Apr 2021 14:01:53 +0200 Subject: [PATCH 35/52] Update qiskit_experiments/data_processing/nodes.py Co-authored-by: Christopher J. Wood --- 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 42d717cf5a..9f98197014 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -101,7 +101,7 @@ def _process(self, datum: np.array) -> np.array: class ToImag(IQPart): """IQ data post-processing. Isolate the imaginary part of the IQ data.""" - def _process_iq(self, datum: np.array) -> np.array: + def _process(self, datum: np.array) -> np.array: """Take the imaginary part of the IQ data. Args: From a35b522b843cd1181a116bcde872695ab4c98524 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 16 Apr 2021 14:03:24 +0200 Subject: [PATCH 36/52] * Removed unnecessary wrapping of _process. --- qiskit_experiments/data_processing/nodes.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 9f98197014..4bf50f7711 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -33,7 +33,7 @@ def __init__(self, scale: Optional[float] = None, validate: bool = True): super().__init__(validate) @abstractmethod - def _process_iq(self, datum: np.array) -> np.array: + def _process(self, datum: np.array) -> np.array: """Defines how the IQ point will be processed. Args: @@ -68,17 +68,6 @@ def _format_data(self, datum: Any, validate: bool = True) -> Any: return datum - def _process(self, datum: np.array) -> np.array: - """Wraps _process_iq. - - Args: - datum: A single item of data. - - Returns: - processed data - """ - return self._process_iq(datum) - class ToReal(IQPart): """IQ data post-processing. Isolate the real part of the IQ data.""" From 74ec8f86ad90d0e069efc5fb219ef57ce92fda64 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 16 Apr 2021 14:12:09 +0200 Subject: [PATCH 37/52] * Polished docstrings and ran black. --- .../data_processing/data_action.py | 22 +++++++++---------- .../data_processing/data_processor.py | 5 ++--- qiskit_experiments/data_processing/nodes.py | 22 +++++++++++-------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index 7fdf8eff9a..24913d0f55 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -18,9 +18,8 @@ class DataAction(metaclass=ABCMeta): """ - Abstract action which is a single action done on measured data to process it. - Each subclass of DataAction must define the type of data that it accepts as input - using decorators. + 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): @@ -36,7 +35,7 @@ def _process(self, datum: Any) -> Any: Applies the data processing step to the data. Args: - datum: A single item of data data which will be processed. + datum: A single item of data which will be processed. Returns: processed data: The data that has been processed. @@ -49,14 +48,14 @@ def _format_data(self, datum: Any, validate: bool = True) -> Any: additionally change the data type, e.g. converting a list to a numpy array. Args: - datum: The data instance to check. - validate: if True the DataAction checks that the format of the datum is valid. + datum: The data instance to check and format. + validate: If True the DataAction checks that the format of the datum is valid. Returns: - datum: The data that was check. + datum: The data that was checked. Raises: - DataProcessorError: if the data does not have the proper format. + DataProcessorError: If the data does not have the proper format. """ def __call__(self, data: Any) -> Any: @@ -64,12 +63,11 @@ def __call__(self, data: Any) -> Any: 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 contain the - appropriate data. + data: The data to process. The action nodes in the data processor will + raise errors if the data does not have the appropriate format. Returns: - processed data: The output data of the node contained in a dict. + processed data: The data processed by self. """ return self._process(self._format_data(data, self._validate)) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 23b875f9bc..5751790971 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -50,7 +50,6 @@ def __init__(self, input_key: str, data_actions: List[DataAction] = None): for node in data_actions: self.append(node) - def append(self, node: DataAction): """ Append new data action node to this data processor. @@ -90,7 +89,7 @@ def call_with_history( ) -> Tuple[Dict[str, Any], List]: """ Call self on the given datum. This method sequentially calls the stored data actions - on the datum and also saves the history of the processed data. + on the datum and also returns the history of the processed data. Args: datum: A single item of data, typically from an ExperimentData instance, that @@ -103,7 +102,7 @@ def call_with_history( history: The datum processed at each node of the data processor. Raises: - DataProcessorError: if no nodes are present. + DataProcessorError: If no nodes are present. """ if len(self._nodes) == 0: raise DataProcessorError("Cannot call an empty data processor.") diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index 4bf50f7711..759a355edd 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -26,7 +26,7 @@ class IQPart(DataAction): def __init__(self, scale: Optional[float] = None, validate: bool = True): """ Args: - scale: float with which to multiply the IQ data. + scale: Float with which to multiply the IQ data. validate: If set to False the DataAction will not validate its input. """ self.scale = scale @@ -37,25 +37,25 @@ def _process(self, datum: np.array) -> np.array: """Defines how the IQ point will be processed. Args: - datum: a 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. + datum: A 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. Returns: Processed IQ point. """ def _format_data(self, datum: Any, validate: bool = True) -> Any: - """Format to np array and check that the IQ data has the correct format. + """Check that the IQ data has the correct format and convert to numpy array. Args: datum: A single item of data which corresponds to single-shot IQ data. It should have dimension three: shots, qubits, iq-point as [real, imaginary]. - validate: if True the DataAction checks that the format of the datum is valid. + validate: If True the DataAction checks that the format of the datum is valid. Returns: - datum: as a numpy array. + datum as a numpy array. Raises: - DataProcessorError: if the datum does not have the correct format. + DataProcessorError: If the datum does not have the correct format. """ datum = np.asarray(datum, dtype=complex) @@ -68,6 +68,10 @@ def _format_data(self, datum: Any, validate: bool = True) -> Any: return datum + def __repr__(self): + """String representation of the node.""" + return f"{self.__class__.__name__}(validate: {self._validate}, scale: {self.scale})" + class ToReal(IQPart): """IQ data post-processing. Isolate the real part of the IQ data.""" @@ -76,7 +80,7 @@ def _process(self, datum: np.array) -> np.array: """Take the real part of the IQ data. Args: - datum: a 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. + datum: A 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. Returns: A 2D array of shots, qubits. Each entry is the real part of the given IQ data. @@ -94,7 +98,7 @@ def _process(self, datum: np.array) -> np.array: """Take the imaginary part of the IQ data. Args: - datum: a 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. + datum: A 3D array of shots, qubits, and a complex IQ point as [real, imaginary]. Returns: A 2D array of shots, qubits. Each entry is the imaginary part of the given IQ data. @@ -125,7 +129,7 @@ def _format_data(self, datum: dict, validate: bool = True) -> dict: Args: datum: An instance of data the should be a dict with bit strings as keys and counts as values. - validate: if True the DataAction checks that the format of the datum is valid. + validate: If True the DataAction checks that the format of the datum is valid. Returns: The datum as given. From ec911bdb5d8a3e32e317af6102eb6cb16f284d46 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Fri, 16 Apr 2021 17:57:38 +0200 Subject: [PATCH 38/52] Update qiskit_experiments/data_processing/data_action.py Co-authored-by: Will Shanks --- 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 24913d0f55..a5504a2bfd 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -73,4 +73,4 @@ def __call__(self, data: Any) -> Any: def __repr__(self): """String representation of the node.""" - return f"{self.__class__.__name__}(validate: {self._validate})" + return f"{self.__class__.__name__}(validate={self._validate})" From 83ed8ff52b156e816a5eb9bf65f375377cfbafb6 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 16 Apr 2021 17:59:25 +0200 Subject: [PATCH 39/52] * Removed unnecessary code in DataProcessingError. --- qiskit_experiments/data_processing/exceptions.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/qiskit_experiments/data_processing/exceptions.py b/qiskit_experiments/data_processing/exceptions.py index 018d109e11..412a3ace60 100644 --- a/qiskit_experiments/data_processing/exceptions.py +++ b/qiskit_experiments/data_processing/exceptions.py @@ -16,13 +16,4 @@ class DataProcessorError(QiskitError): - """Errors raised by the calibration module.""" - - def __init__(self, *message): - """Set the error message.""" - super().__init__(*message) - self.message = " ".join(message) - - def __str__(self): - """Return the message.""" - return repr(self.message) + """Errors raised by the data processing module.""" From 2ffb0d3969a8bbf8cc9e9c850734c18c70b24b4c Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 16 Apr 2021 18:14:55 +0200 Subject: [PATCH 40/52] * Rewrote doc string. --- .../data_processing/data_processor.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 5751790971..f72df421e0 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -21,17 +21,16 @@ class DataProcessor: """ A DataProcessor defines a sequence of operations to perform on experimental data. - The DataProcessor.format_data() method applies this sequence on its input argument. - A DataProcessor is created with a list of DataAction objects. Each DataAction - specifies a set of node_inputs that it accepts and a __node_output__ that it - provides. The __node_output__ of each DataAction must be contained in the node_inputs - of the following DataAction in the DataProcessor's list. DataProcessor.format_data() - usually takes in one entry from the data property of an ExperimentData object - (i.e. a dict containing metadata and memory keys and possibly counts, like the - Result.data property) and produces a new dict containing the formatted data. The data - passed to DataProcessor.format_data() is passed to the first DataAction and the - output is passed on in turn to each DataAction. DataProcessor.format_data() returns - the data produced by the last DataAction. + Calling an instance of DataProcessor applies this sequence on the input argument. + A DataProcessor is created with a list of DataAction instances. Each DataAction + applies its _process method on the data and returns the processed data. The nodes + in the DataProcessor may also perform data validation and some minor formatting. + The output of one data action serves as input for the next data action. + DataProcessor.__call__(datum) usually takes in an entry from the data property of + an ExperimentData object (i.e. a dict containing metadata and memory keys and + possibly counts, like the Result.data property) and produces the formatted data. + DataProcessor.__call__(datum) extracts the data from the given datum under + DataProcessor._input_key (which is specified at initialization) of the given datum. """ def __init__(self, input_key: str, data_actions: List[DataAction] = None): From 657f17bd4b980f9e27c61731490be7148d98882f Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Fri, 16 Apr 2021 18:15:59 +0200 Subject: [PATCH 41/52] * IQ data is now of type float and not complex. --- 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 759a355edd..f8ce1dc16c 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -57,7 +57,7 @@ def _format_data(self, datum: Any, validate: bool = True) -> Any: Raises: DataProcessorError: If the datum does not have the correct format. """ - datum = np.asarray(datum, dtype=complex) + datum = np.asarray(datum, dtype=float) if validate and len(datum.shape) != 3: raise DataProcessorError( From 58ca872112afb811f252f9e325174ce54514fce5 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 20 Apr 2021 10:55:15 +0200 Subject: [PATCH 42/52] * Fixed validate issue. --- qiskit_experiments/data_processing/data_action.py | 5 ++--- qiskit_experiments/data_processing/nodes.py | 9 ++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/qiskit_experiments/data_processing/data_action.py b/qiskit_experiments/data_processing/data_action.py index a5504a2bfd..57e7d4433d 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -42,14 +42,13 @@ def _process(self, datum: Any) -> Any: """ @abstractmethod - def _format_data(self, datum: Any, validate: bool = True) -> Any: + def _format_data(self, datum: Any) -> Any: """ 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. - validate: If True the DataAction checks that the format of the datum is valid. Returns: datum: The data that was checked. @@ -69,7 +68,7 @@ def __call__(self, data: Any) -> Any: Returns: processed data: The data processed by self. """ - return self._process(self._format_data(data, self._validate)) + return self._process(self._format_data(data)) def __repr__(self): """String representation of the node.""" diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index f8ce1dc16c..b2cac685fe 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -43,13 +43,12 @@ def _process(self, datum: np.array) -> np.array: Processed IQ point. """ - def _format_data(self, datum: Any, validate: bool = True) -> Any: + def _format_data(self, datum: Any) -> Any: """Check that the IQ data has the correct format and convert to numpy array. Args: datum: A single item of data which corresponds to single-shot IQ data. It should have dimension three: shots, qubits, iq-point as [real, imaginary]. - validate: If True the DataAction checks that the format of the datum is valid. Returns: datum as a numpy array. @@ -59,7 +58,7 @@ def _format_data(self, datum: Any, validate: bool = True) -> Any: """ datum = np.asarray(datum, dtype=float) - if validate and len(datum.shape) != 3: + if self._validate and len(datum.shape) != 3: raise DataProcessorError( f"Single-shot data given {self.__class__.__name__}" f"must be a 3D array. Instead, a {len(datum.shape)}D " @@ -122,7 +121,7 @@ def __init__(self, outcome: str, validate: bool = True): self._outcome = outcome super().__init__(validate) - def _format_data(self, datum: dict, validate: bool = True) -> dict: + def _format_data(self, datum: dict) -> dict: """ Checks that the given data has a counts format. @@ -137,7 +136,7 @@ def _format_data(self, datum: dict, validate: bool = True) -> dict: Raises: DataProcessorError: if the data is not a counts dict. """ - if validate: + if self._validate: if not isinstance(datum, dict): raise DataProcessorError( f"Given counts datum {datum} to " From d749d95f94096a90ed43e6b61539df73539d3214 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 20 Apr 2021 10:59:39 +0200 Subject: [PATCH 43/52] * Added error message to __call__ and call_with_history. --- qiskit_experiments/data_processing/data_processor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index f72df421e0..01a1c3297e 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -76,6 +76,11 @@ def __call__(self, datum: Dict[str, Any]) -> Tuple[Any, Any]: if len(self._nodes) == 0: raise DataProcessorError("Cannot call an empty data processor.") + if self._input_key not in datum: + raise DataProcessorError( + f"The input key {self._input_key} was not found in the input datum." + ) + datum_ = datum[self._input_key] for node in self._nodes: @@ -106,6 +111,11 @@ def call_with_history( if len(self._nodes) == 0: raise DataProcessorError("Cannot call an empty data processor.") + if self._input_key not in datum: + raise DataProcessorError( + f"The input key {self._input_key} was not found in the input datum." + ) + datum_ = datum[self._input_key] history = [] From cab93390c695c96b972477f6e5395c1c0b7fb952 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 20 Apr 2021 12:39:54 +0200 Subject: [PATCH 44/52] * Improved docstring. --- 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 01a1c3297e..a3bf89681b 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -99,7 +99,8 @@ def call_with_history( datum: A single item of data, typically from an ExperimentData instance, that needs to be processed. history_nodes: The nodes, specified by index in the data processing chain, to - include in the history. + 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. From 4c9acae4700922ff6ffb1f6c53bea009d5547136 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 20 Apr 2021 12:42:03 +0200 Subject: [PATCH 45/52] * Impoved class docstring. --- qiskit_experiments/data_processing/nodes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/nodes.py b/qiskit_experiments/data_processing/nodes.py index b2cac685fe..5b0cda418b 100644 --- a/qiskit_experiments/data_processing/nodes.py +++ b/qiskit_experiments/data_processing/nodes.py @@ -109,7 +109,8 @@ def _process(self, datum: np.array) -> np.array: class Probability(DataAction): - """Count data post processing. This returns qubit 1 state probabilities.""" + """Count data post processing. This returns the probabilities of the outcome string + used to initialize an instance of Probability.""" def __init__(self, outcome: str, validate: bool = True): """Initialize a counts to probability data conversion. From d9109339403571f10e63b2a555903f8d285ff964 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 20 Apr 2021 12:47:06 +0200 Subject: [PATCH 46/52] * Changed how DataProcessor._nodes are initialized in __init__. --- qiskit_experiments/data_processing/data_processor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index a3bf89681b..85617814dd 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -43,11 +43,7 @@ def __init__(self, input_key: str, data_actions: List[DataAction] = None): If None is given an empty DataProcessor will be created. """ self._input_key = input_key - self._nodes = [] - - if data_actions: - for node in data_actions: - self.append(node) + self._nodes = data_actions if data_actions else [] def append(self, node: DataAction): """ From c250ad8a20677a66bea5fa77a3b2be954e049474 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Tue, 20 Apr 2021 13:45:27 +0200 Subject: [PATCH 47/52] * Changed behavior of empty data processor. --- qiskit_experiments/data_processing/data_processor.py | 4 ---- test/data_processing/test_data_processing.py | 9 +++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 85617814dd..84a6460b3c 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -69,8 +69,6 @@ def __call__(self, datum: Dict[str, Any]) -> Tuple[Any, Any]: Raises: DataProcessorError: if no nodes are present. """ - if len(self._nodes) == 0: - raise DataProcessorError("Cannot call an empty data processor.") if self._input_key not in datum: raise DataProcessorError( @@ -105,8 +103,6 @@ def call_with_history( Raises: DataProcessorError: If no nodes are present. """ - if len(self._nodes) == 0: - raise DataProcessorError("Cannot call an empty data processor.") if self._input_key not in datum: raise DataProcessorError( diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index 9a51c75f02..e12c05f878 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -110,11 +110,12 @@ def setUp(self): def test_empty_processor(self): """Check that a DataProcessor without steps does nothing.""" data_processor = DataProcessor("counts") - with self.assertRaises(DataProcessorError): - data_processor(self.exp_data_lvl2.data) - with self.assertRaises(DataProcessorError): - data_processor.call_with_history(self.exp_data_lvl2.data) + datum = data_processor(self.exp_data_lvl2.data[0]) + self.assertEqual(datum, {'00': 4, '10': 6}) + + datum, history = data_processor.call_with_history(self.exp_data_lvl2.data[0]) + self.assertEqual(datum, {'00': 4, '10': 6}) def test_to_real(self): """Test scaling and conversion to real part.""" From bc00e269b11554bfcabd2956bd117c76f6c92ca3 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 21 Apr 2021 09:30:06 +0200 Subject: [PATCH 48/52] * Refactored call and call_with_history to use the call_internal function. --- .../data_processing/data_processor.py | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 84a6460b3c..5bc88821f0 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -69,18 +69,7 @@ def __call__(self, datum: Dict[str, Any]) -> Tuple[Any, Any]: Raises: DataProcessorError: if no nodes are present. """ - - if self._input_key not in datum: - raise DataProcessorError( - f"The input key {self._input_key} was not found in the input datum." - ) - - datum_ = datum[self._input_key] - - for node in self._nodes: - datum_ = node(datum_) - - return datum_ + return self._call_internal(datum, False) def call_with_history( self, datum: Dict[str, Any], history_nodes: Set = None @@ -103,6 +92,23 @@ def call_with_history( Raises: DataProcessorError: If no nodes are present. """ + return self._call_internal(datum, True, history_nodes) + + def _call_internal(self, datum: Dict[str, Any], with_history: bool, history_nodes=None): + """ + Internal function to process the data with or with storing the history of the computation. + + Args: + datum: A single item of data, typically from an ExperimentData instance, that + needs to be processed. + with_history: if True the history is returned otherwise the history is not stored. + 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: + datum_ and history if with_history is True or datum_ if with_history is False. + """ if self._input_key not in datum: raise DataProcessorError( @@ -115,7 +121,10 @@ def call_with_history( for index, node in enumerate(self._nodes): datum_ = node(datum_) - if history_nodes is None or (history_nodes and index in history_nodes): + if with_history and (history_nodes is None or (history_nodes and index in history_nodes)): history.append((node.__class__.__name__, datum_, index)) - return datum_, history + if with_history: + return datum_, history + else: + return datum_ From 81caca76a310050091c48ef0f574c337096dfc8e Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Wed, 21 Apr 2021 09:55:38 +0200 Subject: [PATCH 49/52] * Fixed, lint, black, and docstrings. --- .../data_processing/data_processor.py | 25 ++++++++++--------- test/data_processing/test_data_processing.py | 5 ++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index 5bc88821f0..e8cdbeba8c 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -12,7 +12,7 @@ """Actions done on the data to bring it in a usable form.""" -from typing import Any, Dict, List, Set, Tuple +from typing import Any, Dict, List, Set, Tuple, Union from qiskit_experiments.data_processing.data_action import DataAction from qiskit_experiments.data_processing.exceptions import DataProcessorError @@ -54,7 +54,7 @@ def append(self, node: DataAction): """ self._nodes.append(node) - def __call__(self, datum: Dict[str, Any]) -> Tuple[Any, Any]: + def __call__(self, datum: Dict[str, Any]) -> Any: """ Call self on the given datum. This method sequentially calls the stored data actions on the datum. @@ -65,15 +65,12 @@ def __call__(self, datum: Dict[str, Any]) -> Tuple[Any, Any]: Returns: processed data: The data processed by the data processor. - - Raises: - DataProcessorError: if no nodes are present. """ return self._call_internal(datum, False) def call_with_history( self, datum: Dict[str, Any], history_nodes: Set = None - ) -> Tuple[Dict[str, Any], List]: + ) -> Tuple[Any, List]: """ Call self on the given datum. This method sequentially calls the stored data actions on the datum and also returns the history of the processed data. @@ -88,26 +85,28 @@ def call_with_history( Returns: processed data: The datum processed by the data processor. history: The datum processed at each node of the data processor. - - Raises: - DataProcessorError: If no nodes are present. """ return self._call_internal(datum, True, history_nodes) - def _call_internal(self, datum: Dict[str, Any], with_history: bool, history_nodes=None): + def _call_internal( + self, datum: Dict[str, Any], with_history: bool, history_nodes=None + ) -> Union[Any, Tuple[Any, List]]: """ Internal function to process the data with or with storing the history of the computation. Args: datum: A single item of data, typically from an ExperimentData instance, that needs to be processed. - with_history: if True the history is returned otherwise the history is not stored. + with_history: if True the history is returned otherwise it is not. 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: datum_ and history if with_history is True or datum_ if with_history is False. + + Raises: + DataProcessorError: If the input key of the data processor is not contained in datum. """ if self._input_key not in datum: @@ -121,7 +120,9 @@ def _call_internal(self, datum: Dict[str, Any], with_history: bool, history_node for index, node in enumerate(self._nodes): datum_ = node(datum_) - if with_history and (history_nodes is None or (history_nodes and index in history_nodes)): + if with_history and ( + history_nodes is None or (history_nodes and index in history_nodes) + ): history.append((node.__class__.__name__, datum_, index)) if with_history: diff --git a/test/data_processing/test_data_processing.py b/test/data_processing/test_data_processing.py index e12c05f878..735a3a5298 100644 --- a/test/data_processing/test_data_processing.py +++ b/test/data_processing/test_data_processing.py @@ -112,10 +112,11 @@ def test_empty_processor(self): data_processor = DataProcessor("counts") datum = data_processor(self.exp_data_lvl2.data[0]) - self.assertEqual(datum, {'00': 4, '10': 6}) + self.assertEqual(datum, {"00": 4, "10": 6}) datum, 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): """Test scaling and conversion to real part.""" From 2452a8629d00f3c5288dc0bacedff9fc8a7a91a2 Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Thu, 22 Apr 2021 13:23:43 +0200 Subject: [PATCH 50/52] Update qiskit_experiments/data_processing/data_action.py --- 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 57e7d4433d..ff08ff9f2c 100644 --- a/qiskit_experiments/data_processing/data_action.py +++ b/qiskit_experiments/data_processing/data_action.py @@ -32,7 +32,7 @@ def __init__(self, validate: bool = True): @abstractmethod def _process(self, datum: Any) -> Any: """ - Applies the data processing step to the data. + Applies the data processing step to the datum. Args: datum: A single item of data which will be processed. From a79d270aecd53207a8a24eab664a5cacc05d8d42 Mon Sep 17 00:00:00 2001 From: Daniel Egger Date: Thu, 22 Apr 2021 22:11:33 +0200 Subject: [PATCH 51/52] * Added type hint to call_with_history --- qiskit_experiments/data_processing/data_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit_experiments/data_processing/data_processor.py b/qiskit_experiments/data_processing/data_processor.py index e8cdbeba8c..374751c36b 100644 --- a/qiskit_experiments/data_processing/data_processor.py +++ b/qiskit_experiments/data_processing/data_processor.py @@ -89,7 +89,7 @@ def call_with_history( return self._call_internal(datum, True, history_nodes) def _call_internal( - self, datum: Dict[str, Any], with_history: bool, history_nodes=None + self, datum: Dict[str, Any], with_history: bool, history_nodes: Set = None ) -> Union[Any, Tuple[Any, List]]: """ Internal function to process the data with or with storing the history of the computation. From 5942edeb1570dba3ad7913826f85c5239768ae1a Mon Sep 17 00:00:00 2001 From: Daniel Egger <38065505+eggerdj@users.noreply.github.com> Date: Thu, 22 Apr 2021 22:38:17 +0200 Subject: [PATCH 52/52] Update qiskit_experiments/data_processing/data_processor.py