Repository: rigetti/qcs-sdk-rust (language: python)

Issue:
QPUResultData should include the `memory_values` returned from a job execution result.
A `JobExecutionResult` contains a `memory_values` field that contains the final state of each memory region at the end of execution. We should include this field in `QPUResultData`.


Discussion / hints:

Fix patch:
diff --git a/crates/lib/src/execution_data.rs b/crates/lib/src/execution_data.rs
index aae083d17..ce7d1713f 100644
--- a/crates/lib/src/execution_data.rs
+++ b/crates/lib/src/execution_data.rs
@@ -352,12 +352,16 @@ mod describe_register_map {
     use maplit::hashmap;
     use ndarray::prelude::*;
 
+    use crate::qpu::result_data::MemoryValues;
     use crate::qpu::QpuResultData;
     use crate::qvm::QvmResultData;
 
     use super::{RegisterData, RegisterMap};
     use qcs_api_client_grpc::models::controller::readout_values::Values;
-    use qcs_api_client_grpc::models::controller::{IntegerReadoutValues, ReadoutValues};
+    use qcs_api_client_grpc::models::controller::{
+        self, BinaryDataValue, DataValue as ControllerMemoryValue, IntegerDataValue,
+        IntegerReadoutValues, ReadoutValues, RealDataValue,
+    };
 
     fn dummy_readout_values(v: Vec<i32>) -> ReadoutValues {
         ReadoutValues {
@@ -365,6 +369,34 @@ mod describe_register_map {
         }
     }
 
+    fn dummy_memory_values(
+        binary: Vec<u8>,
+        int: Vec<i64>,
+        real: Vec<f64>,
+    ) -> (
+        ControllerMemoryValue,
+        ControllerMemoryValue,
+        ControllerMemoryValue,
+    ) {
+        (
+            ControllerMemoryValue {
+                value: Some(controller::data_value::Value::Binary(BinaryDataValue {
+                    data: binary,
+                })),
+            },
+            ControllerMemoryValue {
+                value: Some(controller::data_value::Value::Integer(IntegerDataValue {
+                    data: int,
+                })),
+            },
+            ControllerMemoryValue {
+                value: Some(controller::data_value::Value::Real(RealDataValue {
+                    data: real,
+                })),
+            },
+        )
+    }
+
     #[test]
     fn it_converts_rectangular_qpu_result_data_to_register_map() {
         let readout_mappings = hashmap! {
@@ -383,8 +415,19 @@ mod describe_register_map {
             String::from("qE") => dummy_readout_values(vec![2, 3]),
         };
 
-        let qpu_result_data =
-            QpuResultData::from_controller_mappings_and_values(&readout_mappings, &readout_values);
+        let (binary_values, integer_values, real_values) =
+            dummy_memory_values(vec![0, 1, 2], vec![3, 4, 5], vec![6.0, 7.0, 8.0]);
+        let memory_values = hashmap! {
+            String::from("binary") => binary_values,
+            String::from("int") => integer_values,
+            String::from("real") => real_values,
+        };
+
+        let qpu_result_data = QpuResultData::from_controller_mappings_and_values(
+            &readout_mappings,
+            &readout_values,
+            &memory_values,
+        );
 
         let register_map = RegisterMap::from_qpu_result_data(&qpu_result_data)
             .expect("Should be able to create RegisterMap from rectangular QPU readout");
@@ -408,6 +451,13 @@ mod describe_register_map {
         let expected_bar = arr2(&[[2, 0], [3, 1]]);
 
         assert_eq!(bar, expected_bar);
+
+        let expected_memory_values = hashmap! {
+            String::from("binary") => MemoryValues::Binary(vec![0, 1, 2]),
+            String::from("int") => MemoryValues::Integer(vec![3, 4, 5]),
+            String::from("real") => MemoryValues::Real(vec![6.0, 7.0, 8.0]),
+        };
+        assert_eq!(qpu_result_data.memory_values, expected_memory_values);
     }
 
     #[test]
@@ -427,8 +477,11 @@ mod describe_register_map {
             String::from("qE") => dummy_readout_values(vec![44]),
         };
 
-        let qpu_result_data =
-            QpuResultData::from_controller_mappings_and_values(&readout_mappings, &readout_values);
+        let qpu_result_data = QpuResultData::from_controller_mappings_and_values(
+            &readout_mappings,
+            &readout_values,
+            &hashmap! {},
+        );
 
         RegisterMap::from_qpu_result_data(&qpu_result_data)
             .expect_err("Should not be able to create RegisterMap from QPU readout with missing indices for a register");
@@ -448,8 +501,19 @@ mod describe_register_map {
             String::from("qC") => dummy_readout_values(vec![3]),
         };
 
-        let qpu_result_data =
-            QpuResultData::from_controller_mappings_and_values(&readout_mappings, &readout_values);
+        let (binary_values, integer_values, real_values) =
+            dummy_memory_values(vec![0, 1, 2], vec![3, 4, 5], vec![6.0, 7.0, 8.0]);
+        let memory_values = hashmap! {
+            String::from("binary") => binary_values,
+            String::from("int") => integer_values,
+            String::from("real") => real_values,
+        };
+
+        let qpu_result_data = QpuResultData::from_controller_mappings_and_values(
+            &readout_mappings,
+            &readout_values,
+            &memory_values,
+        );
 
         RegisterMap::from_qpu_result_data(&qpu_result_data)
             .expect_err("Should not be able to create RegisterMap from QPU readout with jagged data for a register");
diff --git a/crates/lib/src/qpu/execution.rs b/crates/lib/src/qpu/execution.rs
index 9ccd7db07..886dcbe22 100644
--- a/crates/lib/src/qpu/execution.rs
+++ b/crates/lib/src/qpu/execution.rs
@@ -283,6 +283,7 @@ impl<'a> Execution<'a> {
             result_data: ResultData::Qpu(QpuResultData::from_controller_mappings_and_values(
                 job_handle.readout_map(),
                 &response.readout_values,
+                &response.memory_values,
             )),
             duration: Some(Duration::from_micros(
                 response.execution_duration_microseconds,
diff --git a/crates/lib/src/qpu/result_data.rs b/crates/lib/src/qpu/result_data.rs
index d6dd0b216..b1806af3c 100644
--- a/crates/lib/src/qpu/result_data.rs
+++ b/crates/lib/src/qpu/result_data.rs
@@ -6,7 +6,8 @@ use quil_rs::instruction::MemoryReference;
 use std::collections::HashMap;
 
 use qcs_api_client_grpc::models::controller::{
-    readout_values as controller_readout_values, ReadoutValues as ControllerReadoutValues,
+    self, data_value as controller_memory_value, readout_values as controller_readout_values,
+    DataValue as ControllerMemoryValues, ReadoutValues as ControllerReadoutValues,
 };
 
 /// A row of readout values from the QPU. Each row contains all the values emitted to a
@@ -21,12 +22,25 @@ pub enum ReadoutValues {
     Complex(Vec<Complex64>),
 }
 
+/// A row of data containing the contents of each memory region at the end of a job.
+#[derive(Debug, Clone, EnumAsInner, PartialEq)]
+pub enum MemoryValues {
+    /// Values that correspond to a memory region declared with the BIT or OCTET data type.
+    Binary(Vec<u8>),
+    /// Values that correspond to a memory region declared with the INTEGER data type.
+    Integer(Vec<i64>),
+    /// Values that correspond to a memory region declared with the REAL data type.
+    Real(Vec<f64>),
+}
+
 /// This struct encapsulates data returned from the QPU after executing a job.
 #[allow(clippy::module_name_repetitions)]
 #[derive(Debug, Clone, PartialEq)]
 pub struct QpuResultData {
     pub(crate) mappings: HashMap<String, String>,
     pub(crate) readout_values: HashMap<String, ReadoutValues>,
+    /// The final contents of each memory region, keyed on region name.
+    pub(crate) memory_values: HashMap<String, MemoryValues>,
 }
 
 impl QpuResultData {
@@ -36,21 +50,24 @@ impl QpuResultData {
     pub fn from_mappings_and_values(
         mappings: HashMap<String, String>,
         readout_values: HashMap<String, ReadoutValues>,
+        memory_values: HashMap<String, MemoryValues>,
     ) -> Self {
         Self {
             mappings,
             readout_values,
+            memory_values,
         }
     }
 
     /// Creates a new [`QpuResultData`] using data returned from controller service.
     pub(crate) fn from_controller_mappings_and_values(
         mappings: &HashMap<String, String>,
-        values: &HashMap<String, ControllerReadoutValues>,
+        readout_values: &HashMap<String, ControllerReadoutValues>,
+        memory_values: &HashMap<String, ControllerMemoryValues>,
     ) -> Self {
         Self {
             mappings: mappings.clone(),
-            readout_values: values
+            readout_values: readout_values
                 .iter()
                 .map(|(key, readout_values)| {
                     (
@@ -74,6 +91,27 @@ impl QpuResultData {
                     )
                 })
                 .collect(),
+            memory_values: memory_values
+                .iter()
+                .filter_map(|(key, memory_values)| {
+                    memory_values.value.as_ref().map(|value| {
+                        (
+                            key.clone(),
+                            match value {
+                                controller_memory_value::Value::Binary(
+                                    controller::BinaryDataValue { data: v },
+                                ) => MemoryValues::Binary(v.clone()),
+                                controller_memory_value::Value::Integer(
+                                    controller::IntegerDataValue { data: v },
+                                ) => MemoryValues::Integer(v.clone()),
+                                controller_memory_value::Value::Real(
+                                    controller::RealDataValue { data: v },
+                                ) => MemoryValues::Real(v.clone()),
+                            },
+                        )
+                    })
+                })
+                .collect(),
         }
     }
 
@@ -100,4 +138,10 @@ impl QpuResultData {
     pub fn readout_values(&self) -> &HashMap<String, ReadoutValues> {
         &self.readout_values
     }
+
+    /// Get mapping of a memory region (ie. "ro") to the final contents of that memory region.
+    #[must_use]
+    pub fn memory_values(&self) -> &HashMap<String, MemoryValues> {
+        &self.memory_values
+    }
 }
diff --git a/crates/python/qcs_sdk/qpu/__init__.pyi b/crates/python/qcs_sdk/qpu/__init__.pyi
index ae9de3f21..837e42bef 100644
--- a/crates/python/qcs_sdk/qpu/__init__.pyi
+++ b/crates/python/qcs_sdk/qpu/__init__.pyi
@@ -1,4 +1,4 @@
-from typing import Dict, List, Mapping, Sequence, Optional, Union, final, Any
+from typing import Dict, List, Mapping, Sequence, Optional, Union, final
 
 from qcs_sdk.client import QCSClient
 
@@ -51,6 +51,46 @@ class ReadoutValues:
     @staticmethod
     def from_complex(inner: Sequence[complex]) -> "ReadoutValues": ...
 
+@final
+class MemoryValues:
+    """
+    A row of data containing the contents of a memory region at the end of a job. There
+    is a variant for each possible type the memory region could be.
+
+    Variants:
+        - ``binary``: Corresponds to the Quil `BIT` and `OCTET` types.
+        - ``integer``: Corresponds to the Quil `INTEGER` types.
+        - ``real``: Corresponds to the Quil `REAL` type.
+
+    Methods (each per variant):
+        - ``is_*``: if the underlying values are that type.
+        - ``as_*``: if the underlying values are that type, then those values, otherwise ``None``.
+        - ``to_*``: the underlying values as that type, raises ``ValueError`` if they are not.
+        - ``from_*``: wrap underlying values as this enum type.
+    """
+    def __new__(cls, values: Union[List[int], List[float]]):
+        """Construct a new ReadoutValues from a list of values."""
+        ...
+    def inner(self) -> Union[List[int], List[float]]:
+        """Return the inner list of readout values."""
+        ...
+    def is_integer(self) -> bool: ...
+    def is_binary(self) -> bool: ...
+    def is_real(self) -> bool: ...
+    def as_integer(self) -> Optional[List[int]]: ...
+    def as_binary(self) -> Optional[List[int]]: ...
+    def as_real(self) -> Optional[List[float]]: ...
+    def to_integer(self) -> List[int]: ...
+    def to_binary(self) -> List[int]: ...
+    def to_real(self) -> List[float]: ...
+    @staticmethod
+    def from_integer(inner: Sequence[int]) -> "ReadoutValues": ...
+    @staticmethod
+    def from_binary(inner: Sequence[int]) -> "ReadoutValues": ...
+    @staticmethod
+    def from_real(inner: Sequence[float]) -> "ReadoutValues": ...
+
+
 @final
 class QPUResultData:
     """
@@ -62,7 +102,7 @@ class QPUResultData:
     across all shots.
     """
 
-    def __new__(cls, mappings: Mapping[str, str], readout_values: Mapping[str, ReadoutValues]): ...
+    def __new__(cls, mappings: Mapping[str, str], readout_values: Mapping[str, ReadoutValues], memory_values: Mapping[str, MemoryValues]): ...
     @property
     def mappings(self) -> Dict[str, str]:
         """
@@ -75,13 +115,19 @@ class QPUResultData:
         Get the mappings of a readout values identifier (ie. "q0") to a set of ``ReadoutValues``
         """
         ...
+    @property
+    def memory_values(self) -> Dict[str, MemoryValues]:
+        """
+        Get mapping of a memory region (ie. "ro") to the final contents of that memory region.
+        """
+        ...
     def to_raw_readout_data(
         self,
     ) -> RawQPUReadoutData:
         """
         Get a copy of this result data flattened into a ``RawQPUReadoutData``. This reduces
         the contained data down to primitive types, offering a simpler structure at the
-        cost of the type safety provided by ``ReadoutValues``.
+        cost of the type safety provided by ``ReadoutValues`` and ``MemoryValues``.
         """
         ...
 
@@ -105,6 +151,12 @@ class RawQPUReadoutData:
         Get the mappings of a readout values identifier (ie. "q0") to a list of those readout values
         """
         ...
+    @property
+    def memory_values(self) -> Dict[str, Optional[Union[List[int], List[float]]]]:
+        """
+        Get mapping of a memory region (ie. "ro") to the final contents of that memory region.
+        """
+        ...
 
 class ListQuantumProcessorsError(RuntimeError):
     """A request to list available Quantum Processors failed."""
diff --git a/crates/python/qcs_sdk/qpu/api.pyi b/crates/python/qcs_sdk/qpu/api.pyi
index 6794f0152..886f5a19b 100644
--- a/crates/python/qcs_sdk/qpu/api.pyi
+++ b/crates/python/qcs_sdk/qpu/api.pyi
@@ -1,7 +1,7 @@
-from enum import Enum
-from typing import Callable, Dict, List, Sequence, Mapping, Optional, Union, final
+from typing import Dict, List, Sequence, Mapping, Optional, Union, final
 
 from qcs_sdk.client import QCSClient
+from qcs_sdk.qpu import MemoryValues
 
 class SubmissionError(RuntimeError):
     """There was a problem submitting the program to QCS for execution."""
@@ -77,6 +77,11 @@ class ExecutionResults:
         """
         ...
     @property
+    def memory(self) -> Dict[str, MemoryValues]:
+        """
+        The final state of memory for parameters that were read from and written to during the exectuion of the program.
+        """
+    @property
     def execution_duration_microseconds(self) -> Optional[int]:
         """The time spent executing the program."""
         ...
diff --git a/crates/python/src/execution_data.rs b/crates/python/src/execution_data.rs
index 15c5fd79b..3277ec411 100644
--- a/crates/python/src/execution_data.rs
+++ b/crates/python/src/execution_data.rs
@@ -21,7 +21,7 @@ py_wrap_union_enum! {
         qvm: Qvm => PyQvmResultData
     }
 }
-impl_repr!(PyQpuResultData);
+impl_repr!(PyResultData);
 
 wrap_error!(RustRegisterMatrixConversionError(
     qcs::RegisterMatrixConversionError
diff --git a/crates/python/src/qpu/api.rs b/crates/python/src/qpu/api.rs
index 7fe85954d..f98c407aa 100644
--- a/crates/python/src/qpu/api.rs
+++ b/crates/python/src/qpu/api.rs
@@ -9,10 +9,12 @@ use pyo3::{
     pyclass::CompareOp,
     pyfunction, pymethods,
     types::{PyComplex, PyInt},
-    IntoPy, Py, PyObject, PyResult, Python,
+    IntoPy, Py, PyObject, PyResult, Python, ToPyObject,
 };
 use qcs::qpu::api::{ConnectionStrategy, ExecutionOptions, ExecutionOptionsBuilder};
-use qcs_api_client_grpc::models::controller::{readout_values, ControllerJobExecutionResult};
+use qcs_api_client_grpc::models::controller::{
+    data_value, readout_values, ControllerJobExecutionResult,
+};
 use rigetti_pyo3::{
     create_init_submodule, impl_as_mut_for_wrapper, impl_repr, num_complex, py_wrap_error,
     py_wrap_type, py_wrap_union_enum, wrap_error, PyWrapper, ToPythonError,
@@ -22,6 +24,8 @@ use crate::py_sync::py_function_sync_async;
 
 use crate::client::PyQcsClient;
 
+use super::result_data::PyMemoryValues;
+
 create_init_submodule! {
     classes: [
         PyRegister,
@@ -187,6 +191,8 @@ pub struct ExecutionResults {
     /// QPU execution duration.
     #[pyo3(get)]
     pub execution_duration_microseconds: Option<u64>,
+    #[pyo3(get)]
+    pub memory: HashMap<String, PyMemoryValues>,
 }
 
 #[pymethods]
@@ -194,27 +200,74 @@ impl ExecutionResults {
     #[new]
     fn new(
         buffers: HashMap<String, ExecutionResult>,
+        memory: HashMap<String, PyMemoryValues>,
         execution_duration_microseconds: Option<u64>,
     ) -> Self {
         Self {
             buffers,
             execution_duration_microseconds,
+            memory,
         }
     }
 }
 
-impl From<ControllerJobExecutionResult> for ExecutionResults {
-    fn from(value: ControllerJobExecutionResult) -> Self {
-        let buffers = value
+impl ExecutionResults {
+    fn from_controller_job_execution_result(
+        py: Python<'_>,
+        result: ControllerJobExecutionResult,
+    ) -> PyResult<Self> {
+        let buffers = result
             .readout_values
             .into_iter()
             .filter_map(|(key, val)| val.values.map(|values| (key, values.into())))
             .collect();
 
-        Self {
+        let memory = result.memory_values.iter().try_fold(
+            HashMap::with_capacity(result.memory_values.len()),
+            |mut acc, (key, value)| -> PyResult<HashMap<_, _>> {
+                if let Some(value) = &value.value {
+                    acc.insert(
+                        key.clone(),
+                        match value {
+                            data_value::Value::Binary(value) => PyMemoryValues::from_binary(
+                                py,
+                                value
+                                    .data
+                                    .iter()
+                                    .map(|v| v.to_object(py).extract(py))
+                                    .collect::<PyResult<Vec<_>>>()?,
+                            )?,
+                            data_value::Value::Integer(value) => PyMemoryValues::from_integer(
+                                py,
+                                value
+                                    .data
+                                    .iter()
+                                    .map(|v| v.to_object(py).extract(py))
+                                    .collect::<PyResult<Vec<_>>>()?,
+                            )?,
+                            data_value::Value::Real(value) => PyMemoryValues::from_real(
+                                py,
+                                value
+                                    .data
+                                    .iter()
+                                    .map(|v| v.to_object(py).extract(py))
+                                    .collect::<PyResult<Vec<_>>>()?,
+                            )?,
+                        },
+                    )
+                } else {
+                    None
+                };
+
+                Ok(acc)
+            },
+        )?;
+
+        Ok(Self {
             buffers,
-            execution_duration_microseconds: Some(value.execution_duration_microseconds),
-        }
+            execution_duration_microseconds: Some(result.execution_duration_microseconds),
+            memory,
+        })
     }
 }
 
@@ -234,7 +287,9 @@ py_function_sync_async! {
             .map_err(RustRetrieveResultsError::from)
             .map_err(RustRetrieveResultsError::to_py_err)?;
 
-        Ok(results.into())
+        Python::with_gil(|py| {
+            ExecutionResults::from_controller_job_execution_result(py, results)
+        })
     }
 }
 
diff --git a/crates/python/src/qpu/mod.rs b/crates/python/src/qpu/mod.rs
index 71433fe06..c088583de 100644
--- a/crates/python/src/qpu/mod.rs
+++ b/crates/python/src/qpu/mod.rs
@@ -14,11 +14,14 @@ pub mod translation;
 use crate::client::PyQcsClient;
 use crate::py_sync::py_function_sync_async;
 
+use self::result_data::PyMemoryValues;
+
 create_init_submodule! {
     classes: [
         PyQpuResultData,
         RawQpuReadoutData,
-        PyReadoutValues
+        PyReadoutValues,
+        PyMemoryValues
     ],
     errors: [
         ListQuantumProcessorsError
diff --git a/crates/python/src/qpu/result_data.rs b/crates/python/src/qpu/result_data.rs
index 6b6388655..24f5e8589 100644
--- a/crates/python/src/qpu/result_data.rs
+++ b/crates/python/src/qpu/result_data.rs
@@ -1,12 +1,15 @@
 use std::collections::HashMap;
 
 use pyo3::{
-    pyclass, pymethods,
+    exceptions::PyNotImplementedError,
+    pyclass,
+    pyclass::CompareOp,
+    pymethods,
     types::{PyComplex, PyFloat, PyInt, PyList},
     IntoPy, Py, PyResult, Python,
 };
-use qcs::qpu::{QpuResultData, ReadoutValues};
-use rigetti_pyo3::{py_wrap_type, py_wrap_union_enum, PyTryFrom, PyWrapper, ToPython};
+use qcs::qpu::{result_data::MemoryValues, QpuResultData, ReadoutValues};
+use rigetti_pyo3::{impl_repr, py_wrap_type, py_wrap_union_enum, PyTryFrom, PyWrapper, ToPython};
 
 py_wrap_union_enum! {
     PyReadoutValues(ReadoutValues) as "ReadoutValues" {
@@ -16,9 +19,33 @@ py_wrap_union_enum! {
     }
 }
 
+py_wrap_union_enum! {
+    #[derive(Debug, PartialEq)]
+    PyMemoryValues(MemoryValues) as "MemoryValues" {
+        binary: Binary => Vec<Py<PyInt>>,
+        integer: Integer => Vec<Py<PyInt>>,
+        real: Real => Vec<Py<PyFloat>>
+    }
+}
+impl_repr!(PyMemoryValues);
+
+#[pymethods]
+impl PyMemoryValues {
+    fn __richcmp__(&self, other: &PyMemoryValues, op: CompareOp) -> PyResult<bool> {
+        match op {
+            CompareOp::Eq => Ok(self == other),
+            CompareOp::Ne => Ok(self != other),
+            _ => Err(PyNotImplementedError::new_err(
+                "MemoryValues only supports equality comparisons",
+            )),
+        }
+    }
+}
+
 py_wrap_type! {
     PyQpuResultData(QpuResultData) as "QPUResultData"
 }
+impl_repr!(PyQpuResultData);
 
 #[pymethods]
 impl PyQpuResultData {
@@ -27,10 +54,12 @@ impl PyQpuResultData {
         py: Python<'_>,
         mappings: HashMap<String, String>,
         readout_values: HashMap<String, PyReadoutValues>,
+        memory_values: HashMap<String, PyMemoryValues>,
     ) -> PyResult<Self> {
         Ok(Self(QpuResultData::from_mappings_and_values(
             mappings,
             HashMap::<String, ReadoutValues>::py_try_from(py, &readout_values)?,
+            HashMap::<String, MemoryValues>::py_try_from(py, &memory_values)?,
         )))
     }
 
@@ -44,6 +73,11 @@ impl PyQpuResultData {
         self.as_inner().readout_values().to_python(py)
     }
 
+    #[getter]
+    fn memory_values(&self, py: Python<'_>) -> PyResult<HashMap<String, PyMemoryValues>> {
+        self.as_inner().memory_values().to_python(py)
+    }
+
     pub(crate) fn to_raw_readout_data(&self, py: Python<'_>) -> RawQpuReadoutData {
         RawQpuReadoutData {
             mappings: self.as_inner().mappings().clone(),
@@ -62,6 +96,21 @@ impl PyQpuResultData {
                     )
                 })
                 .collect::<HashMap<String, Py<PyList>>>(),
+            memory_values: self
+                .as_inner()
+                .memory_values()
+                .iter()
+                .map(|(register, memory_values)| {
+                    (
+                        register.to_string(),
+                        match memory_values {
+                            MemoryValues::Binary(values) => PyList::new(py, values).into_py(py),
+                            MemoryValues::Integer(values) => PyList::new(py, values).into_py(py),
+                            MemoryValues::Real(values) => PyList::new(py, values).into_py(py),
+                        },
+                    )
+                })
+                .collect::<HashMap<String, Py<PyList>>>(),
         }
     }
 }
@@ -72,11 +121,11 @@ impl PyQpuResultData {
 /// each register contains.
 #[derive(Debug)]
 #[pyclass(name = "RawQPUReadoutData")]
+#[pyo3(get_all)]
 pub struct RawQpuReadoutData {
-    #[pyo3(get)]
     mappings: HashMap<String, String>,
-    #[pyo3(get)]
     readout_values: HashMap<String, Py<PyList>>,
+    memory_values: HashMap<String, Py<PyList>>,
 }
 
 impl RawQpuReadoutData {


Test patch:
diff --git a/crates/python/tests/execution_data/test_execution_data.py b/crates/python/tests/execution_data/test_execution_data.py
index f0901bf95..5e8426717 100644
--- a/crates/python/tests/execution_data/test_execution_data.py
+++ b/crates/python/tests/execution_data/test_execution_data.py
@@ -1,4 +1,4 @@
-from qcs_sdk.qpu import ReadoutValues, QPUResultData
+from qcs_sdk.qpu import ReadoutValues, MemoryValues, QPUResultData
 from qcs_sdk.qvm import QVMResultData
 from qcs_sdk import ResultData, RegisterData, RegisterMatrix
 import numpy as np
@@ -13,12 +13,15 @@ def test_to_register_map_from_qpu_result_data(self):
             "ro[1]": "qB",
             "ro[2]": "qC",
         }
-        values = {
+        readout_values = {
             "qA": ReadoutValues.from_integer([0, 1]),
             "qB": ReadoutValues.from_integer([1, 2]),
             "qC": ReadoutValues.from_integer([2, 3]),
         }
-        result_data = ResultData.from_qpu(QPUResultData(mappings, values))
+        memory_values = {
+            "ro": MemoryValues.from_integer([1, 2, 3]),
+        }
+        result_data = ResultData.from_qpu(QPUResultData(mappings, readout_values, memory_values))
         register_map = result_data.to_register_map()
         ro = register_map.get_register_matrix("ro")
         assert ro is not None, "'ro' should exist in the register map"
@@ -39,7 +42,7 @@ def test_to_register_map_from_jagged_qpu_result_data(self):
             "qB": ReadoutValues.from_integer([1]),
             "qC": ReadoutValues.from_integer([2, 3]),
         }
-        result_data = ResultData.from_qpu(QPUResultData(mappings, values))
+        result_data = ResultData.from_qpu(QPUResultData(mappings, values, {}))
 
         with pytest.raises(ValueError):
             result_data.to_register_map()
diff --git a/crates/python/tests/qpu/test_qpu.py b/crates/python/tests/qpu/test_qpu.py
index 7f64f5b04..bb18c5d48 100644
--- a/crates/python/tests/qpu/test_qpu.py
+++ b/crates/python/tests/qpu/test_qpu.py
@@ -3,6 +3,7 @@
 from qcs_sdk.qpu import (
     QPUResultData,
     ReadoutValues,
+    MemoryValues,
     ListQuantumProcessorsError,
     list_quantum_processors,
     list_quantum_processors_async,
@@ -26,13 +27,18 @@ def test_readout_values():
 def test_qpu_result_data():
     mappings = {"a": "_q0"}
     readout_values = {"a": ReadoutValues.from_integer([0, 1])}
-    result_data = QPUResultData(mappings, readout_values)
+    memory_values = { "int": MemoryValues.from_integer([2, 3]), "real": MemoryValues.from_real([3.0, 4.0]), "binary": MemoryValues.from_binary([0, 1]) }
+    result_data = QPUResultData(mappings, readout_values, memory_values)
 
     assert result_data.mappings == mappings
     assert result_data.readout_values["a"].as_integer() == readout_values["a"].as_integer()
+    assert result_data.memory_values["int"].as_integer() == memory_values["int"].as_integer()
+    assert result_data.memory_values["binary"].as_integer() == memory_values["binary"].as_integer()
+    assert result_data.memory_values["real"].as_integer() == memory_values["real"].as_integer()
     raw_data = result_data.to_raw_readout_data()
     assert raw_data.mappings == {"a": "_q0"}
     assert raw_data.readout_values == {"a": [0, 1]}
+    assert raw_data.memory_values == {"int": [2, 3], "binary": [0, 1], "real": [3.0, 4.0]}
 
 
 @pytest.mark.qcs_session