Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(python): support TranslationOptions #307

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 41 additions & 6 deletions crates/lib/src/qpu/translation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use std::{collections::HashMap, time::Duration};
use qcs_api_client_grpc::{
models::controller::EncryptedControllerJob,
services::translation::{
translate_quil_to_encrypted_controller_job_request::NumShots,
TranslateQuilToEncryptedControllerJobRequest, TranslationOptions,
self, translate_quil_to_encrypted_controller_job_request::NumShots,
translation_options::TranslationBackend, TranslateQuilToEncryptedControllerJobRequest,
TranslationOptions as RdmTranslationOptions,
},
};
use qcs_api_client_openapi::{
Expand All @@ -33,25 +34,30 @@ pub struct EncryptedTranslationResult {
}

/// Translate a program, returning an encrypted and translated program.
pub async fn translate(
pub async fn translate<TO>(
quantum_processor_id: &str,
quil_program: &str,
num_shots: u32,
client: &Qcs,
translation_options: Option<TranslationOptions>,
) -> Result<EncryptedTranslationResult, GrpcClientError> {
translation_options: TO,
) -> Result<EncryptedTranslationResult, GrpcClientError>
where
TO: Into<Option<RdmTranslationOptions>>,
{
#[cfg(feature = "tracing")]
tracing::debug!(
%num_shots,
"translating program for {}",
quantum_processor_id,
);

let options = translation_options.into();

let request = TranslateQuilToEncryptedControllerJobRequest {
quantum_processor_id: quantum_processor_id.to_owned(),
num_shots: Some(NumShots::NumShotsValue(num_shots)),
quil_program: quil_program.to_owned(),
options: translation_options,
options,
};

let response = client
Expand Down Expand Up @@ -104,3 +110,32 @@ pub async fn get_quilt_calibrations(
})
.await?
}

/// Options available for Quil program translation.
///
/// This wraps [`RdmTranslationOptions`] in order to improve the user experience,
/// because the structs auto-generated by `prost` can be clumsy to use directly.
#[derive(Debug, Default)]
pub struct TranslationOptions {
inner: RdmTranslationOptions,
}

impl TranslationOptions {
/// Use the first-generation translation backend available on QCS since 2018.
pub fn use_backend_v1(&mut self) {
self.inner.translation_backend =
Some(TranslationBackend::V1(translation::BackendV1Options {}))
}

/// Use the second-generation translation backend available on QCS since 2023
pub fn use_backend_v2(&mut self) {
self.inner.translation_backend =
Some(TranslationBackend::V2(translation::BackendV2Options {}))
}
}

impl From<TranslationOptions> for RdmTranslationOptions {
fn from(options: TranslationOptions) -> Self {
options.inner
}
}
45 changes: 45 additions & 0 deletions crates/python/qcs_sdk/grpc/models/translation.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import Optional, final
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is orphaned. The grpc and models directory both need __init__.pyi files exporting models, and translation, respectively.


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@final

class TranslationOptions:
translation_backend: Optional[TranslationBackend] = None

...

@final
class TranslationBackend:
"""
Object specifying which translation backend to use to translate a particular program,
and for that given backend, what options to apply.

Variants:
``v1``: Corresponds to the V1 translation backend.
``v2``: Corresponds to the V2 translation backend.

Methods (each per variant):
- ``is_*``: if the underlying values are that type.
- ``as_*``: if the underlying values are that type, then those values, otherwise ``None``.
- ``to_*``: the underlying values as that type, raises ``ValueError`` if they are not.
- ``from_*``: wrap underlying values as this enum type.

"""

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def inner(self) -> Union[BackendV1Options, BackendV2Options]: ...

def is_v1(self) -> bool: ...
def is_v2(self) -> bool: ...
def as_v1(self) -> Optional[BackendV1Options]: ...
def as_v2(self) -> Optional[BackendV2Options]: ...
def to_v1(self) -> BackendV1Options: ...
def to_v2(self) -> BackendV2Options: ...
@staticmethod
def from_v1(inner: BackendV1Options) -> "TranslationBackend": ...
@staticmethod
def from_v2(inner: BackendV2Options) -> "TranslationBackend": ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@final

class BackendV1Options:
"""
Options for the V1 translation backend.
"""

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@final

class BackendV2Options:
"""
Options for the V2 translation backend.
"""
3 changes: 3 additions & 0 deletions crates/python/qcs_sdk/qpu/translation.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Dict, Optional, final

from qcs_sdk.client import QCSClient
from qcs_sdk.grpc.models.translation import TranslationOptions

class GetQuiltCalibrationsError(RuntimeError):
"""An error occured while fetching Quil-T calibrations."""
Expand Down Expand Up @@ -88,6 +89,7 @@ def translate(
num_shots: int,
quantum_processor_id: str,
client: Optional[QCSClient] = None,
translation_options: Optional[TranslationOptions] = None,
) -> TranslationResult:
"""
Translates a native Quil program into an executable program.
Expand All @@ -109,6 +111,7 @@ async def translate_async(
num_shots: int,
quantum_processor_id: str,
client: Optional[QCSClient] = None,
translation_options: Optional[TranslationOptions] = None,
) -> TranslationResult:
"""
Translates a native Quil program into an executable program.
Expand Down
8 changes: 8 additions & 0 deletions crates/python/src/grpc/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
use rigetti_pyo3::create_init_submodule;

pub mod models;

create_init_submodule! {
submodules: [
"models": models::init_submodule
],
}
8 changes: 8 additions & 0 deletions crates/python/src/grpc/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
use rigetti_pyo3::create_init_submodule;

pub mod controller;
pub mod translation;

create_init_submodule! {
submodules: [
"translation": translation::init_submodule
],
}
40 changes: 39 additions & 1 deletion crates/python/src/grpc/models/translation.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
use qcs_api_client_grpc::services::translation::{
translation_options::TranslationBackend, BackendV1Options, BackendV2Options, TranslationOptions,
};
use rigetti_pyo3::{py_wrap_data_struct, py_wrap_type, py_wrap_union_enum};
use rigetti_pyo3::{
create_init_submodule, py_wrap_data_struct, py_wrap_type, py_wrap_union_enum,
pyo3::{pymethods, PyResult},
};

py_wrap_type! {
#[derive(Default)]
PyBackendV1Options(BackendV1Options) as "BackendV1Options";
}

#[pymethods]
impl PyBackendV1Options {
#[new]
fn __new__() -> PyResult<Self> {
Ok(Self::default())
}
}

py_wrap_type! {
#[derive(Default)]
PyBackendV2Options(BackendV2Options) as "BackendV2Options";
}

#[pymethods]
impl PyBackendV2Options {
#[new]
fn __new__() -> PyResult<Self> {
Ok(Self::default())
}
}

py_wrap_union_enum! {
PyTranslationBackend(TranslationBackend) as "TranslationBackend" {
v1: V1 => PyBackendV1Options,
Expand All @@ -26,3 +45,22 @@ py_wrap_data_struct! {
translation_backend: Option<TranslationBackend> => Option<PyTranslationBackend>
}
}

#[pymethods]
impl PyTranslationOptions {
#[new]
fn __new__(translation_backend: Option<PyTranslationBackend>) -> PyResult<Self> {
Ok(Self(TranslationOptions {
translation_backend: translation_backend.map(|b| b.0),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically, we use the PyWrapper as_inner() method to get at the inner value of a wrapped type:

Suggested change
translation_backend: translation_backend.map(|b| b.0),
translation_backend: translation_backend.map(|b| b.as_inner()),

}))
}
}

create_init_submodule! {
classes: [
PyTranslationBackend,
PyTranslationOptions,
PyBackendV1Options,
PyBackendV2Options
],
}
1 change: 1 addition & 0 deletions crates/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ create_init_submodule! {
submodules: [
"client": client::init_submodule,
"compiler": compiler::init_submodule,
"grpc": grpc::init_submodule,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you are just carrying forward with an existing module and just actually exporting it here, and I know the plan is to add a wrapped type later, but I don't love exporting raw bindings to the API here, and especially exposing them as a "GRPC" module to users.

Other modules get coupled to it because by it's very nature it contains models that requests across the stack need, but the individual models are typically scoped to a single kind of request, so why have them all co-located? It's also ambiguous when you need to pull it in, since our services use a mix of REST and GRPC.

To me, it feels like we're exposing unnecessary implementation details to users and making it more complex than needed to reason about our API and how to use it. translation is a clean concept within the scope of our product but grpc is ambiguous and doesn't help users discover useful things about our API.

I could be convinced to re-export the needed GRPC model directly from the translation module, that way it is at least grouped within a concept, reducing the number of imports needed and making it easier to find the needed type with tooling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Went in that direction on a new PR, #308

"qpu": qpu::init_submodule,
"qvm": qvm::init_submodule
],
Expand Down