diff --git a/Cargo.lock b/Cargo.lock index 96cd6ff1b..d16ae1228 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1000,7 +1000,7 @@ dependencies = [ "tokio", "tokio-rustls 0.22.0", "tower-service", - "webpki 0.21.4", + "webpki", ] [[package]] @@ -1017,20 +1017,21 @@ dependencies = [ "rustls-native-certs 0.5.0", "tokio", "tokio-rustls 0.22.0", - "webpki 0.21.4", + "webpki", ] [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ + "futures-util", "http", "hyper", - "rustls 0.20.8", + "rustls 0.21.6", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", ] [[package]] @@ -2223,9 +2224,9 @@ checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "reqwest" -version = "0.11.16" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ "base64 0.21.0", "bytes", @@ -2236,7 +2237,7 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls 0.23.2", + "hyper-rustls 0.24.1", "ipnet", "js-sys", "log", @@ -2245,14 +2246,14 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.20.8", + "rustls 0.21.6", "rustls-native-certs 0.6.2", "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", "tokio-socks", "tower-service", "url", @@ -2403,26 +2404,14 @@ dependencies = [ "log", "ring", "sct 0.6.1", - "webpki 0.21.4", + "webpki", ] [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" -dependencies = [ - "log", - "ring", - "sct 0.7.0", - "webpki 0.22.0", -] - -[[package]] -name = "rustls" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07180898a28ed6a7f7ba2311594308f595e3dd2e3c3812fa0a80a47b45f17e5d" +checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" dependencies = [ "log", "ring", @@ -2465,9 +2454,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.100.1" +version = "0.101.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ "ring", "untrusted", @@ -2982,27 +2971,16 @@ checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ "rustls 0.19.1", "tokio", - "webpki 0.21.4", -] - -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.8", - "tokio", - "webpki 0.22.0", + "webpki", ] [[package]] name = "tokio-rustls" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.0", + "rustls 0.21.6", "tokio", ] @@ -3101,7 +3079,7 @@ dependencies = [ "rustls-native-certs 0.6.2", "rustls-pemfile", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", "tokio-stream", "tower", "tower-layer", @@ -3514,24 +3492,11 @@ dependencies = [ "untrusted", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki 0.22.0", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "which" @@ -3733,11 +3698,12 @@ dependencies = [ [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e07b4884f..c386dc8c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = ["crates/*"] +resolver = "2" [workspace.dependencies] qcs-api = "0.2.1" @@ -27,3 +28,4 @@ pyo3 = { version = "0.17" } pyo3-asyncio = { version = "0.17", features = ["tokio-runtime"] } pyo3-build-config = { version = "0.17" } rigetti-pyo3 = { version = "0.1.0", features = ["complex"] } + diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index f21895cc0..633db1218 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -29,7 +29,7 @@ qcs-api-client-common.workspace = true qcs-api-client-openapi.workspace = true qcs-api-client-grpc.workspace = true quil-rs.workspace = true -reqwest = { version = "0.11.12", default-features = false, features = [ +reqwest = { version = "0.11.20", default-features = false, features = [ "rustls-tls", "json", ] } @@ -37,7 +37,7 @@ rmp-serde = "1.1.1" serde = { version = "1.0.145", features = ["derive"] } serde_json.workspace = true thiserror.workspace = true -tokio = { workspace = true, features = ["fs"] } +tokio = { workspace = true, features = ["fs", "rt-multi-thread"] } toml = "0.7.3" tracing = { version = "0.1", optional = true, features = ["log"] } uuid = { version = "1.2.1", features = ["v4"] } diff --git a/crates/lib/src/executable.rs b/crates/lib/src/executable.rs index 6f02d034f..43880f300 100644 --- a/crates/lib/src/executable.rs +++ b/crates/lib/src/executable.rs @@ -111,6 +111,7 @@ impl<'executable> Executable<'executable, '_> { /// 1. `quil` is a string slice representing the original program to be run. The returned /// [`Executable`] will only live as long as this reference. #[must_use] + #[allow(clippy::missing_panics_doc)] pub fn from_quil>>(quil: Quil) -> Self { Self { quil: quil.into(), diff --git a/crates/lib/src/execution_data.rs b/crates/lib/src/execution_data.rs index 7bbda14b9..aae083d17 100644 --- a/crates/lib/src/execution_data.rs +++ b/crates/lib/src/execution_data.rs @@ -83,7 +83,7 @@ pub enum RegisterMatrix { /// register. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[repr(transparent)] -pub struct RegisterMap(HashMap); +pub struct RegisterMap(pub HashMap); /// Errors that may occur when trying to build a [`RegisterMatrix`] from execution data #[allow(missing_docs)] diff --git a/crates/python/.stubtest-allowlist b/crates/python/.stubtest-allowlist index d69c5b1df..5bae07b5f 100644 --- a/crates/python/.stubtest-allowlist +++ b/crates/python/.stubtest-allowlist @@ -1,3 +1,3 @@ -qcs_sdk.qcs_sdk qcs_sdk._gather_diagnostics qcs_sdk.diagnostics +qcs_sdk.qcs_sdk diff --git a/crates/python/qcs_sdk/__init__.pyi b/crates/python/qcs_sdk/__init__.pyi index 15b50f6b6..e5482847a 100644 --- a/crates/python/qcs_sdk/__init__.pyi +++ b/crates/python/qcs_sdk/__init__.pyi @@ -1,12 +1,12 @@ import datetime from enum import Enum -from typing import Dict, List, Sequence, Optional, Union, final +from typing import Dict, List, Sequence, Optional, Union, final, Iterable, Tuple import numpy as np from numpy.typing import NDArray -from qcs_sdk.qpu import QPUResultData -from qcs_sdk.qvm import QVMResultData +from qcs_sdk.qpu import QPUResultData, RawQPUReadoutData +from qcs_sdk.qvm import QVMResultData, RawQVMReadoutData from qcs_sdk.compiler.quilc import CompilerOpts from qcs_sdk.client import QCSClient as QCSClient @@ -205,6 +205,11 @@ class RegisterMatrix: def from_real(inner: NDArray[np.float64]) -> "RegisterMatrix": ... @staticmethod def from_complex(inner: NDArray[np.complex128]) -> "RegisterMatrix": ... + def to_ndarray(self) -> Union[NDArray[np.complex128], NDArray[np.int64], NDArray[np.float64]]: + """ + Get the RegisterMatrix as numpy ``ndarray``. + """ + ... @final class RegisterMap: @@ -213,6 +218,14 @@ class RegisterMap: def get_register_matrix(self, register_name: str) -> Optional[RegisterMatrix]: """Get the ``RegisterMatrix`` for the given register. Returns `None` if the register doesn't exist.""" ... + def get(self, default: Optional[RegisterMatrix] = None) -> Optional[RegisterMatrix]: ... + def items(self) -> Iterable[Tuple[str, RegisterMatrix]]: ... + def keys(self) -> Iterable[str]: ... + def values(self) -> Iterable[RegisterMatrix]: ... + def __iter__(self) -> Iterable[str]: ... + def __getitem__(self, item: str) -> RegisterMatrix: ... + def __contains__(self, key: str) -> bool: ... + def __len__(self) -> int: ... @final class ResultData: @@ -261,6 +274,11 @@ class ResultData: - ``from_*``: wrap underlying values as this enum type. """ + def __new__(cls, inner: Union[QPUResultData, QVMResultData]) -> "ResultData": + """ + Create a new ResultData from either QVM or QPU result data. + """ + ... def to_register_map(self) -> RegisterMap: """ Convert ``ResultData`` from its inner representation as ``QVMResultData`` or @@ -281,6 +299,11 @@ class ResultData: selects the last value per-shot based on the program that was run. """ ... + def to_raw_readout_data(self) -> Union[RawQPUReadoutData, RawQVMReadoutData]: + """ + Get the raw data returned from the QVM or QPU. See ``RawQPUReadoutData`` and + ``RawQVMReadoutData`` for more information. + """ def inner( self, ) -> Union[QVMResultData, QPUResultData]: @@ -299,6 +322,7 @@ class ResultData: @final class ExecutionData: + def __new__(cls, result_data: ResultData, duration: Optional[datetime.timedelta] = None): ... @property def result_data(self) -> ResultData: ... @result_data.setter @@ -327,11 +351,15 @@ class RegisterData: """ + def __new__(cls, inner: Union[List[List[int]], List[List[float]], List[List[complex]]]) -> "RegisterData": ... def inner( self, ) -> Union[List[List[int]], List[List[float]], List[List[complex]]]: """Returns the inner value.""" ... + def as_ndarray(self) -> Union[NDArray[np.int64], NDArray[np.float64], NDArray[np.complex128]]: + """Returns the values as an ``ndarray``.""" + ... def is_i8(self) -> bool: ... def is_i16(self) -> bool: ... def is_f64(self) -> bool: ... diff --git a/crates/python/qcs_sdk/qpu/__init__.pyi b/crates/python/qcs_sdk/qpu/__init__.pyi index 47ed7058b..ae9de3f21 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 +from typing import Dict, List, Mapping, Sequence, Optional, Union, final, Any from qcs_sdk.client import QCSClient @@ -29,6 +29,9 @@ class ReadoutValues: """ + def __new__(cls, values: Union[List[int], List[float], List[complex]]): + """Construct a new ReadoutValues from a list of values.""" + ... def inner(self) -> Union[List[int], List[float], List[complex]]: """Return the inner list of readout values.""" ... @@ -52,11 +55,14 @@ class ReadoutValues: class QPUResultData: """ Encapsulates data returned from the QPU after executing a job. + + ``QPUResultData`` contains "mappings", which map declared memory regions + in a program (ie. "ro[0]") to that regions readout key in "readout_values". + "readout_values" maps those readout keys to the values emitted for that region + 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]): ... @property def mappings(self) -> Dict[str, str]: """ @@ -69,6 +75,36 @@ class QPUResultData: Get the mappings of a readout values identifier (ie. "q0") to a set of ``ReadoutValues`` """ ... + 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``. + """ + ... + +@final +class RawQPUReadoutData: + """ + Encapsulates data returned from the QPU after executing a job. Compared to + ``QPUReadoutData``, the readout values in this class are returned as lists + of numbers instead of values wrapped by the ``ReadoutValues`` class. + """ + + @property + def mappings(self) -> Dict[str, str]: + """ + Get the mappings of a memory region (ie. "ro[0]") to it's key name in readout_values + """ + ... + @property + def readout_values(self) -> Dict[str, Union[List[int], List[float], List[complex]]]: + """ + Get the mappings of a readout values identifier (ie. "q0") to a list of those readout values + """ + ... 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 39e7041b8..7bb899d23 100644 --- a/crates/python/qcs_sdk/qpu/api.pyi +++ b/crates/python/qcs_sdk/qpu/api.pyi @@ -48,6 +48,9 @@ class Register: class ExecutionResult: """Execution readout data from a particular memory location.""" + @staticmethod + def from_register(register: Register) -> "ExecutionResult": + """Build an `ExecutionResult` from a `Register`.""" @property def shape(self) -> List[int]: """The shape of the result data.""" diff --git a/crates/python/qcs_sdk/qvm/__init__.pyi b/crates/python/qcs_sdk/qvm/__init__.pyi index 826983518..0e77908a4 100644 --- a/crates/python/qcs_sdk/qvm/__init__.pyi +++ b/crates/python/qcs_sdk/qvm/__init__.pyi @@ -1,4 +1,4 @@ -from typing import final, Mapping, Optional, Sequence, Tuple, Union +from typing import final, Mapping, Optional, Sequence, Tuple, Union, Dict, List from qcs_sdk import RegisterData, QCSClient @@ -24,6 +24,22 @@ class QVMResultData: Get the mapping of register names (ie. "ro") to a ``RegisterData`` matrix containing the register values. """ ... + def to_raw_readout_data( + self, + ) -> RawQVMReadoutData: + """ + Get a copy of this result data flattened into a ``RawQVMReadoutData`` + """ + ... + +@final +class RawQVMReadoutData: + @property + def memory(self) -> Dict[str, Union[List[List[int]], List[List[float]]]]: + """ + The mapping of register names (ie. "ro") to a 2-d list containing the + values for that register. + """ @final class QVMOptions: diff --git a/crates/python/src/execution_data.rs b/crates/python/src/execution_data.rs index 7e953d78b..6fa5b3821 100644 --- a/crates/python/src/execution_data.rs +++ b/crates/python/src/execution_data.rs @@ -1,16 +1,19 @@ use std::time::Duration; use numpy::{Complex64, PyArray2}; -use pyo3::{exceptions::PyValueError, pymethods, types::PyDelta, Py, PyResult, Python}; +use pyo3::exceptions::PyKeyError; +use pyo3::{ + exceptions::PyValueError, pyclass, pymethods, types::PyDelta, IntoPy, Py, PyObject, PyRef, + PyRefMut, PyResult, Python, ToPyObject, +}; use qcs::{ExecutionData, RegisterMap, RegisterMatrix, ResultData}; -use qcs_api_client_grpc::models::controller::{readout_values::Values, ReadoutValues}; use rigetti_pyo3::{ - py_wrap_data_struct, py_wrap_error, py_wrap_type, py_wrap_union_enum, wrap_error, PyTryFrom, - PyWrapper, ToPython, ToPythonError, + impl_repr, py_wrap_data_struct, py_wrap_error, py_wrap_type, py_wrap_union_enum, wrap_error, + PyTryFrom, PyWrapper, ToPython, ToPythonError, }; +use crate::qpu::PyQpuResultData; use crate::qvm::PyQvmResultData; -use crate::{grpc::models::controller::PyReadoutValuesValues, qpu::PyQpuResultData}; py_wrap_union_enum! { PyResultData(ResultData) as "ResultData" { @@ -18,6 +21,7 @@ py_wrap_union_enum! { qvm: Qvm => PyQvmResultData } } +impl_repr!(PyQpuResultData); wrap_error!(RustRegisterMatrixConversionError( qcs::RegisterMatrixConversionError @@ -38,6 +42,17 @@ impl PyResultData { .map_err(ToPythonError::to_py_err)? .to_python(py) } + + pub fn to_raw_readout_data(&self, py: Python) -> PyResult { + match self.as_inner() { + ResultData::Qpu(_) => self + .to_qpu(py) + .map(|data| data.to_raw_readout_data(py).into_py(py)), + ResultData::Qvm(_) => self + .to_qvm(py) + .map(|data| data.to_raw_readout_data(py).into_py(py)), + } + } } py_wrap_data_struct! { @@ -46,35 +61,52 @@ py_wrap_data_struct! { duration: Option => Option> } } +impl_repr!(PyExecutionData); #[pymethods] impl PyExecutionData { #[new] - fn __new__(py: Python<'_>, result_data: PyResultData, duration: Option) -> PyResult { + fn __new__( + py: Python<'_>, + result_data: PyResultData, + duration: Option>, + ) -> PyResult { Ok(Self(ExecutionData { result_data: ResultData::py_try_from(py, &result_data)?, - duration: duration.map(Duration::from_micros), + duration: duration + .map(|delta| { + delta + .as_ref(py) + .call_method0("total_seconds") + .map(|result| result.extract::())? + .map(Duration::from_secs_f64) + }) + .transpose()?, })) } } -// From gRPC -py_wrap_data_struct! { - PyReadoutValues(ReadoutValues) as "ReadoutValues" { - values: Option => Option - } -} - py_wrap_type! { + #[pyo3(mapping)] PyRegisterMap(RegisterMap) as "RegisterMap"; } +impl_repr!(PyRegisterMap); py_wrap_type! { PyRegisterMatrix(RegisterMatrix) as "RegisterMatrix" } +impl_repr!(PyRegisterMatrix); #[pymethods] impl PyRegisterMatrix { + fn to_ndarray(&self, py: Python<'_>) -> PyResult { + match self.as_inner() { + RegisterMatrix::Integer(_) => self.to_integer(py).map(|matrix| matrix.to_object(py)), + RegisterMatrix::Real(_) => self.to_real(py).map(|matrix| matrix.to_object(py)), + RegisterMatrix::Complex(_) => self.to_complex(py).map(|matrix| matrix.to_object(py)), + } + } + #[staticmethod] fn from_integer(matrix: &PyArray2) -> PyRegisterMatrix { Self(RegisterMatrix::Integer(matrix.to_owned_array())) @@ -153,9 +185,114 @@ impl PyRegisterMatrix { #[pymethods] impl PyRegisterMap { - pub fn get_register_matrix(&self, register_name: String) -> Option { + pub fn get_register_matrix(&self, register_name: &str) -> Option { self.as_inner() - .get_register_matrix(®ister_name) + .get_register_matrix(register_name) .map(PyRegisterMatrix::from) } + + pub fn __len__(&self) -> usize { + self.as_inner().0.len() + } + + pub fn __contains__(&self, key: &str) -> bool { + self.as_inner().0.contains_key(key) + } + + pub fn __getitem__(&self, item: &str) -> PyResult { + self.get_register_matrix(item) + .ok_or(PyKeyError::new_err(format!( + "Key {item} not found in RegisterMap" + ))) + } + + pub fn __iter__(&self, py: Python<'_>) -> PyResult> { + Py::new( + py, + PyRegisterMapKeysIter { + inner: self.as_inner().0.clone().into_iter(), + }, + ) + } + + pub fn keys(&self, py: Python<'_>) -> PyResult> { + self.__iter__(py) + } + + pub fn values(&self, py: Python<'_>) -> PyResult> { + Py::new( + py, + PyRegisterMapValuesIter { + inner: self.as_inner().0.clone().into_iter(), + }, + ) + } + + pub fn items(&self, py: Python<'_>) -> PyResult> { + Py::new( + py, + PyRegisterMapItemsIter { + inner: self.as_inner().0.clone().into_iter(), + }, + ) + } + + pub fn get(&self, key: &str, default: Option) -> Option { + self.__getitem__(key).ok().or(default) + } +} + +#[pyclass] +pub struct PyRegisterMapItemsIter { + inner: std::collections::hash_map::IntoIter, +} + +#[pymethods] +impl PyRegisterMapItemsIter { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(String, PyRegisterMatrix)> { + slf.inner + .next() + .map(|(register, matrix)| (register, PyRegisterMatrix(matrix))) + } +} + +// The keys and values iterators are built on the iterator of the full +// `HashMap`, because the iterators returned by `keys()` and `values()` +// return an iterator with a _reference_ to the underlying `HashMap`. +// The reference would require these structs to specify a lifetime, +// which is incompatible with `#[pyclass]`. +#[pyclass] +pub struct PyRegisterMapKeysIter { + inner: std::collections::hash_map::IntoIter, +} + +#[pymethods] +impl PyRegisterMapKeysIter { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + slf.inner.next().map(|(register, _)| register) + } +} + +#[pyclass] +pub struct PyRegisterMapValuesIter { + inner: std::collections::hash_map::IntoIter, +} + +#[pymethods] +impl PyRegisterMapValuesIter { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + slf.inner.next().map(|(_, matrix)| PyRegisterMatrix(matrix)) + } } diff --git a/crates/python/src/qpu/api.rs b/crates/python/src/qpu/api.rs index 60f948030..ded1082cc 100644 --- a/crates/python/src/qpu/api.rs +++ b/crates/python/src/qpu/api.rs @@ -158,6 +158,25 @@ impl From for ExecutionResult { } } +#[pymethods] +impl ExecutionResult { + #[staticmethod] + fn from_register(register: PyRegister) -> Self { + match register.as_inner() { + Register::I32(values) => ExecutionResult { + shape: [values.len(), 1], + dtype: "integer".into(), + data: register, + }, + Register::Complex32(values) => ExecutionResult { + shape: [values.len(), 1], + dtype: "complex".into(), + data: register, + }, + } + } +} + #[pyclass] #[derive(Clone, Debug)] pub struct ExecutionResults { @@ -169,6 +188,20 @@ pub struct ExecutionResults { pub execution_duration_microseconds: Option, } +#[pymethods] +impl ExecutionResults { + #[new] + fn new( + buffers: HashMap, + execution_duration_microseconds: Option, + ) -> Self { + Self { + buffers, + execution_duration_microseconds, + } + } +} + impl From for ExecutionResults { fn from(value: ControllerJobExecutionResult) -> Self { let buffers = value diff --git a/crates/python/src/qpu/mod.rs b/crates/python/src/qpu/mod.rs index 283481dce..a77054551 100644 --- a/crates/python/src/qpu/mod.rs +++ b/crates/python/src/qpu/mod.rs @@ -3,7 +3,7 @@ use std::time::Duration; use pyo3::{exceptions::PyRuntimeError, pyfunction, PyResult}; use rigetti_pyo3::{create_init_submodule, py_wrap_error, wrap_error, ToPythonError}; -pub use result_data::{PyQpuResultData, PyReadoutValues}; +pub use result_data::{PyQpuResultData, PyReadoutValues, RawQpuReadoutData}; pub mod api; pub mod isa; @@ -17,6 +17,7 @@ use crate::py_sync::py_function_sync_async; create_init_submodule! { classes: [ PyQpuResultData, + RawQpuReadoutData, PyReadoutValues ], errors: [ diff --git a/crates/python/src/qpu/result_data.rs b/crates/python/src/qpu/result_data.rs index c95e434d5..6b6388655 100644 --- a/crates/python/src/qpu/result_data.rs +++ b/crates/python/src/qpu/result_data.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use pyo3::{ - pymethods, - types::{PyComplex, PyFloat, PyInt}, - Py, PyResult, Python, + pyclass, 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}; @@ -43,4 +43,44 @@ impl PyQpuResultData { fn readout_values(&self, py: Python<'_>) -> PyResult> { self.as_inner().readout_values().to_python(py) } + + pub(crate) fn to_raw_readout_data(&self, py: Python<'_>) -> RawQpuReadoutData { + RawQpuReadoutData { + mappings: self.as_inner().mappings().clone(), + readout_values: self + .as_inner() + .readout_values() + .iter() + .map(|(register, values)| { + ( + register.to_string(), + match values { + ReadoutValues::Integer(values) => PyList::new(py, values).into_py(py), + ReadoutValues::Real(values) => PyList::new(py, values).into_py(py), + ReadoutValues::Complex(values) => PyList::new(py, values).into_py(py), + }, + ) + }) + .collect::>>(), + } + } +} + +/// A wrapper type for data returned by the QPU in a more flat structure than +/// [`PyQpuResultData`] offers. This makes it more convenient to work with +/// the data if you don't care what type of number the readout values for +/// each register contains. +#[derive(Debug)] +#[pyclass(name = "RawQPUReadoutData")] +pub struct RawQpuReadoutData { + #[pyo3(get)] + mappings: HashMap, + #[pyo3(get)] + readout_values: HashMap>, +} + +impl RawQpuReadoutData { + fn __repr__(&self) -> String { + format!("{self:?}") + } } diff --git a/crates/python/src/qvm/mod.rs b/crates/python/src/qvm/mod.rs index 064122f43..546b8673e 100644 --- a/crates/python/src/qvm/mod.rs +++ b/crates/python/src/qvm/mod.rs @@ -1,3 +1,4 @@ +use pyo3::types::PyList; use qcs::{ qvm::{QvmOptions, QvmResultData}, RegisterData, @@ -16,15 +17,8 @@ mod api; use api::PyAddressRequest; -wrap_error!(RustQvmError(qcs::qvm::Error)); -py_wrap_error!(api, RustQvmError, QVMError, PyRuntimeError); - -py_wrap_type! { - PyQvmResultData(QvmResultData) as "QVMResultData" -} - create_init_submodule! { - classes: [PyQvmResultData, PyQvmOptions], + classes: [PyQvmResultData, PyQvmOptions, RawQvmReadoutData], errors: [QVMError], funcs: [py_run, py_run_async], submodules: [ @@ -32,8 +26,24 @@ create_init_submodule! { ], } +wrap_error!(RustQvmError(qcs::qvm::Error)); +py_wrap_error!(api, RustQvmError, QVMError, PyRuntimeError); + +py_wrap_type! { + PyQvmResultData(QvmResultData) as "QVMResultData" +} + #[pymethods] impl PyQvmResultData { + #[new] + fn new(memory: HashMap) -> Self { + let memory = memory + .into_iter() + .map(|(key, value)| (key, value.into_inner())) + .collect(); + Self::from(QvmResultData::from_memory_map(memory)) + } + #[staticmethod] fn from_memory_map(py: Python<'_>, memory: HashMap) -> PyResult { Ok(Self(QvmResultData::from_memory_map(HashMap::< @@ -48,6 +58,40 @@ impl PyQvmResultData { fn memory(&self, py: Python<'_>) -> PyResult> { self.as_inner().memory().to_python(py) } + + pub(crate) fn to_raw_readout_data(&self, py: Python<'_>) -> RawQvmReadoutData { + RawQvmReadoutData { + memory: self + .as_inner() + .memory() + .iter() + .map(|(register, matrix)| { + ( + register.to_string(), + match matrix { + RegisterData::I8(matrix) => PyList::new(py, matrix).into_py(py), + RegisterData::F64(matrix) => PyList::new(py, matrix).into_py(py), + RegisterData::I16(matrix) => PyList::new(py, matrix).into_py(py), + RegisterData::Complex32(matrix) => PyList::new(py, matrix).into_py(py), + }, + ) + }) + .collect::>>(), + } + } +} + +#[pyclass(name = "RawQVMReadoutData")] +#[derive(Debug)] +pub(crate) struct RawQvmReadoutData { + #[pyo3(get)] + memory: HashMap>, +} + +impl RawQvmReadoutData { + fn __repr__(&self) -> String { + format!("{self:?}") + } } py_wrap_type! { diff --git a/crates/python/src/register_data.rs b/crates/python/src/register_data.rs index 9f6fbc1ac..b045a3f73 100644 --- a/crates/python/src/register_data.rs +++ b/crates/python/src/register_data.rs @@ -1,9 +1,11 @@ +use numpy::PyArray; use pyo3::{ + pymethods, types::{PyComplex, PyFloat, PyInt}, - Py, + Py, PyErr, PyObject, PyResult, Python, ToPyObject, }; use qcs::RegisterData; -use rigetti_pyo3::py_wrap_union_enum; +use rigetti_pyo3::{py_wrap_union_enum, PyWrapper}; py_wrap_union_enum! { PyRegisterData(RegisterData) as "RegisterData" { @@ -13,3 +15,24 @@ py_wrap_union_enum! { complex32: Complex32 => Vec>> } } + +#[pymethods] +impl PyRegisterData { + pub fn as_ndarray(&self, py: Python<'_>) -> PyResult { + match self.as_inner() { + RegisterData::I8(matrix) => { + PyArray::from_vec2(py, matrix.as_slice()).map(|arr| arr.to_object(py)) + } + RegisterData::F64(matrix) => { + PyArray::from_vec2(py, matrix.as_slice()).map(|arr| arr.to_object(py)) + } + RegisterData::I16(matrix) => { + PyArray::from_vec2(py, matrix.as_slice()).map(|arr| arr.to_object(py)) + } + RegisterData::Complex32(matrix) => { + PyArray::from_vec2(py, matrix.as_slice()).map(|arr| arr.to_object(py)) + } + } + .map_err(PyErr::from) + } +} diff --git a/crates/python/tests/execution_data/test_execution_data.py b/crates/python/tests/execution_data/test_execution_data.py index 08f73251a..f0901bf95 100644 --- a/crates/python/tests/execution_data/test_execution_data.py +++ b/crates/python/tests/execution_data/test_execution_data.py @@ -64,9 +64,7 @@ def test_integer(self): register_matrix = RegisterMatrix.from_integer(m) assert register_matrix.is_integer() register_matrix = register_matrix.as_integer() - assert ( - register_matrix is not None - ), "register_matrix should be an integer matrix" + assert register_matrix is not None, "register_matrix should be an integer matrix" assert_array_equal(register_matrix, m) def test_real(self): @@ -89,3 +87,21 @@ def test_complex(self): register_matrix = register_matrix.as_complex() assert register_matrix is not None, "register_matrix should be a complex matrix" assert_array_equal(register_matrix, m) + + +class TestRegisterMap: + def test_iter(self): + memory_map = { + "ro": RegisterData.from_i16([[0, 1, 2], [1, 2, 3]]), + "foo": RegisterData.from_i16([[0, 1, 2], [1, 2, 3]]), + } + qvm_result_data = QVMResultData.from_memory_map(memory_map) + result_data = ResultData.from_qvm(qvm_result_data) + register_map = result_data.to_register_map() + expected_keys = {"ro", "foo"} + actual_keys = set() + for key, matrix in register_map.items(): + actual_keys.add(key) + assert np.all(matrix.to_ndarray() == np.matrix([[0, 1, 2], [1, 2, 3]])) + + assert expected_keys == actual_keys == set(register_map.keys()) diff --git a/crates/python/tests/qpu/test_qpu.py b/crates/python/tests/qpu/test_qpu.py index 1478b6c1c..7f64f5b04 100644 --- a/crates/python/tests/qpu/test_qpu.py +++ b/crates/python/tests/qpu/test_qpu.py @@ -8,6 +8,7 @@ list_quantum_processors_async, ) + def test_readout_values(): inner = [0, 1] readout_values = ReadoutValues.from_integer(inner) @@ -23,12 +24,15 @@ def test_readout_values(): def test_qpu_result_data(): - mappings = { "a": "_q0" } - readout_values = { "a": ReadoutValues.from_integer([0, 1]) } + mappings = {"a": "_q0"} + readout_values = {"a": ReadoutValues.from_integer([0, 1])} result_data = QPUResultData(mappings, readout_values) assert result_data.mappings == mappings assert result_data.readout_values["a"].as_integer() == readout_values["a"].as_integer() + raw_data = result_data.to_raw_readout_data() + assert raw_data.mappings == {"a": "_q0"} + assert raw_data.readout_values == {"a": [0, 1]} @pytest.mark.qcs_session @@ -47,4 +51,3 @@ def test_list_quantum_processors_timeout(): async def test_list_quantum_processors_async(): quantum_processor_ids = await list_quantum_processors_async() assert len(quantum_processor_ids) > 0 - diff --git a/crates/python/tests/qvm/test_qvm.py b/crates/python/tests/qvm/test_qvm.py new file mode 100644 index 000000000..c3ca7a959 --- /dev/null +++ b/crates/python/tests/qvm/test_qvm.py @@ -0,0 +1,8 @@ +from qcs_sdk import RegisterData +from qcs_sdk.qvm import QVMResultData + + +def test_qvm_result_data(): + register_data = RegisterData.from_i8([[1, 2, 3], [4, 5, 6]]) + raw_data = QVMResultData({"ro": register_data}).to_raw_readout_data() + assert raw_data.memory == {"ro": [[1, 2, 3], [4, 5, 6]]} diff --git a/deny.toml b/deny.toml index 8c34ee53e..b925716b4 100644 --- a/deny.toml +++ b/deny.toml @@ -24,7 +24,11 @@ yanked = "deny" notice = "deny" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. -ignore = [] +ignore = [ + "RUSTSEC-2023-0052", # Introduced by transitive dependency `webpki`. + # `hyper-proxy`, then `qcs-api-client-rust` need to update in order to remove + # `webpki`. +] # This section is considered when running `cargo deny check licenses` # More documentation for the licenses section can be found here: