1use 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#[derive(Debug, thiserror::Error)]
62enum RustSubmissionError {
63 #[error("An API error occurred: {0}")]
65 QpuApiError(#[from] qcs::qpu::api::QpuApiError),
66
67 #[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 #[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 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#[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#[pyclass]
167#[derive(Clone, Debug)]
168pub struct ExecutionResult {
169 #[pyo3(get)]
171 pub shape: [usize; 2],
172 #[pyo3(get)]
174 pub data: PyRegister,
175 #[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 #[pyo3(get)]
227 pub buffers: HashMap<String, ExecutionResult>,
228 #[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 *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 *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}