From 7bf58b086bd9a94bb1639d9e15ba2715b6bf07bd Mon Sep 17 00:00:00 2001 From: Marquess Valdez Date: Mon, 29 Jul 2024 08:37:08 -0500 Subject: [PATCH 1/3] feat(python): ConnectionStrategy supports the pickle module --- crates/python/src/qpu/api.rs | 74 ++++++++++++++++++++++------- crates/python/tests/qpu/test_api.py | 15 ++++++ 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/crates/python/src/qpu/api.rs b/crates/python/src/qpu/api.rs index 7a62dcbe..088a4da9 100644 --- a/crates/python/src/qpu/api.rs +++ b/crates/python/src/qpu/api.rs @@ -8,8 +8,8 @@ use pyo3::{ pyclass, pyclass::CompareOp, pyfunction, pymethods, - types::{PyComplex, PyDict, PyInt}, - IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, + types::{PyComplex, PyInt, PyTuple}, + IntoPy, Py, PyObject, PyResult, Python, ToPyObject, }; use qcs::qpu::api::{ ApiExecutionOptions, ApiExecutionOptionsBuilder, ConnectionStrategy, ExecutionOptions, @@ -382,6 +382,7 @@ py_function_sync_async! { py_wrap_type! { #[derive(Debug, Default)] + #[pyo3(module = "qcs_sdk.qpu.api")] PyExecutionOptions(ExecutionOptions) as "ExecutionOptions" } impl_repr!(PyExecutionOptions); @@ -432,21 +433,22 @@ impl PyExecutionOptions { } } - fn __getstate__(&self, py: Python<'_>) -> PyResult> { - let dict = PyDict::new(py); - dict.set_item("connection_strategy", self.connection_strategy())?; - dict.set_item("timeout_seconds", self.timeout_seconds())?; - dict.set_item("api_options", self.api_options())?; - Ok(dict.into()) - } - - fn __setstate__(&mut self, py: Python<'_>, state: Py) -> PyResult<()> { - *self = Self::_from_parts( - state.getattr(py, "connection_strategy")?.extract(py)?, - state.getattr(py, "timeout_seconds")?.extract(py)?, - state.getattr(py, "api_options")?.extract(py)?, - )?; - Ok(()) + fn __reduce__<'py>(&mut self, py: Python<'py>) -> PyResult<&'py PyTuple> { + let callable = py.get_type::().getattr("_from_parts")?; + Ok(PyTuple::new( + py, + [ + callable, + PyTuple::new( + py, + &[ + self.connection_strategy().into_py(py), + self.timeout_seconds().into_py(py), + self.api_options().into_py(py), + ], + ), + ], + )) } #[staticmethod] @@ -575,6 +577,7 @@ impl PyApiExecutionOptionsBuilder { py_wrap_type! { #[derive(Default)] + #[pyo3(module = "qcs_sdk.qpu.api")] PyConnectionStrategy(ConnectionStrategy) as "ConnectionStrategy" } impl_repr!(PyConnectionStrategy); @@ -608,4 +611,41 @@ impl PyConnectionStrategy { _ => py.NotImplemented(), } } + + // // Implementing this method is required for the pickle module to recognize the class as + // // pickleable, but __reduce__ is self sufficient, so we can just return an empty dictionary here. + // fn __getstate__<'py>(&'py self, py: Python<'py>) -> &'py PyDict { + // PyDict::new(py) + // } + // + fn __reduce__(&self, py: Python<'_>) -> PyResult { + Ok(match self.as_inner() { + ConnectionStrategy::Gateway => PyTuple::new( + py, + &[ + py.get_type::().getattr("gateway")?.to_object(py), + PyTuple::empty(py).to_object(py), + ], + ) + .to_object(py), + ConnectionStrategy::DirectAccess => PyTuple::new( + py, + &[ + py.get_type::() + .getattr("direct_access")? + .to_object(py), + PyTuple::empty(py).to_object(py), + ], + ) + .to_object(py), + ConnectionStrategy::EndpointId(endpoint_id) => PyTuple::new( + py, + &[ + py.get_type::().getattr("endpoint_id")?.to_object(py), + PyTuple::new(py, [endpoint_id]).to_object(py), + ], + ) + .to_object(py), + }) + } } diff --git a/crates/python/tests/qpu/test_api.py b/crates/python/tests/qpu/test_api.py index da51e2e8..988708e3 100644 --- a/crates/python/tests/qpu/test_api.py +++ b/crates/python/tests/qpu/test_api.py @@ -1,3 +1,4 @@ +import pickle import pytest from qcs_sdk.qpu.translation import ( @@ -5,6 +6,8 @@ ) from qcs_sdk.qpu.api import ( + ConnectionStrategy, + ExecutionOptions, Register, retrieve_results, submit, @@ -44,3 +47,15 @@ def test_submit_retrieve( job_id = submit(program, memory, quantum_processor_id) results = retrieve_results(job_id) + +class TestPickle(): + @pytest.mark.parametrize("strategy", [ConnectionStrategy.gateway(), ConnectionStrategy.direct_access(), ConnectionStrategy.endpoint_id("endpoint_id")]) + def test_connection_strategy(self, strategy: ConnectionStrategy): + pickled = pickle.dumps(strategy) + unpickled = pickle.loads(pickled) + + def test_execution_options(self): + options = ExecutionOptions.default() + pickled = pickle.dumps(options) + unpickled = pickle.loads(pickled) + assert unpickled == options From 47f53990b64195d201513aa65ba4e03d1e85dd8a Mon Sep 17 00:00:00 2001 From: Marquess Valdez Date: Mon, 29 Jul 2024 09:36:02 -0700 Subject: [PATCH 2/3] less constricted quil requirement --- crates/python/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/python/pyproject.toml b/crates/python/pyproject.toml index 09efe1c8..5595bc79 100644 --- a/crates/python/pyproject.toml +++ b/crates/python/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Operating System :: OS Independent", ] -dependencies = ["quil==0.11.1"] +dependencies = ["quil>=0.11.2"] # PEP 621 specifies the [project] table as the source for project metadata. However, Poetry only supports [tool.poetry] # We can remove this table once this issue is resolved: https://github.com/python-poetry/poetry/issues/3332 From b0a867a45019f96522e3622081a8b173f327ef15 Mon Sep 17 00:00:00 2001 From: Marquess Valdez Date: Mon, 29 Jul 2024 09:50:21 -0700 Subject: [PATCH 3/3] fix missing assert, remove stale code --- crates/python/src/qpu/api.rs | 6 ------ crates/python/tests/qpu/test_api.py | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/python/src/qpu/api.rs b/crates/python/src/qpu/api.rs index 088a4da9..fc5050f1 100644 --- a/crates/python/src/qpu/api.rs +++ b/crates/python/src/qpu/api.rs @@ -612,12 +612,6 @@ impl PyConnectionStrategy { } } - // // Implementing this method is required for the pickle module to recognize the class as - // // pickleable, but __reduce__ is self sufficient, so we can just return an empty dictionary here. - // fn __getstate__<'py>(&'py self, py: Python<'py>) -> &'py PyDict { - // PyDict::new(py) - // } - // fn __reduce__(&self, py: Python<'_>) -> PyResult { Ok(match self.as_inner() { ConnectionStrategy::Gateway => PyTuple::new( diff --git a/crates/python/tests/qpu/test_api.py b/crates/python/tests/qpu/test_api.py index 988708e3..4d7c8a1c 100644 --- a/crates/python/tests/qpu/test_api.py +++ b/crates/python/tests/qpu/test_api.py @@ -53,6 +53,7 @@ class TestPickle(): def test_connection_strategy(self, strategy: ConnectionStrategy): pickled = pickle.dumps(strategy) unpickled = pickle.loads(pickled) + assert unpickled == strategy def test_execution_options(self): options = ExecutionOptions.default()