use std::borrow::Cow;
use std::collections::HashMap;
use std::num::NonZeroU16;
use std::sync::Arc;
use std::time::Duration;
#[cfg(feature = "stubs")]
use pyo3_stub_gen::derive::gen_stub_pyclass_enum;
use qcs_api_client_common::configuration::LoadError;
use quil_rs::quil::ToQuilError;
use crate::client::Qcs;
use crate::compiler::quilc::{self, CompilerOpts};
use crate::execution_data::{self, ResultData};
use crate::qpu::api::{ExecutionOptions, JobId};
use crate::qpu::translation::TranslationOptions;
use crate::qpu::ExecutionError;
use crate::qvm::http::AddressRequest;
use crate::{qpu, qvm};
use quil_rs::program::ProgramError;
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct Executable<'executable, 'execution> {
quil: Arc<str>,
shots: NonZeroU16,
readout_memory_region_names: Option<Vec<Cow<'executable, str>>>,
params: Parameters,
qcs_client: Option<Arc<Qcs>>,
quilc_client: Option<Arc<dyn quilc::Client + Send + Sync>>,
compiler_options: CompilerOpts,
qpu: Option<qpu::Execution<'execution>>,
qvm: Option<qvm::Execution>,
}
pub(crate) type Parameters = HashMap<Box<str>, Vec<f64>>;
impl<'executable> Executable<'executable, '_> {
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn from_quil<Quil: Into<Arc<str>>>(quil: Quil) -> Self {
Self {
quil: quil.into(),
shots: NonZeroU16::new(1).expect("value is non-zero"),
readout_memory_region_names: None,
params: Parameters::new(),
compiler_options: CompilerOpts::default(),
qpu: None,
qvm: None,
qcs_client: None,
quilc_client: None,
}
}
#[must_use]
pub fn read_from<S>(mut self, register: S) -> Self
where
S: Into<Cow<'executable, str>>,
{
let register = register.into();
#[cfg(feature = "tracing")]
tracing::trace!("reading from register {:?}", register);
let mut readouts = self.readout_memory_region_names.take().unwrap_or_default();
readouts.push(register);
self.readout_memory_region_names = Some(readouts);
self
}
pub fn with_parameter<Param: Into<Box<str>>>(
&mut self,
param_name: Param,
index: usize,
value: f64,
) -> &mut Self {
let param_name = param_name.into();
#[cfg(feature = "tracing")]
tracing::trace!("setting parameter {}[{}] to {}", param_name, index, value);
let mut values = self
.params
.remove(¶m_name)
.unwrap_or_else(|| vec![0.0; index]);
if index >= values.len() {
values.resize(index + 1, 0.0);
}
values[index] = value;
self.params.insert(param_name, values);
self
}
#[must_use]
pub fn with_qcs_client(mut self, client: Qcs) -> Self {
self.qcs_client = Some(Arc::from(client));
self
}
pub fn qcs_client(&mut self) -> Arc<Qcs> {
if let Some(client) = &self.qcs_client {
client.clone()
} else {
let client = Arc::new(Qcs::load());
self.qcs_client = Some(client.clone());
client
}
}
}
pub type ExecutionResult = Result<execution_data::ExecutionData, Error>;
impl Executable<'_, '_> {
#[must_use]
pub fn with_shots(mut self, shots: NonZeroU16) -> Self {
self.shots = shots;
self
}
#[must_use]
#[allow(trivial_casts)]
pub fn with_quilc_client<C: quilc::Client + Send + Sync + 'static>(
mut self,
client: Option<C>,
) -> Self {
self.quilc_client = client.map(|c| Arc::new(c) as _);
self
}
#[must_use]
pub fn compiler_options(mut self, options: CompilerOpts) -> Self {
self.compiler_options = options;
self
}
fn get_readouts(&self) -> &[Cow<'_, str>] {
self.readout_memory_region_names
.as_ref()
.map_or(&[Cow::Borrowed("ro")], Vec::as_slice)
}
pub async fn execute_on_qvm<V: qvm::Client + ?Sized>(&mut self, client: &V) -> ExecutionResult {
#[cfg(feature = "tracing")]
tracing::debug!(
num_shots = %self.shots,
"running Executable on QVM",
);
let qvm = if let Some(qvm) = self.qvm.take() {
qvm
} else {
qvm::Execution::new(&self.quil)?
};
let result = qvm
.run(
self.shots,
self.get_readouts()
.iter()
.map(|address| (address.to_string(), AddressRequest::IncludeAll()))
.collect(),
&self.params,
client,
)
.await;
self.qvm = Some(qvm);
result
.map_err(Error::from)
.map(|registers| execution_data::ExecutionData {
result_data: ResultData::Qvm(registers),
duration: None,
})
}
}
impl<'execution> Executable<'_, 'execution> {
async fn qpu_for_id<S>(&mut self, id: S) -> Result<qpu::Execution<'execution>, Error>
where
S: Into<Cow<'execution, str>>,
{
let id = id.into();
if let Some(qpu) = self.qpu.take() {
if qpu.quantum_processor_id == id.as_ref() && qpu.shots == self.shots {
return Ok(qpu);
}
}
qpu::Execution::new(
self.quil.clone(),
self.shots,
id,
self.qcs_client(),
self.quilc_client.clone(),
self.compiler_options,
)
.await
.map_err(Error::from)
}
pub async fn execute_on_qpu_with_endpoint<S>(
&mut self,
quantum_processor_id: S,
endpoint_id: S,
translation_options: Option<TranslationOptions>,
) -> ExecutionResult
where
S: Into<Cow<'execution, str>>,
{
let job_handle = self
.submit_to_qpu_with_endpoint(quantum_processor_id, endpoint_id, translation_options)
.await?;
self.retrieve_results(job_handle).await
}
pub async fn execute_on_qpu<S>(
&mut self,
quantum_processor_id: S,
translation_options: Option<TranslationOptions>,
execution_options: &ExecutionOptions,
) -> ExecutionResult
where
S: Into<Cow<'execution, str>>,
{
let quantum_processor_id = quantum_processor_id.into();
#[cfg(feature = "tracing")]
tracing::debug!(
num_shots = %self.shots,
%quantum_processor_id,
"running Executable on QPU",
);
let job_handle = self
.submit_to_qpu(quantum_processor_id, translation_options, execution_options)
.await?;
self.retrieve_results(job_handle).await
}
pub async fn submit_to_qpu<S>(
&mut self,
quantum_processor_id: S,
translation_options: Option<TranslationOptions>,
execution_options: &ExecutionOptions,
) -> Result<JobHandle<'execution>, Error>
where
S: Into<Cow<'execution, str>>,
{
let quantum_processor_id = quantum_processor_id.into();
#[cfg(feature = "tracing")]
tracing::debug!(
num_shots = %self.shots,
%quantum_processor_id,
"submitting Executable to QPU",
);
let job_handle = self
.qpu_for_id(quantum_processor_id)
.await?
.submit(&self.params, translation_options, execution_options)
.await?;
Ok(job_handle)
}
pub async fn submit_to_qpu_with_endpoint<S>(
&mut self,
quantum_processor_id: S,
endpoint_id: S,
translation_options: Option<TranslationOptions>,
) -> Result<JobHandle<'execution>, Error>
where
S: Into<Cow<'execution, str>>,
{
let job_handle = self
.qpu_for_id(quantum_processor_id)
.await?
.submit_to_endpoint_id(&self.params, endpoint_id.into(), translation_options)
.await?;
Ok(job_handle)
}
pub async fn cancel_qpu_job(&mut self, job_handle: JobHandle<'execution>) -> Result<(), Error> {
let quantum_processor_id = job_handle.quantum_processor_id.to_string();
let qpu = self.qpu_for_id(quantum_processor_id).await?;
Ok(qpu.cancel_job(job_handle).await?)
}
pub async fn retrieve_results(&mut self, job_handle: JobHandle<'execution>) -> ExecutionResult {
let quantum_processor_id = job_handle.quantum_processor_id.to_string();
let qpu = self.qpu_for_id(quantum_processor_id).await?;
qpu.retrieve_results(job_handle).await.map_err(Error::from)
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("There was a problem related to your QCS settings: {0}")]
Settings(String),
#[error("Could not authenticate a request to QCS for the requested QPU.")]
Authentication,
#[error("An API error occurred while connecting to the QPU: {0}")]
QpuApiError(#[from] qpu::api::QpuApiError),
#[error("QPU currently unavailable, retry after {} seconds", .0.as_secs())]
QpuUnavailable(Duration),
#[error("Error connecting to service {0:?}")]
Connection(Service),
#[error("There was a problem with the Quil program: {0}")]
Quil(#[from] ProgramError),
#[error("There was a problem converting the program to valid Quil: {0}")]
ToQuil(#[from] ToQuilError),
#[error("There was a problem compiling the Quil program: {0}")]
Compilation(String),
#[error("There was a problem translating the Quil program: {0}")]
Translation(String),
#[error("There was a problem substituting parameters in the Quil program: {0}")]
Substitution(String),
#[error("The Quil program is missing readout sources")]
MissingRoSources,
#[error("An unexpected error occurred, please open an issue on GitHub: {0:?}")]
Unexpected(String),
#[error("The job handle was not valid")]
InvalidJobHandle,
#[error("The QCS client configuration failed to load")]
QcsConfigLoadFailure(#[from] LoadError),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass_enum)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk", rename_all = "SCREAMING_SNAKE_CASE", eq)
)]
pub enum Service {
Quilc,
Qvm,
Qcs,
Qpu,
}
impl From<ExecutionError> for Error {
fn from(err: ExecutionError) -> Self {
match err {
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),
ExecutionError::ToQuil(e) => Self::ToQuil(e),
ExecutionError::Compilation { details } => Self::Compilation(details),
ExecutionError::RpcqClient(e) => Self::Unexpected(format!("{e:?}")),
ExecutionError::QpuApi(e) => Self::QpuApiError(e),
}
}
}
impl From<qvm::Error> for Error {
fn from(err: qvm::Error) -> Self {
match err {
qvm::Error::QvmCommunication { .. } | qvm::Error::Client { .. } => {
Self::Connection(Service::Qvm)
}
qvm::Error::ToQuil(q) => Self::ToQuil(q),
qvm::Error::Parsing(_)
| qvm::Error::ShotsMustBePositive
| qvm::Error::RegionSizeMismatch { .. }
| qvm::Error::RegionNotFound { .. }
| qvm::Error::Qvm { .. } => Self::Compilation(format!("{err}")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JobHandle<'executable> {
job_id: JobId,
quantum_processor_id: Cow<'executable, str>,
endpoint_id: Option<Cow<'executable, str>>,
readout_map: HashMap<String, String>,
execution_options: ExecutionOptions,
}
impl<'a> JobHandle<'a> {
#[must_use]
pub(crate) fn new<S>(
job_id: JobId,
quantum_processor_id: S,
endpoint_id: Option<S>,
readout_map: HashMap<String, String>,
execution_options: ExecutionOptions,
) -> Self
where
S: Into<Cow<'a, str>>,
{
Self {
job_id,
quantum_processor_id: quantum_processor_id.into(),
endpoint_id: endpoint_id.map(Into::into),
readout_map,
execution_options,
}
}
#[must_use]
pub fn job_id(&self) -> JobId {
self.job_id.clone()
}
#[must_use]
pub fn quantum_processor_id(&self) -> &str {
&self.quantum_processor_id
}
#[must_use]
pub fn readout_map(&self) -> &HashMap<String, String> {
&self.readout_map
}
#[must_use]
pub fn execution_options(&self) -> &ExecutionOptions {
&self.execution_options
}
}
#[cfg(test)]
#[cfg(feature = "manual-tests")]
mod describe_get_config {
use crate::client::Qcs;
use crate::{compiler::rpcq, Executable};
fn quilc_client() -> rpcq::Client {
let qcs = Qcs::load();
let endpoint = qcs.get_config().quilc_url();
rpcq::Client::new(endpoint).unwrap()
}
#[tokio::test]
async fn it_resizes_params_dynamically() {
let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client()));
exe.with_parameter("foo", 0, 0.0);
let params = exe.params.get("foo").unwrap().len();
assert_eq!(params, 1);
exe.with_parameter("foo", 10, 10.0);
let params = exe.params.get("foo").unwrap().len();
assert_eq!(params, 11);
}
}
#[cfg(test)]
#[cfg(feature = "manual-tests")]
mod describe_qpu_for_id {
use assert2::let_assert;
use std::num::NonZeroU16;
use crate::compiler::quilc::CompilerOpts;
use crate::compiler::rpcq;
use crate::qpu;
use crate::{client::Qcs, Executable};
fn quilc_client() -> rpcq::Client {
let qcs = Qcs::load();
let endpoint = qcs.get_config().quilc_url();
rpcq::Client::new(endpoint).unwrap()
}
#[tokio::test]
async fn it_refreshes_auth_token() {
let mut exe = Executable::from_quil("")
.with_qcs_client(Qcs::load())
.with_quilc_client(Some(quilc_client()));
let result = exe.qpu_for_id("blah").await;
let Err(err) = result else {
panic!("Expected an error!");
};
let result_string = format!("{err:?}");
assert!(result_string.contains("refresh_token"));
}
#[tokio::test]
async fn it_loads_cached_version() {
let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client()));
let shots = NonZeroU16::new(17).expect("value is non-zero");
exe.shots = shots;
exe.qpu = Some(
qpu::Execution::new(
"".into(),
shots,
"Aspen-M-3".into(),
exe.qcs_client(),
exe.quilc_client.clone(),
CompilerOpts::default(),
)
.await
.unwrap(),
);
let mut exe = exe.with_qcs_client(Qcs::default());
assert!(exe.qpu_for_id("Aspen-M-3").await.is_ok());
}
#[tokio::test]
async fn it_creates_new_after_shot_change() {
let original_shots = NonZeroU16::new(23).expect("value is non-zero");
let mut exe = Executable::from_quil("")
.with_quilc_client(Some(quilc_client()))
.with_shots(original_shots);
let qpu = exe.qpu_for_id("Aspen-9").await.unwrap();
assert_eq!(qpu.shots, original_shots);
exe.qpu = Some(qpu);
let new_shots = NonZeroU16::new(32).expect("value is non-zero");
exe = exe.with_shots(new_shots);
let qpu = exe.qpu_for_id("Aspen-9").await.unwrap();
assert_eq!(qpu.shots, new_shots);
}
#[tokio::test]
async fn it_creates_new_for_new_qpu_id() {
let mut exe = Executable::from_quil("").with_quilc_client(Some(quilc_client()));
let qpu = exe.qpu_for_id("Aspen-9").await.unwrap();
assert_eq!(qpu.quantum_processor_id, "Aspen-9");
exe.qpu = Some(qpu);
let mut exe = exe.with_qcs_client(Qcs::default());
let result = exe.qpu_for_id("Aspen-8").await;
let_assert!(Err(crate::executable::Error::Unexpected(err)) = result);
assert!(err.contains("NoRefreshToken"));
assert!(exe.qpu.is_none());
}
}