Repository: Qiskit/qiskit

Issue:
Add data extraction methods for pulse to the result object
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?
With the coming of pulse, measurement results can be returned in multiple formats. The schemas we're updated in #2085 to support this. In addition result extraction code is required for the cases below

| `meas_level` | `meas_return`  | external format                                                                                           | internal format                                                                                                                        | implemented |
|--------------|----------------|-----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|-------------|
| 0            | `single`       | Nested `List` of shape [shots, memory_slots, memory_slot_size, 2]                                         | `np.array` of dtype `np.dtype.complex32`  with shape [shots, memory_slots, memory_slot_size]                                           |     [ ]     |
| 0            | `avg`          | Nested `List` of shape  [memory_slots, memory_slot_size, 2]                                               | `np.array` of dtype `np.dtype.complex32`  with shape [memory_slots, memory_slot_size]                                                  |     [ ]     |
| 1            | `single`       | Nested `List` of shape [shots, memory_slots, 2]                                                           | `np.array` of dtype `np.dtype.complex32`  with shape [shots, memory_slots]                                                             |     [ ]     |
| 1            | `avg`          | Nested `List` of shape [memory_slots, 2]                                                                  | `np.array` of dtype `np.dtype.complex32` with shape [memory_slots]                                                                     |     [ ]     |
| 2            | `memory=True`  | `List` of length `shots` where each element is the little-endian hex measurement for the respective shot. | `List` of length `shots` where each element is the register assigned little-endian bitstring measurement  for the respective shot. |     [x]     |
| 2            | `memory=False` | Dictionary of binned counts  identified by string hex of measurement result.                              | Dictionary of binned counts identified by register assigned  bitstring of measurement result.                                          |     [x]     |



Discussion:

Resolving patch:
diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py
--- a/qiskit/pulse/__init__.py
+++ b/qiskit/pulse/__init__.py
@@ -6,6 +6,7 @@
 # the LICENSE.txt file in the root directory of this source tree.
 
 """Module for Pulses."""
+from qiskit.pulse.schedule import Schedule
 from qiskit.pulse.channels import DeviceSpecification
 from qiskit.pulse.commands import (Acquire, FrameChange, PersistentValue,
                                    SamplePulse, Snapshot,
diff --git a/qiskit/result/postprocess.py b/qiskit/result/postprocess.py
--- a/qiskit/result/postprocess.py
+++ b/qiskit/result/postprocess.py
@@ -9,6 +9,8 @@
 
 import numpy as np
 
+from qiskit.exceptions import QiskitError
+
 
 def _hex_to_bin(hexstring):
     """Convert hexadecimal readouts (memory) to binary readouts."""
@@ -31,7 +33,7 @@ def _separate_bitstring(bitstring, creg_sizes):
     return ' '.join(substrings)
 
 
-def format_memory(memory, header):
+def format_counts_memory(shot_memory, header=None):
     """
     Format a single bitstring (memory) from a single shot experiment.
 
@@ -40,25 +42,107 @@ def format_memory(memory, header):
     - Spaces are inserted at register divisions.
 
     Args:
-        memory (str): result of a single experiment.
+        shot_memory (str): result of a single experiment.
         header (dict): the experiment header dictionary containing
-            useful information for postprocessing.
+            useful information for postprocessing. creg_sizes
+            are a nested list where the inner element is a list
+            of creg name, creg size pairs. memory_slots is an integers
+            specifying the number of total memory_slots in the experiment.
 
     Returns:
         dict: a formatted memory
     """
-    creg_sizes = header.get('creg_sizes')
-    memory_slots = header.get('memory_slots')
-    if memory.startswith('0x'):
-        memory = _hex_to_bin(memory)
-    if memory_slots:
-        memory = _pad_zeros(memory, memory_slots)
-    if creg_sizes:
-        memory = _separate_bitstring(memory, creg_sizes)
-    return memory
-
-
-def format_counts(counts, header):
+    if shot_memory.startswith('0x'):
+        shot_memory = _hex_to_bin(shot_memory)
+    if header:
+        creg_sizes = header.get('creg_sizes', None)
+        memory_slots = header.get('memory_slots', None)
+        if memory_slots:
+            shot_memory = _pad_zeros(shot_memory, memory_slots)
+        if creg_sizes and memory_slots:
+            shot_memory = _separate_bitstring(shot_memory, creg_sizes)
+    return shot_memory
+
+
+def _list_to_complex_array(complex_list):
+    """Convert nested list of shape (..., 2) to complex numpy array with shape (...)
+
+    Args:
+        complex_list (list): List to convert.
+
+    Returns:
+        np.ndarray: Complex numpy aray
+
+    Raises:
+        QiskitError: If inner most array of input nested list is not of length 2.
+    """
+    arr = np.asarray(complex_list, dtype=np.complex_)
+    if not arr.shape[-1] == 2:
+        raise QiskitError('Inner most nested list is not of length 2.')
+
+    return arr[..., 0] + 1j*arr[..., 1]
+
+
+def format_level_0_memory(memory):
+    """ Format an experiment result memory object for measurement level 0.
+
+    Args:
+        memory (list): Memory from experiment with `meas_level==1`. `avg` or
+            `single` will be inferred from shape of result memory.
+
+    Returns:
+        np.ndarray: Measurement level 0 complex numpy array
+
+    Raises:
+        QiskitError: If the returned numpy array does not have 2 (avg) or 3 (single)
+            indicies.
+    """
+    formatted_memory = _list_to_complex_array(memory)
+    # infer meas_return from shape of returned data.
+    if not 2 <= len(formatted_memory.shape) <= 3:
+        raise QiskitError('Level zero memory is not of correct shape.')
+    return formatted_memory
+
+
+def format_level_1_memory(memory):
+    """ Format an experiment result memory object for measurement level 1.
+
+    Args:
+        memory (list): Memory from experiment with `meas_level==1`. `avg` or
+            `single` will be inferred from shape of result memory.
+
+    Returns:
+        np.ndarray: Measurement level 1 complex numpy array
+
+    Raises:
+        QiskitError: If the returned numpy array does not have 1 (avg) or 2 (single)
+            indicies.
+    """
+    formatted_memory = _list_to_complex_array(memory)
+    # infer meas_return from shape of returned data.
+    if not 1 <= len(formatted_memory.shape) <= 2:
+        raise QiskitError('Level one memory is not of correct shape.')
+    return formatted_memory
+
+
+def format_level_2_memory(memory, header=None):
+    """ Format an experiment result memory object for measurement level 2.
+
+    Args:
+        memory (list): Memory from experiment with `meas_level==2` and `memory==True`.
+        header (dict): the experiment header dictionary containing
+            useful information for postprocessing.
+
+    Returns:
+        list[str]: List of bitstrings
+    """
+    memory_list = []
+    for shot_memory in memory:
+        memory_list.append(format_counts_memory(shot_memory, header))
+    return memory_list
+
+
+def format_counts(counts, header=None):
     """Format a single experiment result coming from backend to present
     to the Qiskit user.
 
@@ -72,7 +156,7 @@ def format_counts(counts, header):
     """
     counts_dict = {}
     for key, val in counts.items():
-        key = format_memory(key, header)
+        key = format_counts_memory(key, header)
         counts_dict[key] = val
     return counts_dict
 
diff --git a/qiskit/result/result.py b/qiskit/result/result.py
--- a/qiskit/result/result.py
+++ b/qiskit/result/result.py
@@ -8,11 +8,11 @@
 """Model for schema-conformant Results."""
 
 from qiskit.circuit.quantumcircuit import QuantumCircuit
+from qiskit.pulse.schedule import Schedule
 from qiskit.exceptions import QiskitError
 
 from qiskit.validation.base import BaseModel, bind_schema
-from .postprocess import (format_counts, format_statevector,
-                          format_unitary, format_memory)
+from qiskit.result import postprocess
 from .models import ResultSchema
 
 
@@ -45,7 +45,7 @@ def __init__(self, backend_name, backend_version, qobj_id, job_id, success,
 
         super().__init__(**kwargs)
 
-    def data(self, circuit=None):
+    def data(self, experiment=None):
         """Get the raw data for an experiment.
 
         Note this data will be a single classical and quantum register and in a
@@ -53,16 +53,18 @@ def data(self, circuit=None):
         the get_xxx method, and the data will be post-processed for the data type.
 
         Args:
-            circuit (str or QuantumCircuit or int or None): the index of the
+            experiment (str or QuantumCircuit or Schedule or int or None): the index of the
                 experiment. Several types are accepted for convenience::
                 * str: the name of the experiment.
-                * QuantumCircuit: the name of the instance will be used.
+                * QuantumCircuit: the name of the circuit instance will be used.
+                * Schedule: the name of the schedule instance will be used.
                 * int: the position of the experiment.
                 * None: if there is only one experiment, returns it.
 
         Returns:
             dict: A dictionary of results data for an experiment. The data
-            depends on the backend it ran on.
+            depends on the backend it ran on and the settings of `meas_level`,
+            `meas_return` and `memory`.
 
             QASM backends return a dictionary of dictionary with the key
             'counts' and  with the counts, with the second dictionary keys
@@ -92,40 +94,63 @@ def data(self, circuit=None):
             QiskitError: if data for the experiment could not be retrieved.
         """
         try:
-            return self._get_experiment(circuit).data.to_dict()
+            return self._get_experiment(experiment).data.to_dict()
         except (KeyError, TypeError):
-            raise QiskitError('No data for circuit "{0}"'.format(circuit))
+            raise QiskitError('No data for experiment "{0}"'.format(experiment))
 
-    def get_memory(self, circuit=None):
+    def get_memory(self, experiment=None):
         """Get the sequence of memory states (readouts) for each shot
         The data from the experiment is a list of format
         ['00000', '01000', '10100', '10100', '11101', '11100', '00101', ..., '01010']
 
         Args:
-            circuit (str or QuantumCircuit or int or None): the index of the
+            experiment (str or QuantumCircuit or Schedule or int or None): the index of the
                 experiment, as specified by ``data()``.
 
         Returns:
-            List[str]: the list of each outcome, formatted according to
-                registers in circuit.
+            List[str] or np.ndarray: Either the list of each outcome, formatted according to
+                registers in circuit or a complex numpy np.darray with shape:
+
+                | `meas_level` | `meas_return` | shape                                             |
+                |--------------|---------------|---------------------------------------------------|
+                | 0            | `single`      | np.ndarray[shots, memory_slots, memory_slot_size] |
+                | 0            | `avg`         | np.ndarray[memory_slots, memory_slot_size]        |
+                | 1            | `single`      | np.ndarray[shots, memory_slots]                   |
+                | 1            | `avg`         | np.ndarray[memory_slots]                          |
+                | 2            | `memory=True` | list                                              |
 
         Raises:
             QiskitError: if there is no memory data for the circuit.
         """
         try:
-            header = self._get_experiment(circuit).header.to_dict()
-            memory_list = []
-            for memory in self.data(circuit)['memory']:
-                memory_list.append(format_memory(memory, header))
-            return memory_list
+            exp_result = self._get_experiment(experiment)
+
+            try:  # header is not available
+                header = exp_result.header.to_dict()
+            except (AttributeError, QiskitError):
+                header = None
+
+            meas_level = exp_result.meas_level
+
+            memory = self.data(experiment)['memory']
+
+            if meas_level == 2:
+                return postprocess.format_level_2_memory(memory, header)
+            elif meas_level == 1:
+                return postprocess.format_level_1_memory(memory)
+            elif meas_level == 0:
+                return postprocess.format_level_0_memory(memory)
+            else:
+                raise QiskitError('Measurement level {0} is not supported'.format(meas_level))
+
         except KeyError:
-            raise QiskitError('No memory for circuit "{0}".'.format(circuit))
+            raise QiskitError('No memory for experiment "{0}".'.format(experiment))
 
-    def get_counts(self, circuit=None):
+    def get_counts(self, experiment=None):
         """Get the histogram data of an experiment.
 
         Args:
-            circuit (str or QuantumCircuit or int or None): the index of the
+            experiment (str or QuantumCircuit or Schedule or int or None): the index of the
                 experiment, as specified by ``get_data()``.
 
         Returns:
@@ -138,16 +163,22 @@ def get_counts(self, circuit=None):
             QiskitError: if there are no counts for the experiment.
         """
         try:
-            return format_counts(self.data(circuit)['counts'],
-                                 self._get_experiment(circuit).header.to_dict())
+            exp = self._get_experiment(experiment)
+            try:
+                header = exp.header.to_dict()
+            except (AttributeError, QiskitError):  # header is not available
+                header = None
+
+            return postprocess.format_counts(self.data(experiment)['counts'],
+                                             header)
         except KeyError:
-            raise QiskitError('No counts for circuit "{0}"'.format(circuit))
+            raise QiskitError('No counts for experiment "{0}"'.format(experiment))
 
-    def get_statevector(self, circuit=None, decimals=None):
+    def get_statevector(self, experiment=None, decimals=None):
         """Get the final statevector of an experiment.
 
         Args:
-            circuit (str or QuantumCircuit or int or None): the index of the
+            experiment (str or QuantumCircuit or Schedule or int or None): the index of the
                 experiment, as specified by ``data()``.
             decimals (int): the number of decimals in the statevector.
                 If None, does not round.
@@ -159,16 +190,16 @@ def get_statevector(self, circuit=None, decimals=None):
             QiskitError: if there is no statevector for the experiment.
         """
         try:
-            return format_statevector(self.data(circuit)['statevector'],
-                                      decimals=decimals)
+            return postprocess.format_statevector(self.data(experiment)['statevector'],
+                                                  decimals=decimals)
         except KeyError:
-            raise QiskitError('No statevector for circuit "{0}"'.format(circuit))
+            raise QiskitError('No statevector for experiment "{0}"'.format(experiment))
 
-    def get_unitary(self, circuit=None, decimals=None):
+    def get_unitary(self, experiment=None, decimals=None):
         """Get the final unitary of an experiment.
 
         Args:
-            circuit (str or QuantumCircuit or int or None): the index of the
+            experiment (str or QuantumCircuit or Schedule or int or None): the index of the
                 experiment, as specified by ``data()``.
             decimals (int): the number of decimals in the unitary.
                 If None, does not round.
@@ -181,23 +212,23 @@ def get_unitary(self, circuit=None, decimals=None):
             QiskitError: if there is no unitary for the experiment.
         """
         try:
-            return format_unitary(self.data(circuit)['unitary'],
-                                  decimals=decimals)
+            return postprocess.format_unitary(self.data(experiment)['unitary'],
+                                              decimals=decimals)
         except KeyError:
-            raise QiskitError('No unitary for circuit "{0}"'.format(circuit))
+            raise QiskitError('No unitary for experiment "{0}"'.format(experiment))
 
     def _get_experiment(self, key=None):
         """Return a single experiment result from a given key.
 
         Args:
-            key (str or QuantumCircuit or int or None): the index of the
+            key (str or QuantumCircuit or Schedule or int or None): the index of the
                 experiment, as specified by ``get_data()``.
 
         Returns:
             ExperimentResult: the results for an experiment.
 
         Raises:
-            QiskitError: if there is no data for the circuit, or an unhandled
+            QiskitError: if there is no data for the experiment, or an unhandled
                 error occurred while fetching the data.
         """
         if not self.success:
@@ -208,17 +239,17 @@ def _get_experiment(self, key=None):
         if key is None:
             if len(self.results) != 1:
                 raise QiskitError(
-                    'You have to select a circuit when there is more than '
+                    'You have to select a circuit or schedule when there is more than '
                     'one available')
-            else:
-                key = 0
+
+            key = 0
 
         # Key is an integer: return result by index.
         if isinstance(key, int):
             return self.results[key]
 
-        # Key is a QuantumCircuit or str: retrieve result by name.
-        if isinstance(key, QuantumCircuit):
+        # Key is a QuantumCircuit/Schedule or str: retrieve result by name.
+        if isinstance(key, (QuantumCircuit, Schedule)):
             key = key.name
 
         try:


Test patch:
diff --git a/test/python/result/__init__.py b/test/python/result/__init__.py
new file mode 100644
--- /dev/null
+++ b/test/python/result/__init__.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2019, IBM.
+#
+# This source code is licensed under the Apache License, Version 2.0 found in
+# the LICENSE.txt file in the root directory of this source tree.
+
+"""Qiskit result integration tests."""
diff --git a/test/python/result/test_result.py b/test/python/result/test_result.py
new file mode 100644
--- /dev/null
+++ b/test/python/result/test_result.py
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2019, IBM.
+#
+# This source code is licensed under the Apache License, Version 2.0 found in
+# the LICENSE.txt file in the root directory of this source tree.
+
+
+"""Test Qiskit's Result class."""
+
+import numpy as np
+
+from qiskit.result import models
+from qiskit.validation import base
+from qiskit.result import Result
+from qiskit.test import QiskitTestCase
+
+
+class TestResultOperations(QiskitTestCase):
+    """Result operations methods."""
+
+    def setUp(self):
+        self.base_result_args = dict(backend_name='test_backend',
+                                     backend_version='1.0.0',
+                                     qobj_id='id-123',
+                                     job_id='job-123',
+                                     success=True)
+
+        super().setUp()
+
+    def test_counts_no_header(self):
+        """Test that counts are extracted properly without header."""
+        raw_counts = {'0x0': 4, '0x2': 10}
+        no_header_processed_counts = {bin(int(bs[2:], 16))[2:]: counts for
+                                      (bs, counts) in raw_counts.items()}
+        data = models.ExperimentResultData(counts=base.Obj(**raw_counts))
+        exp_result = models.ExperimentResult(shots=14, success=True, meas_level=2, data=data)
+        result = Result(results=[exp_result], **self.base_result_args)
+
+        self.assertEqual(result.get_counts(0), no_header_processed_counts)
+
+    def test_counts_header(self):
+        """Test that counts are extracted properly with header."""
+        raw_counts = {'0x0': 4, '0x2': 10}
+        processed_counts = {'0 0 00': 4, '0 0 10': 10}
+        data = models.ExperimentResultData(counts=base.Obj(**raw_counts))
+        exp_result_header = base.Obj(creg_sizes=[['c0', 2], ['c0', 1], ['c1', 1]],
+                                     memory_slots=4)
+        exp_result = models.ExperimentResult(shots=14, success=True, meas_level=2,
+                                             data=data, header=exp_result_header)
+        result = Result(results=[exp_result], **self.base_result_args)
+
+        self.assertEqual(result.get_counts(0), processed_counts)
+
+    def test_memory_counts_no_header(self):
+        """Test that memory bitstrings are extracted properly without header."""
+        raw_memory = ['0x0', '0x0', '0x2', '0x2', '0x2', '0x2', '0x2']
+        no_header_processed_memory = [bin(int(bs[2:], 16))[2:] for bs in raw_memory]
+        data = models.ExperimentResultData(memory=raw_memory)
+        exp_result = models.ExperimentResult(shots=14, success=True, meas_level=2,
+                                             memory=True, data=data)
+        result = Result(results=[exp_result], **self.base_result_args)
+
+        self.assertEqual(result.get_memory(0), no_header_processed_memory)
+
+    def test_memory_counts_header(self):
+        """Test that memory bitstrings are extracted properly with header."""
+        raw_memory = ['0x0', '0x0', '0x2', '0x2', '0x2', '0x2', '0x2']
+        no_header_processed_memory = ['0 0 00', '0 0 00', '0 0 10', '0 0 10',
+                                      '0 0 10', '0 0 10', '0 0 10']
+        data = models.ExperimentResultData(memory=raw_memory)
+        exp_result_header = base.Obj(creg_sizes=[['c0', 2], ['c0', 1], ['c1', 1]],
+                                     memory_slots=4)
+        exp_result = models.ExperimentResult(shots=14, success=True, meas_level=2,
+                                             memory=True, data=data,
+                                             header=exp_result_header)
+        result = Result(results=[exp_result], **self.base_result_args)
+
+        self.assertEqual(result.get_memory(0), no_header_processed_memory)
+
+    def test_meas_level_1_avg(self):
+        """Test measurment level 1 average result."""
+        # 3 qubits
+        raw_memory = [[0., 1.], [1., 0.], [0.5, 0.5]]
+        processed_memory = np.array([1.j, 1., 0.5+0.5j], dtype=np.complex_)
+        data = models.ExperimentResultData(memory=raw_memory)
+        exp_result = models.ExperimentResult(shots=2, success=True, meas_level=1,
+                                             meas_return='avg', data=data)
+        result = Result(results=[exp_result], **self.base_result_args)
+        memory = result.get_memory(0)
+
+        self.assertEqual(memory.shape, (3,))
+        self.assertEqual(memory.dtype, np.complex_)
+        np.testing.assert_almost_equal(memory, processed_memory)
+
+    def test_meas_level_1_single(self):
+        """Test measurment level 1 single result."""
+        # 3 qubits
+        raw_memory = [[[0., 1.], [1., 0.], [0.5, 0.5]],
+                      [[0.5, 0.5], [1., 0.], [0., 1.]]]
+        processed_memory = np.array([[1.j, 1., 0.5+0.5j],
+                                     [0.5+0.5j, 1., 1.j]], dtype=np.complex_)
+        data = models.ExperimentResultData(memory=raw_memory)
+        exp_result = models.ExperimentResult(shots=2, success=True, meas_level=1,
+                                             meas_return='single', data=data)
+        result = Result(results=[exp_result], **self.base_result_args)
+        memory = result.get_memory(0)
+
+        self.assertEqual(memory.shape, (2, 3))
+        self.assertEqual(memory.dtype, np.complex_)
+        np.testing.assert_almost_equal(memory, processed_memory)
+
+    def test_meas_level_0_avg(self):
+        """Test measurment level 0 average result."""
+        # 3 qubits
+        raw_memory = [[[0., 1.], [0., 1.], [0., 1.]],
+                      [[1., 0.], [1., 0.], [1., 0.]]]
+        processed_memory = np.array([[1.j, 1.j, 1.j],
+                                     [1., 1., 1.]], dtype=np.complex_)
+        data = models.ExperimentResultData(memory=raw_memory)
+        exp_result = models.ExperimentResult(shots=2, success=True, meas_level=0,
+                                             meas_return='avg', data=data)
+        result = Result(results=[exp_result], **self.base_result_args)
+        memory = result.get_memory(0)
+
+        self.assertEqual(memory.shape, (2, 3))
+        self.assertEqual(memory.dtype, np.complex_)
+        np.testing.assert_almost_equal(memory, processed_memory)
+
+    def test_meas_level_0_single(self):
+        """Test measurment level 0 single result."""
+        # 3 qubits
+        raw_memory = [[[[0., 1.], [0., 1.], [0., 1.]],
+                       [[1., 0.], [1., 0.], [1., 0.]]],
+                      [[[0., 1.], [0., 1.], [0., 1.]],
+                       [[1., 0.], [1., 0.], [1., 0.]]]]
+        processed_memory = np.array([[[1.j, 1.j, 1.j],
+                                      [1., 1., 1.]],
+                                     [[1.j, 1.j, 1.j],
+                                      [1., 1., 1.]]], dtype=np.complex_)
+        data = models.ExperimentResultData(memory=raw_memory)
+        exp_result = models.ExperimentResult(shots=2, success=True, meas_level=0,
+                                             meas_return='single', data=data)
+        result = Result(results=[exp_result], **self.base_result_args)
+        memory = result.get_memory(0)
+
+        self.assertEqual(memory.shape, (2, 2, 3))
+        self.assertEqual(memory.dtype, np.complex_)
+        np.testing.assert_almost_equal(memory, processed_memory)