Skip to main content

qcs_sdk/
executable.rs

1use std::{num::NonZeroU16, sync::Arc};
2
3use opentelemetry::trace::FutureExt;
4use pyo3::{pyclass, FromPyObject};
5use qcs::{Error, Executable, ExecutionData, JobHandle, Service};
6use rigetti_pyo3::{
7    impl_as_mut_for_wrapper, py_async, py_sync, py_wrap_error, py_wrap_simple_enum, py_wrap_type,
8    pyo3::{exceptions::PyRuntimeError, pymethods, types::PyDict, Py, PyAny, PyResult, Python},
9    wrap_error, PyWrapper, ToPython, ToPythonError,
10};
11use tokio::sync::Mutex;
12use tracing::instrument;
13
14use crate::{
15    compiler::quilc::{PyCompilerOpts, PyQuilcClient},
16    execution_data::PyExecutionData,
17    qpu::{api::PyExecutionOptions, translation::PyTranslationOptions},
18};
19
20wrap_error!(RustExecutionError(Error));
21py_wrap_error!(
22    executable,
23    RustExecutionError,
24    ExecutionError,
25    PyRuntimeError
26);
27
28// Because Python is garbage-collected, no lifetimes can be guaranteed except `'static`.
29//
30// `Arc<Mutex<>>` to work around https://github.com/awestlake87/pyo3-asyncio/issues/50
31py_wrap_type! {
32    PyExecutable(Arc<Mutex<Executable<'static, 'static>>>) as "Executable";
33}
34
35impl_as_mut_for_wrapper!(PyExecutable);
36
37#[pyclass]
38#[pyo3(name = "ExeParameter")]
39#[derive(FromPyObject)]
40pub struct PyParameter {
41    #[pyo3(get, set)]
42    pub name: String,
43    #[pyo3(get, set)]
44    pub index: usize,
45    #[pyo3(get, set)]
46    pub value: f64,
47}
48
49#[pymethods]
50impl PyParameter {
51    #[new]
52    pub fn new(name: String, index: usize, value: f64) -> Self {
53        Self { name, index, value }
54    }
55}
56
57/// Invoke a PyExecutable's inner Executable::method with given arguments,
58/// then mapped to `Future<Output = Result<PyExecutionData, ExecutionError>>`
59macro_rules! py_executable_data {
60    ($self: ident, $method: ident $(, $arg: expr)* $(,)?) => {{
61        let arc = $self.as_inner().clone();
62        async move {
63            arc.lock()
64                .await
65                .$method($($arg),*)
66                .await
67                .map(ExecutionData::from)
68                .map(PyExecutionData::from)
69                .map_err(RustExecutionError::from)
70                .map_err(RustExecutionError::to_py_err)
71        }.with_current_context()
72    }};
73}
74
75/// Invoke a PyExecutable's inner Executable::method with given arguments,
76/// then mapped to `Future<Output = Result<PyJobHandle, ExecutionError>>`
77macro_rules! py_job_handle {
78    ($self: ident, $method: ident $(, $arg: expr)* $(,)?) => {{
79        let arc = $self.as_inner().clone();
80        async move {
81            arc.lock()
82                .await
83                .$method($($arg),*)
84                .await
85                .map(JobHandle::from)
86                .map(PyJobHandle::from)
87                .map_err(RustExecutionError::from)
88                .map_err(RustExecutionError::to_py_err)
89        }
90        .with_current_context()
91    }};
92}
93
94#[pyo3_opentelemetry::pypropagate(exclude(new), on_context_extraction_failure = "ignore")]
95#[pymethods]
96impl PyExecutable {
97    #[new]
98    #[pyo3(signature = (
99        quil,
100        /,
101        registers = Vec::new(),
102        parameters = Vec::new(),
103        shots = None,
104        quilc_client = None,
105        compiler_options = None,
106    ))]
107    pub fn new(
108        quil: String,
109        registers: Vec<String>,
110        parameters: Vec<PyParameter>,
111        #[pyo3(from_py_with = "crate::from_py::optional_non_zero_u16")] shots: Option<NonZeroU16>,
112        quilc_client: Option<PyQuilcClient>,
113        compiler_options: Option<PyCompilerOpts>,
114    ) -> Self {
115        let quilc_client = quilc_client.map(|c| c.inner);
116        let mut exe = Executable::from_quil(quil).with_quilc_client(quilc_client);
117
118        for reg in registers {
119            exe = exe.read_from(reg);
120        }
121
122        for param in parameters {
123            exe.with_parameter(param.name, param.index, param.value);
124        }
125
126        if let Some(shots) = shots {
127            exe = exe.with_shots(shots);
128        }
129
130        if let Some(options) = compiler_options {
131            exe = exe.compiler_options(options.into_inner());
132        }
133
134        Self::from(Arc::new(Mutex::new(exe)))
135    }
136
137    #[instrument(skip_all)]
138    pub fn execute_on_qvm(
139        &self,
140        py: Python<'_>,
141        client: crate::qvm::PyQvmClient,
142    ) -> PyResult<PyExecutionData> {
143        py_sync!(py, py_executable_data!(self, execute_on_qvm, &client))
144    }
145
146    #[instrument(skip_all)]
147    pub fn execute_on_qvm_async<'py>(
148        &'py self,
149        py: Python<'py>,
150        client: crate::qvm::PyQvmClient,
151    ) -> PyResult<&PyAny> {
152        py_async!(py, py_executable_data!(self, execute_on_qvm, &client))
153    }
154
155    #[pyo3(signature = (quantum_processor_id, endpoint_id = None, translation_options = None, execution_options = None))]
156    pub fn execute_on_qpu(
157        &self,
158        py: Python<'_>,
159        quantum_processor_id: String,
160        endpoint_id: Option<String>,
161        translation_options: Option<PyTranslationOptions>,
162        execution_options: Option<PyExecutionOptions>,
163    ) -> PyResult<PyExecutionData> {
164        let translation_options = translation_options.map(|opts| opts.as_inner().clone());
165        match endpoint_id {
166            Some(endpoint_id) => py_sync!(
167                py,
168                py_executable_data!(
169                    self,
170                    execute_on_qpu_with_endpoint,
171                    quantum_processor_id,
172                    endpoint_id,
173                    translation_options,
174                )
175            ),
176            None => py_sync!(
177                py,
178                py_executable_data!(
179                    self,
180                    execute_on_qpu,
181                    quantum_processor_id,
182                    translation_options,
183                    execution_options.unwrap_or_default().as_inner(),
184                )
185            ),
186        }
187    }
188
189    #[pyo3(signature = (quantum_processor_id, endpoint_id = None, translation_options = None, execution_options = None))]
190    pub fn execute_on_qpu_async<'py>(
191        &'py self,
192        py: Python<'py>,
193        quantum_processor_id: String,
194        endpoint_id: Option<String>,
195        translation_options: Option<PyTranslationOptions>,
196        execution_options: Option<PyExecutionOptions>,
197    ) -> PyResult<&PyAny> {
198        let translation_options = translation_options.map(|opts| opts.as_inner().clone());
199        match endpoint_id {
200            Some(endpoint_id) => py_async!(
201                py,
202                py_executable_data!(
203                    self,
204                    execute_on_qpu_with_endpoint,
205                    quantum_processor_id,
206                    endpoint_id,
207                    translation_options,
208                )
209            ),
210            None => py_async!(
211                py,
212                py_executable_data!(
213                    self,
214                    execute_on_qpu,
215                    quantum_processor_id,
216                    translation_options,
217                    execution_options.unwrap_or_default().as_inner(),
218                )
219            ),
220        }
221    }
222
223    #[pyo3(signature = (quantum_processor_id, endpoint_id = None, translation_options = None, execution_options = None))]
224    pub fn submit_to_qpu(
225        &self,
226        py: Python<'_>,
227        quantum_processor_id: String,
228        endpoint_id: Option<String>,
229        translation_options: Option<PyTranslationOptions>,
230        execution_options: Option<PyExecutionOptions>,
231    ) -> PyResult<PyJobHandle> {
232        let translation_options = translation_options.map(|opts| opts.as_inner().clone());
233        match endpoint_id {
234            Some(endpoint_id) => py_sync!(
235                py,
236                py_job_handle!(
237                    self,
238                    submit_to_qpu_with_endpoint,
239                    quantum_processor_id,
240                    endpoint_id,
241                    translation_options,
242                )
243            ),
244            None => py_sync!(
245                py,
246                py_job_handle!(
247                    self,
248                    submit_to_qpu,
249                    quantum_processor_id,
250                    translation_options,
251                    execution_options.unwrap_or_default().as_inner(),
252                )
253            ),
254        }
255    }
256
257    #[pyo3(signature = (quantum_processor_id, endpoint_id = None, translation_options = None, execution_options = None))]
258    pub fn submit_to_qpu_async<'py>(
259        &'py self,
260        py: Python<'py>,
261        quantum_processor_id: String,
262        endpoint_id: Option<String>,
263        translation_options: Option<PyTranslationOptions>,
264        execution_options: Option<PyExecutionOptions>,
265    ) -> PyResult<&PyAny> {
266        let translation_options = translation_options.map(|opts| opts.as_inner().clone());
267        match endpoint_id {
268            Some(endpoint_id) => {
269                py_async!(
270                    py,
271                    py_job_handle!(
272                        self,
273                        submit_to_qpu_with_endpoint,
274                        quantum_processor_id,
275                        endpoint_id,
276                        translation_options,
277                    )
278                )
279            }
280            None => py_async!(
281                py,
282                py_job_handle!(
283                    self,
284                    submit_to_qpu,
285                    quantum_processor_id,
286                    translation_options,
287                    execution_options.unwrap_or_default().as_inner(),
288                )
289            ),
290        }
291    }
292
293    pub fn retrieve_results(
294        &mut self,
295        py: Python<'_>,
296        job_handle: PyJobHandle,
297    ) -> PyResult<PyExecutionData> {
298        py_sync!(
299            py,
300            py_executable_data!(self, retrieve_results, job_handle.into())
301        )
302    }
303
304    pub fn retrieve_results_async<'py>(
305        &'py mut self,
306        py: Python<'py>,
307        job_handle: PyJobHandle,
308    ) -> PyResult<&PyAny> {
309        py_async!(
310            py,
311            py_executable_data!(self, retrieve_results, job_handle.into())
312        )
313    }
314}
315
316py_wrap_simple_enum! {
317    PyService(Service) as "Service" {
318        Quilc as Quilc,
319        Qvm as QVM,
320        Qcs as QCS,
321        Qpu as QPU
322    }
323}
324
325py_wrap_type! {
326    PyJobHandle(JobHandle<'static>) as "JobHandle";
327}
328
329#[pymethods]
330impl PyJobHandle {
331    #[getter]
332    pub fn job_id(&self) -> String {
333        self.as_inner().job_id().to_string()
334    }
335
336    #[getter]
337    pub fn readout_map(&self, py: Python) -> PyResult<Py<PyDict>> {
338        self.as_inner().readout_map().to_python(py)
339    }
340}