Skip to main content

qcs_sdk/qpu/
api.rs

1//! Running programs on a QPU.
2use std::collections::HashMap;
3use std::time::Duration;
4
5use numpy::Complex32;
6use pyo3::{
7    exceptions::{PyRuntimeError, PyValueError},
8    pyclass,
9    pyclass::CompareOp,
10    pyfunction, pymethods,
11    types::{PyComplex, PyInt, PyTuple},
12    IntoPy, Py, PyObject, PyResult, Python, ToPyObject,
13};
14use qcs::qpu::api::{
15    ApiExecutionOptions, ApiExecutionOptionsBuilder, ConnectionStrategy, ExecutionOptions,
16    ExecutionOptionsBuilder, QpuApiDuration,
17};
18use qcs_api_client_grpc::models::controller::{
19    data_value, readout_values, ControllerJobExecutionResult,
20};
21use rigetti_pyo3::{
22    create_init_submodule, impl_as_mut_for_wrapper, impl_repr, num_complex, py_wrap_error,
23    py_wrap_type, py_wrap_union_enum, wrap_error, PyWrapper, ToPythonError,
24};
25
26use crate::client::PyQcsClient;
27
28use super::result_data::PyMemoryValues;
29
30create_init_submodule! {
31    classes: [
32        PyRegister,
33        ExecutionResult,
34        ExecutionResults,
35        PyConnectionStrategy,
36        PyExecutionOptions,
37        PyExecutionOptionsBuilder,
38        PyApiExecutionOptions,
39        PyApiExecutionOptionsBuilder,
40        PyQpuApiDuration
41    ],
42    errors: [
43        SubmissionError,
44        QpuApiError
45    ],
46    funcs: [
47        py_submit,
48        py_submit_async,
49        py_submit_with_parameter_batch,
50        py_submit_with_parameter_batch_async,
51        py_cancel_job,
52        py_cancel_job_async,
53        py_cancel_jobs,
54        py_cancel_jobs_async,
55        py_retrieve_results,
56        py_retrieve_results_async
57    ],
58}
59
60/// Errors that may occur when submitting a program for execution
61#[derive(Debug, thiserror::Error)]
62enum RustSubmissionError {
63    /// An API error occurred
64    #[error("An API error occurred: {0}")]
65    QpuApiError(#[from] qcs::qpu::api::QpuApiError),
66
67    /// Job could not be deserialized
68    #[error("Failed to deserialize job: {0}")]
69    DeserializeError(#[from] serde_json::Error),
70}
71
72py_wrap_error!(runner, RustSubmissionError, SubmissionError, PyRuntimeError);
73
74crate::py_sync::py_function_sync_async! {
75    /// Submits an executable `program` to be run on the specified QPU
76    ///
77    /// # Errors
78    ///
79    /// May return an error if
80    /// * an engagement is not available
81    /// * an RPCQ client cannot be built
82    /// * the program cannot be submitted
83    #[pyfunction]
84    #[pyo3(signature = (program, patch_values, quantum_processor_id = None, client = None, execution_options = None))]
85    #[pyo3_opentelemetry::pypropagate(on_context_extraction_failure="ignore")]
86    async fn submit(
87        program: String,
88        patch_values: HashMap<String, Vec<f64>>,
89        quantum_processor_id: Option<String>,
90        client: Option<PyQcsClient>,
91        execution_options: Option<PyExecutionOptions>,
92    ) -> PyResult<String> {
93        let client = PyQcsClient::get_or_create_client(client);
94
95        // Is there a better way to map these patch_values keys? This
96        // negates the whole purpose of [`submit`] using `Box<str>`,
97        // instead of `String` directly, which normally would decrease
98        // copies _and_ require less space, since str can't be extended.
99        let patch_values = patch_values
100            .into_iter()
101            .map(|(k, v)| (k.into_boxed_str(), v))
102            .collect();
103
104        let job = serde_json::from_str(&program)
105            .map_err(RustSubmissionError::from)
106            .map_err(RustSubmissionError::to_py_err)?;
107
108        let job_id = qcs::qpu::api::submit(quantum_processor_id.as_deref(), job, &patch_values, &client, execution_options.unwrap_or_default().as_inner()).await
109            .map_err(RustSubmissionError::from)
110            .map_err(RustSubmissionError::to_py_err)?;
111
112        Ok(job_id.to_string())
113    }
114}
115
116crate::py_sync::py_function_sync_async! {
117    #[pyfunction]
118    #[pyo3(signature = (program, patch_values, quantum_processor_id = None, client = None, execution_options = None))]
119    #[pyo3_opentelemetry::pypropagate(on_context_extraction_failure="ignore")]
120    async fn submit_with_parameter_batch(
121        program: String,
122        patch_values: Vec<HashMap<String, Vec<f64>>>,
123        quantum_processor_id: Option<String>,
124        client: Option<PyQcsClient>,
125        execution_options: Option<PyExecutionOptions>,
126    ) -> PyResult<Vec<String>> {
127        let client = PyQcsClient::get_or_create_client(client);
128
129        let patch_values: Vec<HashMap<Box<str>, Vec<f64>>> = patch_values
130            .into_iter()
131            .map(|m| m.into_iter().map(|(k, v)| (k.into_boxed_str(), v)).collect())
132            .collect();
133
134        let job = serde_json::from_str(&program)
135            .map_err(RustSubmissionError::from)
136            .map_err(RustSubmissionError::to_py_err)?;
137
138        Ok(qcs::qpu::api::submit_with_parameter_batch(quantum_processor_id.as_deref(), job, &patch_values, &client, execution_options.unwrap_or_default().as_inner()).await
139            .map_err(RustSubmissionError::from)
140            .map_err(RustSubmissionError::to_py_err)?
141            .into_iter()
142            .map(|id| id.to_string())
143            .collect())
144    }
145}
146
147wrap_error!(RustQpuApiError(qcs::qpu::api::QpuApiError));
148py_wrap_error!(runner, RustQpuApiError, QpuApiError, PyRuntimeError);
149
150/// Variants of data vectors within a single ExecutionResult.
151#[derive(Clone, Debug)]
152pub enum Register {
153    I32(Vec<i32>),
154    Complex32(Vec<Complex32>),
155}
156
157py_wrap_union_enum! {
158    #[derive(Debug)]
159    PyRegister(Register) as "Register" {
160        i32: I32 => Vec<Py<PyInt>>,
161        complex32: Complex32 => Vec<Py<PyComplex>>
162    }
163}
164
165/// The execution readout data from a particular memory location.
166#[pyclass]
167#[derive(Clone, Debug)]
168pub struct ExecutionResult {
169    /// Describes result shape dimensions.
170    #[pyo3(get)]
171    pub shape: [usize; 2],
172    /// Register data for this result.
173    #[pyo3(get)]
174    pub data: PyRegister,
175    /// Name of the data type.
176    #[pyo3(get)]
177    pub dtype: String,
178}
179
180impl From<readout_values::Values> for ExecutionResult {
181    fn from(values: readout_values::Values) -> Self {
182        match values {
183            readout_values::Values::ComplexValues(cs) => ExecutionResult {
184                shape: [cs.values.len(), 1],
185                dtype: "complex".into(),
186                data: Register::Complex32(
187                    cs.values
188                        .into_iter()
189                        .map(|c| num_complex::Complex32::new(c.real, c.imaginary))
190                        .collect(),
191                )
192                .into(),
193            },
194            readout_values::Values::IntegerValues(ns) => ExecutionResult {
195                shape: [ns.values.len(), 1],
196                dtype: "integer".into(),
197                data: Register::I32(ns.values).into(),
198            },
199        }
200    }
201}
202
203#[pymethods]
204impl ExecutionResult {
205    #[staticmethod]
206    fn from_register(register: PyRegister) -> Self {
207        match register.as_inner() {
208            Register::I32(values) => ExecutionResult {
209                shape: [values.len(), 1],
210                dtype: "integer".into(),
211                data: register,
212            },
213            Register::Complex32(values) => ExecutionResult {
214                shape: [values.len(), 1],
215                dtype: "complex".into(),
216                data: register,
217            },
218        }
219    }
220}
221
222#[pyclass]
223#[derive(Clone, Debug)]
224pub struct ExecutionResults {
225    /// Result data buffers keyed by readout alias name.
226    #[pyo3(get)]
227    pub buffers: HashMap<String, ExecutionResult>,
228    /// QPU execution duration.
229    #[pyo3(get)]
230    pub execution_duration_microseconds: Option<u64>,
231    #[pyo3(get)]
232    pub memory: HashMap<String, PyMemoryValues>,
233}
234
235#[pymethods]
236impl ExecutionResults {
237    #[new]
238    fn new(
239        buffers: HashMap<String, ExecutionResult>,
240        memory: HashMap<String, PyMemoryValues>,
241        execution_duration_microseconds: Option<u64>,
242    ) -> Self {
243        Self {
244            buffers,
245            execution_duration_microseconds,
246            memory,
247        }
248    }
249}
250
251impl ExecutionResults {
252    fn from_controller_job_execution_result(
253        py: Python<'_>,
254        result: ControllerJobExecutionResult,
255    ) -> PyResult<Self> {
256        let buffers = result
257            .readout_values
258            .into_iter()
259            .filter_map(|(key, val)| val.values.map(|values| (key, values.into())))
260            .collect();
261
262        let memory = result.memory_values.iter().try_fold(
263            HashMap::with_capacity(result.memory_values.len()),
264            |mut acc, (key, value)| -> PyResult<HashMap<_, _>> {
265                if let Some(value) = &value.value {
266                    acc.insert(
267                        key.clone(),
268                        match value {
269                            data_value::Value::Binary(value) => PyMemoryValues::from_binary(
270                                py,
271                                value
272                                    .data
273                                    .iter()
274                                    .map(|v| v.to_object(py).extract(py))
275                                    .collect::<PyResult<Vec<_>>>()?,
276                            )?,
277                            data_value::Value::Integer(value) => PyMemoryValues::from_integer(
278                                py,
279                                value
280                                    .data
281                                    .iter()
282                                    .map(|v| v.to_object(py).extract(py))
283                                    .collect::<PyResult<Vec<_>>>()?,
284                            )?,
285                            data_value::Value::Real(value) => PyMemoryValues::from_real(
286                                py,
287                                value
288                                    .data
289                                    .iter()
290                                    .map(|v| v.to_object(py).extract(py))
291                                    .collect::<PyResult<Vec<_>>>()?,
292                            )?,
293                        },
294                    )
295                } else {
296                    None
297                };
298
299                Ok(acc)
300            },
301        )?;
302
303        Ok(Self {
304            buffers,
305            execution_duration_microseconds: Some(result.execution_duration_microseconds),
306            memory,
307        })
308    }
309}
310
311crate::py_sync::py_function_sync_async! {
312    #[pyfunction]
313    #[pyo3(signature = (job_ids, quantum_processor_id = None, client = None, execution_options = None))]
314    async fn cancel_jobs(
315        job_ids: Vec<String>,
316        quantum_processor_id: Option<String>,
317        client: Option<PyQcsClient>,
318        execution_options: Option<PyExecutionOptions>,
319    ) -> PyResult<()> {
320        let client = PyQcsClient::get_or_create_client(client);
321
322        qcs::qpu::api::cancel_jobs(
323            job_ids.into_iter().map(|id| id.into()).collect(),
324            quantum_processor_id.as_deref(),
325            &client,
326            execution_options.unwrap_or_default().as_inner()
327        )
328        .await
329        .map_err(RustQpuApiError::from).map_err(RustQpuApiError::to_py_err)?;
330
331        Ok(())
332    }
333}
334
335crate::py_sync::py_function_sync_async! {
336    #[pyfunction]
337    #[pyo3(signature = (job_id, quantum_processor_id = None, client = None, execution_options = None))]
338    async fn cancel_job(
339        job_id: String,
340        quantum_processor_id: Option<String>,
341        client: Option<PyQcsClient>,
342        execution_options: Option<PyExecutionOptions>,
343    ) -> PyResult<()> {
344        let client = PyQcsClient::get_or_create_client(client);
345
346        qcs::qpu::api::cancel_job(
347            job_id.into(),
348            quantum_processor_id.as_deref(),
349            &client,
350            execution_options.unwrap_or_default().as_inner()
351        )
352        .await
353        .map_err(RustQpuApiError::from).map_err(RustQpuApiError::to_py_err)?;
354
355        Ok(())
356    }
357}
358
359crate::py_sync::py_function_sync_async! {
360    #[pyfunction]
361    #[pyo3(signature = (job_id, quantum_processor_id = None, client = None, execution_options = None))]
362    #[pyo3_opentelemetry::pypropagate(on_context_extraction_failure="ignore")]
363    async fn retrieve_results(
364        job_id: String,
365        quantum_processor_id: Option<String>,
366        client: Option<PyQcsClient>,
367        execution_options: Option<PyExecutionOptions>
368    ) -> PyResult<ExecutionResults> {
369        let client = PyQcsClient::get_or_create_client(client);
370
371        let results = qcs::qpu::api::retrieve_results(job_id.into(), quantum_processor_id.as_deref(), &client, execution_options.unwrap_or_default().as_inner())
372            .await
373            .map_err(RustQpuApiError::from)
374            .map_err(RustQpuApiError::to_py_err)?;
375
376        Python::with_gil(|py| {
377            ExecutionResults::from_controller_job_execution_result(py, results)
378        })
379    }
380}
381
382py_wrap_type! {
383    #[derive(Debug, Default)]
384    #[pyo3(module = "qcs_sdk.qpu.api")]
385    PyExecutionOptions(ExecutionOptions) as "ExecutionOptions"
386}
387impl_repr!(PyExecutionOptions);
388impl_as_mut_for_wrapper!(PyExecutionOptions);
389
390py_wrap_type! {
391    #[derive(Debug, Default)]
392    PyApiExecutionOptions(ApiExecutionOptions) as "APIExecutionOptions"
393}
394impl_repr!(PyApiExecutionOptions);
395impl_as_mut_for_wrapper!(PyApiExecutionOptions);
396
397#[pymethods]
398impl PyExecutionOptions {
399    #[staticmethod]
400    fn default() -> Self {
401        Self::from(ExecutionOptions::default())
402    }
403
404    #[staticmethod]
405    fn builder() -> PyExecutionOptionsBuilder {
406        PyExecutionOptionsBuilder::default()
407    }
408
409    #[getter]
410    fn connection_strategy(&self) -> PyConnectionStrategy {
411        PyConnectionStrategy(self.as_inner().connection_strategy().clone())
412    }
413
414    #[getter]
415    fn timeout_seconds(&self) -> Option<f64> {
416        self.as_inner()
417            .timeout()
418            .map(|timeout| timeout.as_secs_f64())
419    }
420
421    #[getter]
422    fn api_options(&self) -> Option<PyApiExecutionOptions> {
423        self.as_inner()
424            .api_options()
425            .map(|x| PyApiExecutionOptions((*x).into()))
426    }
427
428    fn __richcmp__(&self, py: Python<'_>, other: &Self, op: CompareOp) -> PyObject {
429        match op {
430            CompareOp::Eq => (self.as_inner() == other.as_inner()).into_py(py),
431            _ => py.NotImplemented(),
432        }
433    }
434
435    fn __reduce__<'py>(&mut self, py: Python<'py>) -> PyResult<&'py PyTuple> {
436        let callable = py.get_type::<Self>().getattr("_from_parts")?;
437        Ok(PyTuple::new(
438            py,
439            [
440                callable,
441                PyTuple::new(
442                    py,
443                    &[
444                        self.connection_strategy().into_py(py),
445                        self.timeout_seconds().into_py(py),
446                        self.api_options().into_py(py),
447                    ],
448                ),
449            ],
450        ))
451    }
452
453    #[staticmethod]
454    fn _from_parts(
455        connection_strategy: PyConnectionStrategy,
456        timeout_seconds: Option<f64>,
457        api_options: Option<PyApiExecutionOptions>,
458    ) -> PyResult<Self> {
459        let mut builder = Self::builder();
460        builder.connection_strategy(connection_strategy);
461        builder.timeout_seconds(timeout_seconds);
462        builder.api_options(api_options);
463        builder.build()
464    }
465}
466
467#[pymethods]
468impl PyApiExecutionOptions {
469    #[staticmethod]
470    fn default() -> Self {
471        Self::from(ApiExecutionOptions::default())
472    }
473
474    #[staticmethod]
475    fn builder() -> PyApiExecutionOptionsBuilder {
476        PyApiExecutionOptionsBuilder::default()
477    }
478
479    #[getter]
480    fn bypass_settings_protection(&self) -> bool {
481        self.as_inner().bypass_settings_protection()
482    }
483
484    #[getter]
485    fn timeout(&self) -> Option<PyQpuApiDuration> {
486        self.as_inner()
487            .timeout()
488            .map(|inner| PyQpuApiDuration { inner })
489    }
490}
491
492py_wrap_type! {
493    PyExecutionOptionsBuilder(ExecutionOptionsBuilder) as "ExecutionOptionsBuilder"
494}
495
496#[pymethods]
497impl PyExecutionOptionsBuilder {
498    #[new]
499    fn new() -> Self {
500        Self::default()
501    }
502
503    #[staticmethod]
504    fn default() -> Self {
505        Self::from(ExecutionOptionsBuilder::default())
506    }
507
508    #[setter]
509    fn connection_strategy(&mut self, connection_strategy: PyConnectionStrategy) {
510        // `derive_builder::Builder` doesn't implement AsMut, meaning we can't use `PyWrapperMut`,
511        // which forces us into this awkward clone.
512
513        *self = Self::from(
514            self.as_inner()
515                .clone()
516                .connection_strategy(connection_strategy.as_inner().clone())
517                .clone(),
518        );
519    }
520
521    #[setter]
522    fn api_options(&mut self, api_options: Option<PyApiExecutionOptions>) {
523        *self = Self::from(
524            self.as_inner()
525                .clone()
526                .api_options(api_options.map(|x| x.into_inner().into()))
527                .clone(),
528        );
529    }
530
531    #[setter]
532    fn timeout_seconds(&mut self, timeout_seconds: Option<f64>) {
533        let timeout = timeout_seconds.map(Duration::from_secs_f64);
534        *self = Self::from(self.as_inner().clone().timeout(timeout).clone());
535    }
536
537    fn build(&self) -> PyResult<PyExecutionOptions> {
538        Ok(PyExecutionOptions::from(
539            self.as_inner()
540                .build()
541                .map_err(|err| PyValueError::new_err(err.to_string()))?,
542        ))
543    }
544}
545
546py_wrap_type! {
547    PyApiExecutionOptionsBuilder(ApiExecutionOptionsBuilder) as "APIExecutionOptionsBuilder"
548}
549
550#[pyclass(name = "QpuApiDuration")]
551#[derive(Debug, Clone)]
552pub struct PyQpuApiDuration {
553    inner: QpuApiDuration,
554}
555
556#[pymethods]
557impl PyQpuApiDuration {
558    #[new]
559    fn new(seconds: i64, nanos: i32) -> Self {
560        Self {
561            inner: QpuApiDuration { seconds, nanos },
562        }
563    }
564
565    #[getter]
566    fn seconds(&self) -> i64 {
567        self.inner.seconds
568    }
569
570    #[getter]
571    fn nanos(&self) -> i32 {
572        self.inner.nanos
573    }
574}
575
576#[pymethods]
577impl PyApiExecutionOptionsBuilder {
578    #[new]
579    fn new() -> Self {
580        Self::default()
581    }
582
583    #[staticmethod]
584    fn default() -> Self {
585        Self::from(ApiExecutionOptionsBuilder::default())
586    }
587
588    #[setter]
589    fn bypass_settings_protection(&mut self, bypass_settings_protection: bool) {
590        // `derive_builder::Builder` doesn't implement AsMut, meaning we can't use `PyWrapperMut`,
591        // which forces us into this awkward clone.
592
593        *self = Self::from(
594            self.as_inner()
595                .clone()
596                .bypass_settings_protection(bypass_settings_protection)
597                .clone(),
598        );
599    }
600
601    #[setter]
602    fn timeout(&mut self, timeout: Option<PyQpuApiDuration>) -> PyResult<()> {
603        *self = Self::from(
604            self.as_inner()
605                .clone()
606                .timeout(timeout.map(|timeout| timeout.inner))
607                .clone(),
608        );
609        Ok(())
610    }
611
612    fn build(&self) -> PyResult<PyApiExecutionOptions> {
613        Ok(PyApiExecutionOptions::from(
614            self.as_inner()
615                .build()
616                .map_err(|err| PyValueError::new_err(err.to_string()))?,
617        ))
618    }
619}
620
621py_wrap_type! {
622    #[derive(Debug, Default)]
623    #[pyo3(module = "qcs_sdk.qpu.api")]
624    PyConnectionStrategy(ConnectionStrategy) as "ConnectionStrategy"
625}
626impl_repr!(PyConnectionStrategy);
627
628#[pymethods]
629impl PyConnectionStrategy {
630    #[staticmethod]
631    #[pyo3(name = "default")]
632    fn py_default() -> Self {
633        Self::default()
634    }
635
636    #[staticmethod]
637    fn gateway() -> Self {
638        Self(ConnectionStrategy::Gateway)
639    }
640
641    fn is_gateway(&self) -> bool {
642        matches!(self.as_inner(), ConnectionStrategy::Gateway)
643    }
644
645    #[staticmethod]
646    fn direct_access() -> Self {
647        Self(ConnectionStrategy::DirectAccess)
648    }
649
650    fn is_direct_access(&self) -> bool {
651        matches!(self.as_inner(), ConnectionStrategy::DirectAccess)
652    }
653
654    #[staticmethod]
655    fn endpoint_id(endpoint_id: String) -> PyResult<Self> {
656        Ok(Self(ConnectionStrategy::EndpointId(endpoint_id)))
657    }
658
659    fn is_endpoint_id(&self) -> bool {
660        matches!(self.as_inner(), ConnectionStrategy::EndpointId(_))
661    }
662
663    fn get_endpoint_id(&self) -> PyResult<String> {
664        match self.as_inner() {
665            ConnectionStrategy::EndpointId(id) => Ok(id.clone()),
666            _ => Err(PyValueError::new_err(
667                "ConnectionStrategy is not an EndpointId",
668            )),
669        }
670    }
671
672    fn __richcmp__(&self, py: Python<'_>, other: &Self, op: CompareOp) -> PyObject {
673        match op {
674            CompareOp::Eq => (self.as_inner() == other.as_inner()).into_py(py),
675            _ => py.NotImplemented(),
676        }
677    }
678
679    fn __reduce__(&self, py: Python<'_>) -> PyResult<PyObject> {
680        Ok(match self.as_inner() {
681            ConnectionStrategy::Gateway => PyTuple::new(
682                py,
683                &[
684                    py.get_type::<Self>().getattr("gateway")?.to_object(py),
685                    PyTuple::empty(py).to_object(py),
686                ],
687            )
688            .to_object(py),
689            ConnectionStrategy::DirectAccess => PyTuple::new(
690                py,
691                &[
692                    py.get_type::<Self>()
693                        .getattr("direct_access")?
694                        .to_object(py),
695                    PyTuple::empty(py).to_object(py),
696                ],
697            )
698            .to_object(py),
699            ConnectionStrategy::EndpointId(endpoint_id) => PyTuple::new(
700                py,
701                &[
702                    py.get_type::<Self>().getattr("endpoint_id")?.to_object(py),
703                    PyTuple::new(py, [endpoint_id]).to_object(py),
704                ],
705            )
706            .to_object(py),
707        })
708    }
709}