Repository: rigetti/qcs-sdk-rust (language: python)

Issue:
Fetch calibration program using translation services gRPC endpoint
The translation module uses gRPC for all requests _except_ for fetching the calibration program. The latest release of the QCS gRPC client includes a request for getting the calibration program, we should use it.


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 8f4949ef1..75ebb2f40 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -380,6 +380,15 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "chrono"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "clang-sys"
 version = "0.29.3"
@@ -1817,6 +1826,21 @@ dependencies = [
  "prost-types",
 ]
 
+[[package]]
+name = "pbjson-types"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a88c8d87f99a4ac14325e7a4c24af190fca261956e3b82dd7ed67e77e6c7043"
+dependencies = [
+ "bytes",
+ "chrono",
+ "pbjson",
+ "pbjson-build",
+ "prost",
+ "prost-build",
+ "serde",
+]
+
 [[package]]
 name = "peeking_take_while"
 version = "0.1.2"
@@ -2135,9 +2159,9 @@ dependencies = [
 
 [[package]]
 name = "qcs-api-client-grpc"
-version = "0.7.9"
+version = "0.7.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37bd1842ca3485e8c0abf36be4cbcee1ea12e97da15ac731ce9dae20f7f58f41"
+checksum = "5639253e10f82014f416fdcc91c0aec0f4e276d81ebd8fdaca1b7277ba2269ad"
 dependencies = [
  "http",
  "http-body",
@@ -2149,6 +2173,7 @@ dependencies = [
  "opentelemetry_api",
  "pbjson",
  "pbjson-build",
+ "pbjson-types",
  "prost",
  "prost-build",
  "qcs-api-client-common",
@@ -2164,9 +2189,9 @@ dependencies = [
 
 [[package]]
 name = "qcs-api-client-openapi"
-version = "0.8.9"
+version = "0.8.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c605c4bdf887f487dbe7c0055f303728a992a6d508eedc55d7efa41b36adba07"
+checksum = "a4e51ee4c7359a1226ea601d3f080d4664d141a9b28dd7d2a3abb548ff100e44"
 dependencies = [
  "anyhow",
  "qcs-api-client-common",
diff --git a/Cargo.toml b/Cargo.toml
index 350b420b7..4aa7a89b5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,8 +5,8 @@ resolver = "2"
 [workspace.dependencies]
 qcs-api = "0.2.1"
 qcs-api-client-common = "0.7.9"
-qcs-api-client-grpc = "0.7.9"
-qcs-api-client-openapi = "0.8.9"
+qcs-api-client-grpc = "0.7.10"
+qcs-api-client-openapi = "0.8.10"
 serde_json = "1.0.86"
 thiserror = "1.0.37"
 tokio = "1.34.0"
diff --git a/crates/lib/src/executable.rs b/crates/lib/src/executable.rs
index 29c5064ee..fcae8dc97 100644
--- a/crates/lib/src/executable.rs
+++ b/crates/lib/src/executable.rs
@@ -715,6 +715,7 @@ impl From<ExecutionError> for Error {
             ExecutionError::Unexpected(inner) => Self::Unexpected(format!("{inner:?}")),
             ExecutionError::Quilc { .. } => Self::Connection(Service::Quilc),
             ExecutionError::QcsClient(v) => Self::Unexpected(format!("{v:?}")),
+            ExecutionError::Translation(v) => Self::Translation(v.to_string()),
             ExecutionError::Isa(v) => Self::Unexpected(format!("{v:?}")),
             ExecutionError::ReadoutParse(v) => Self::Unexpected(format!("{v:?}")),
             ExecutionError::Quil(e) => Self::Quil(e),
diff --git a/crates/lib/src/qpu/execution.rs b/crates/lib/src/qpu/execution.rs
index c3be03faa..e8b6e8f87 100644
--- a/crates/lib/src/qpu/execution.rs
+++ b/crates/lib/src/qpu/execution.rs
@@ -51,6 +51,8 @@ pub(crate) enum Error {
     Quilc { uri: String, details: String },
     #[error("Problem using QCS API: {0}")]
     QcsClient(#[from] GrpcClientError),
+    #[error(transparent)]
+    Translation(#[from] super::translation::Error),
     #[error("Problem fetching ISA: {0}")]
     Isa(#[from] GetIsaError),
     #[error("Problem parsing memory readout: {0}")]
diff --git a/crates/lib/src/qpu/translation.rs b/crates/lib/src/qpu/translation.rs
index fb54e54dc..730697059 100644
--- a/crates/lib/src/qpu/translation.rs
+++ b/crates/lib/src/qpu/translation.rs
@@ -8,17 +8,25 @@ use qcs_api_client_grpc::{
     services::translation::{
         translate_quil_to_encrypted_controller_job_request::NumShots,
         translation_options::TranslationBackend, BackendV1Options, BackendV2Options,
+        GetQuantumProcessorQuilCalibrationProgramRequest,
         TranslateQuilToEncryptedControllerJobRequest, TranslationOptions as ApiTranslationOptions,
     },
 };
-use qcs_api_client_openapi::{
-    apis::{translation_api, Error as OpenAPIError},
-    models::GetQuiltCalibrationsResponse,
-};
 use tokio::time::error::Elapsed;
 
 use crate::client::{GrpcClientError, Qcs, DEFAULT_HTTP_API_TIMEOUT};
 
+/// Errors that can occur when making a request to translation service.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    /// Error due to gRPC client
+    #[error(transparent)]
+    Grpc(#[from] GrpcClientError),
+    /// Error due to client timeout
+    #[error("Client configured timeout exceeded")]
+    ClientTimeout(#[from] Elapsed),
+}
+
 /// An encrypted and translated program, along with `readout_map`
 /// to map job `readout_data` back to program-declared variables.
 #[derive(Debug)]
@@ -40,7 +48,7 @@ pub async fn translate<TO>(
     num_shots: u32,
     client: &Qcs,
     translation_options: Option<TO>,
-) -> Result<EncryptedTranslationResult, GrpcClientError>
+) -> Result<EncryptedTranslationResult, Error>
 where
     TO: Into<ApiTranslationOptions>,
 {
@@ -61,9 +69,11 @@ where
     };
 
     let response = client
-        .get_translation_client()?
+        .get_translation_client()
+        .map_err(GrpcClientError::from)?
         .translate_quil_to_encrypted_controller_job(request)
-        .await?
+        .await
+        .map_err(GrpcClientError::from)?
         .into_inner();
 
     Ok(EncryptedTranslationResult {
@@ -77,36 +87,33 @@ where
     })
 }
 
-/// API Errors encountered when trying to get Quil-T calibrations.
-#[derive(Debug, thiserror::Error)]
-pub enum GetQuiltCalibrationsError {
-    /// Failed the http call
-    #[error("Failed to get Quil-T calibrations via API: {0}")]
-    ApiError(#[from] OpenAPIError<translation_api::GetQuiltCalibrationsError>),
-
-    /// API call did not finish before timeout
-    #[error("API call did not finish before timeout.")]
-    TimeoutError(#[from] Elapsed),
-}
-
 /// Query the QCS API for Quil-T calibrations.
 /// If `None`, the default `timeout` used is 10 seconds.
 pub async fn get_quilt_calibrations(
-    quantum_processor_id: &str,
+    quantum_processor_id: String,
     client: &Qcs,
     timeout: Option<Duration>,
-) -> Result<GetQuiltCalibrationsResponse, GetQuiltCalibrationsError> {
+) -> Result<String, Error> {
     #[cfg(feature = "tracing")]
     tracing::debug!("getting Quil-T calibrations for {}", quantum_processor_id);
 
     let timeout = timeout.unwrap_or(DEFAULT_HTTP_API_TIMEOUT);
 
+    let mut translation_client = client
+        .get_translation_client()
+        .map_err(GrpcClientError::from)?;
+
     tokio::time::timeout(timeout, async move {
-        Ok(translation_api::get_quilt_calibrations(
-            &client.get_openapi_client(),
-            quantum_processor_id,
-        )
-        .await?)
+        Ok(translation_client
+            .get_quantum_processor_quil_calibration_program(
+                GetQuantumProcessorQuilCalibrationProgramRequest {
+                    quantum_processor_id,
+                },
+            )
+            .await
+            .map_err(GrpcClientError::from)?
+            .into_inner()
+            .quil_calibration_program)
     })
     .await?
 }
diff --git a/crates/python/qcs_sdk/qpu/translation.pyi b/crates/python/qcs_sdk/qpu/translation.pyi
index 48b5e948c..73edc1113 100644
--- a/crates/python/qcs_sdk/qpu/translation.pyi
+++ b/crates/python/qcs_sdk/qpu/translation.pyi
@@ -3,33 +3,11 @@ from enum import Enum, auto
 
 from qcs_sdk.client import QCSClient
 
-class GetQuiltCalibrationsError(RuntimeError):
-    """An error occured while fetching Quil-T calibrations."""
-
-    ...
-
 class TranslationError(RuntimeError):
-    """An error occured while translating a program."""
+    """Errors that can occur while using the translation module."""
 
     ...
 
-@final
-class QuiltCalibrations:
-    """Result of `get_quilt_calibrations`."""
-
-    @property
-    def quilt(self) -> str:
-        """Calibrations suitable for use in a Quil-T program."""
-        ...
-    @quilt.setter
-    def quilt(self, value: str): ...
-    @property
-    def settings_timestamp(self) -> Optional[str]:
-        """ISO8601 timestamp of the settings used to generate these calibrations."""
-        ...
-    @settings_timestamp.setter
-    def settings_timestamp(self, value: Optional[str]): ...
-
 @final
 class TranslationResult:
     """
@@ -45,7 +23,6 @@ class TranslationResult:
         """A mapping from the program's memory references to the key used to index the results map."""
         ...
 
-
 @final
 class TranslationBackend(Enum):
     V1 = auto()
@@ -55,7 +32,7 @@ def get_quilt_calibrations(
     quantum_processor_id: str,
     client: Optional[QCSClient] = None,
     timeout: Optional[float] = None,
-) -> QuiltCalibrations:
+) -> str:
     """
     Retrieve the calibration data used for client-side Quil-T generation.
 
@@ -63,10 +40,10 @@ def get_quilt_calibrations(
     :param client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
     :param timeout: Maximum duration to wait for API calls to complete, in seconds.
 
-    :returns QuiltCalibrations:
+    :returns str: The Quil calibration program for the requested quantum processor.
 
     :raises LoadClientError: If there is an issue loading the QCS Client configuration.
-    :raises GetQuiltCalibrationsError: If there was a problem fetching Quil-T calibrations.
+    :raises TranslationError: If there was a problem fetching Quil-T calibrations.
     """
     ...
 
@@ -74,7 +51,7 @@ async def get_quilt_calibrations_async(
     quantum_processor_id: str,
     client: Optional[QCSClient] = None,
     timeout: Optional[float] = None,
-) -> QuiltCalibrations:
+) -> str:
     """
     Retrieve the calibration data used for client-side Quil-T generation.
     (async analog of ``get_quilt_calibrations``)
@@ -83,10 +60,10 @@ async def get_quilt_calibrations_async(
     :param client: The ``QCSClient`` to use. Creates one using environment configuration if unset - see https://docs.rigetti.com/qcs/references/qcs-client-configuration
     :param timeout: Maximum duration to wait for API calls to complete, in seconds.
 
-    :returns QuiltCalibrations:
+    :returns str: The Quil calibration program for the requested quantum processor.
 
     :raises LoadClientError: If there is an issue loading the QCS Client configuration.
-    :raises GetQuiltCalibrationsError: If there was a problem fetching Quil-T calibrations.
+    :raises TranslationError: If there was a problem fetching Quil-T calibrations.
     """
     ...
 
@@ -146,23 +123,19 @@ class TranslationOptions:
         """
         Get the selected translation backend
         """
-
     def use_backend_v1(self) -> None:
         """
         Use the v1 backend for translation, available on QCS since 2018.
         """
-
     def use_backend_v2(self) -> None:
         """
         Use the v2 backend for translation, available on QCS since 2023.
         """
-
     @staticmethod
     def v1() -> "TranslationOptions":
         """
         Use the v1 backend for translation, available on QCS since 2018.
         """
-
     @staticmethod
     def v2(
         *,
diff --git a/crates/python/src/qpu/translation.rs b/crates/python/src/qpu/translation.rs
index 412b9560a..6cc1f55f9 100644
--- a/crates/python/src/qpu/translation.rs
+++ b/crates/python/src/qpu/translation.rs
@@ -1,17 +1,10 @@
 //! Translating programs.
 use std::{collections::HashMap, time::Duration};
 
-use pyo3::{
-    exceptions::PyRuntimeError, pyclass, pyfunction, pymethods, types::PyString, Py, PyResult,
-};
-use qcs::client::GrpcClientError;
+use pyo3::{exceptions::PyRuntimeError, pyclass, pyfunction, pymethods, PyResult};
 use qcs::qpu::translation::TranslationOptions;
 use qcs_api_client_grpc::services::translation::translation_options::TranslationBackend as ApiTranslationBackend;
-use qcs_api_client_openapi::models::GetQuiltCalibrationsResponse;
-use rigetti_pyo3::{
-    create_init_submodule, py_wrap_data_struct, py_wrap_error, py_wrap_simple_enum, wrap_error,
-    ToPythonError,
-};
+use rigetti_pyo3::{create_init_submodule, py_wrap_error, py_wrap_simple_enum, ToPythonError};
 
 use crate::py_sync::py_function_sync_async;
 
@@ -19,13 +12,11 @@ use crate::client::PyQcsClient;
 
 create_init_submodule! {
     classes: [
-        PyQuiltCalibrations,
         PyTranslationOptions,
         PyTranslationResult,
         PyTranslationBackend
     ],
     errors: [
-        GetQuiltCalibrationsError,
         TranslationError
     ],
     funcs: [
@@ -36,23 +27,6 @@ create_init_submodule! {
     ],
 }
 
-py_wrap_data_struct! {
-    PyQuiltCalibrations(GetQuiltCalibrationsResponse) as "QuiltCalibrations" {
-        quilt: String => Py<PyString>,
-        settings_timestamp: Option<String> => Option<Py<PyString>>
-    }
-}
-
-wrap_error!(RustGetQuiltCalibrationsError(
-    qcs::qpu::translation::GetQuiltCalibrationsError
-));
-py_wrap_error!(
-    translation,
-    RustGetQuiltCalibrationsError,
-    GetQuiltCalibrationsError,
-    PyRuntimeError
-);
-
 py_function_sync_async! {
     /// Query the QCS API for Quil-T calibrations.
     /// If `None`, the default `timeout` used is 10 seconds.
@@ -62,26 +36,20 @@ py_function_sync_async! {
         quantum_processor_id: String,
         client: Option<PyQcsClient>,
         timeout: Option<f64>,
-    ) -> PyResult<PyQuiltCalibrations> {
+    ) -> PyResult<String> {
         let client = PyQcsClient::get_or_create_client(client).await;
         let timeout = timeout.map(Duration::from_secs_f64);
-        qcs::qpu::translation::get_quilt_calibrations(&quantum_processor_id, &client, timeout)
-            .await
-            .map(PyQuiltCalibrations::from)
-            .map_err(RustGetQuiltCalibrationsError::from)
-            .map_err(RustGetQuiltCalibrationsError::to_py_err)
+        qcs::qpu::translation::get_quilt_calibrations(quantum_processor_id, &client, timeout)
+            .await.map_err(RustTranslationError::from).map_err(RustTranslationError::to_py_err)
     }
 }
 
-/// Errors that can happen during translation
 #[derive(Debug, thiserror::Error)]
 pub enum RustTranslationError {
-    /// The program could not be translated
-    #[error("Could not translate quil: {0}")]
-    Translate(#[from] GrpcClientError),
-    /// The result of translation could not be deserialized
-    #[error("Could not serialize translation result: {0}")]
-    Serialize(#[from] serde_json::Error),
+    #[error(transparent)]
+    Translation(#[from] qcs::qpu::translation::Error),
+    #[error("Failed to serialize translation result: {0}")]
+    Serialization(#[from] serde_json::Error),
 }
 
 py_wrap_error!(


Test patch:
diff --git a/crates/lib/tests/mocked_qpu.rs b/crates/lib/tests/mocked_qpu.rs
index 79989b23e..421c77fe7 100644
--- a/crates/lib/tests/mocked_qpu.rs
+++ b/crates/lib/tests/mocked_qpu.rs
@@ -244,7 +244,9 @@ mod translation {
         Translation, TranslationServer,
     };
     use qcs_api_client_grpc::services::translation::{
-        TranslateQuilToEncryptedControllerJobRequest, TranslateQuilToEncryptedControllerJobResponse,
+        GetQuantumProcessorQuilCalibrationProgramRequest, QuantumProcessorQuilCalibrationProgram,
+        TranslateQuilToEncryptedControllerJobRequest,
+        TranslateQuilToEncryptedControllerJobResponse,
     };
     use tonic::{transport::Server, Request};
     use tonic::{Response, Status};
@@ -273,6 +275,17 @@ mod translation {
                 },
             ))
         }
+
+        async fn get_quantum_processor_quil_calibration_program(
+            &self,
+            _request: Request<GetQuantumProcessorQuilCalibrationProgramRequest>,
+        ) -> Result<Response<QuantumProcessorQuilCalibrationProgram>, Status> {
+            Ok(Response::new(QuantumProcessorQuilCalibrationProgram {
+                quil_calibration_program:
+                    "DEFCAL X 0:\nPULSE 0 \"xy\" gaussian(duration: 1, fwhm: 2, t0: 3)\n"
+                        .to_string(),
+            }))
+        }
     }
 
     pub(crate) async fn run() {
diff --git a/crates/python/tests/qpu/test_translation.py b/crates/python/tests/qpu/test_translation.py
index 1fe768f99..32540da77 100644
--- a/crates/python/tests/qpu/test_translation.py
+++ b/crates/python/tests/qpu/test_translation.py
@@ -1,7 +1,6 @@
 import pytest
 
 from qcs_sdk.qpu.translation import (
-    GetQuiltCalibrationsError,
     TranslationError,
     translate,
     translate_async,
@@ -47,7 +46,7 @@ def test_get_quilt_calibrations(
 
 @pytest.mark.qcs_session
 def test_get_quilt_calibrations_error():
-    with pytest.raises(GetQuiltCalibrationsError):
+    with pytest.raises(TranslationError):
         get_quilt_calibrations("--")
 
 
@@ -62,6 +61,7 @@ async def test_get_quilt_calibrations(
 
 def test_translation_backend():
     from qcs_sdk.qpu.translation import TranslationBackend, TranslationOptions
+
     opts = TranslationOptions()
     assert opts.backend is None
     opts.use_backend_v1()