use std::{collections::HashMap, num::NonZeroU16, str::FromStr, sync::Arc, time::Duration};
#[cfg(feature = "stubs")]
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
use quil_rs::{
instruction::{ArithmeticOperand, Instruction, MemoryReference, Move},
program::ProgramError,
quil::{Quil, ToQuilError},
Program,
};
use serde::{Deserialize, Serialize};
pub(crate) use execution::Execution;
use crate::{executable::Parameters, RegisterData};
use self::http::AddressRequest;
mod execution;
pub mod http;
#[cfg(feature = "libquil")]
pub mod libquil;
#[cfg(feature = "python")]
pub mod python;
const DEFAULT_QVM_TIMEOUT: Duration = Duration::from_secs(30);
#[async_trait::async_trait]
pub trait Client {
async fn get_version_info(&self, options: &QvmOptions) -> Result<String, Error>;
async fn run(
&self,
request: &http::MultishotRequest,
options: &QvmOptions,
) -> Result<http::MultishotResponse, Error>;
async fn run_and_measure(
&self,
request: &http::MultishotMeasureRequest,
options: &QvmOptions,
) -> Result<Vec<Vec<i64>>, Error>;
async fn measure_expectation(
&self,
request: &http::ExpectationRequest,
options: &QvmOptions,
) -> Result<Vec<f64>, Error>;
async fn get_wavefunction(
&self,
request: &http::WavefunctionRequest,
options: &QvmOptions,
) -> Result<Vec<u8>, Error>;
}
#[async_trait::async_trait]
impl<T: Client + Sync + Send> Client for Arc<T> {
async fn get_version_info(&self, options: &QvmOptions) -> Result<String, Error> {
self.as_ref().get_version_info(options).await
}
async fn run(
&self,
request: &http::MultishotRequest,
options: &QvmOptions,
) -> Result<http::MultishotResponse, Error> {
self.as_ref().run(request, options).await
}
async fn run_and_measure(
&self,
request: &http::MultishotMeasureRequest,
options: &QvmOptions,
) -> Result<Vec<Vec<i64>>, Error> {
self.as_ref().run_and_measure(request, options).await
}
async fn measure_expectation(
&self,
request: &http::ExpectationRequest,
options: &QvmOptions,
) -> Result<Vec<f64>, Error> {
self.as_ref().measure_expectation(request, options).await
}
async fn get_wavefunction(
&self,
request: &http::WavefunctionRequest,
options: &QvmOptions,
) -> Result<Vec<u8>, Error> {
self.as_ref().get_wavefunction(request, options).await
}
}
#[expect(clippy::module_name_repetitions, clippy::unsafe_derive_deserialize)]
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.qvm", name = "QVMResultData", get_all, frozen)
)]
pub struct QvmResultData {
pub(crate) memory: HashMap<String, RegisterData>,
}
#[cfg_attr(not(feature = "python"), optipy::strip_pyo3)]
#[cfg_attr(feature = "stubs", gen_stub_pymethods)]
#[cfg_attr(feature = "python", pyo3::pymethods)]
impl QvmResultData {
#[must_use]
#[staticmethod]
pub fn from_memory_map(memory: HashMap<String, RegisterData>) -> Self {
Self { memory }
}
}
impl QvmResultData {
#[must_use]
pub fn memory(&self) -> &HashMap<String, RegisterData> {
&self.memory
}
}
pub async fn run<C: Client + Send + Sync + ?Sized>(
quil: &str,
shots: NonZeroU16,
addresses: HashMap<String, AddressRequest>,
params: &Parameters,
measurement_noise: Option<(f64, f64, f64)>,
gate_noise: Option<(f64, f64, f64)>,
rng_seed: Option<i64>,
client: &C,
options: &QvmOptions,
) -> Result<QvmResultData, Error> {
#[cfg(feature = "tracing")]
tracing::debug!("parsing a program to be executed on the qvm");
let program = Program::from_str(quil).map_err(Error::Parsing)?;
run_program(
&program,
shots,
addresses,
params,
measurement_noise,
gate_noise,
rng_seed,
client,
options,
)
.await
}
pub async fn run_program<C: Client + ?Sized>(
program: &Program,
shots: NonZeroU16,
addresses: HashMap<String, AddressRequest>,
params: &Parameters,
measurement_noise: Option<(f64, f64, f64)>,
gate_noise: Option<(f64, f64, f64)>,
rng_seed: Option<i64>,
client: &C,
options: &QvmOptions,
) -> Result<QvmResultData, Error> {
#[cfg(feature = "tracing")]
tracing::debug!(
%shots,
?addresses,
?params,
"executing program on QVM"
);
let program = apply_parameters_to_program(program, params)?;
let request = http::MultishotRequest::new(
program.to_quil()?,
shots,
addresses,
measurement_noise,
gate_noise,
rng_seed,
);
client
.run(&request, options)
.await
.map(|response| QvmResultData::from_memory_map(response.registers))
}
pub fn apply_parameters_to_program(
program: &Program,
params: &Parameters,
) -> Result<Program, Error> {
let mut new_program = program.clone_without_body_instructions();
params.iter().try_for_each(|(name, values)| {
match program.memory_regions.get(name.as_ref()) {
Some(region) => {
if region.size.length == values.len() as u64 {
Ok(())
} else {
Err(Error::RegionSizeMismatch {
name: name.to_string(),
declared: region.size.length,
parameters: values.len(),
})
}
}
None => Err(Error::RegionNotFound { name: name.clone() }),
}
})?;
new_program.add_instructions(
params
.iter()
.flat_map(|(name, values)| {
values.iter().enumerate().map(move |(index, value)| {
Instruction::Move(Move {
destination: MemoryReference {
name: name.to_string(),
index: index as u64,
},
source: ArithmeticOperand::LiteralReal(*value),
})
})
})
.chain(program.body_instructions().cloned())
.collect::<Vec<_>>(),
);
Ok(new_program)
}
#[allow(clippy::module_name_repetitions)]
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.qvm", name = "QVMOptions")
)]
pub struct QvmOptions {
pub timeout: Option<Duration>,
}
impl QvmOptions {
#[must_use]
pub fn new() -> Self {
Self { timeout: None }
}
}
impl Default for QvmOptions {
fn default() -> Self {
Self {
timeout: Some(DEFAULT_QVM_TIMEOUT),
}
}
}
#[expect(clippy::large_enum_variant)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Error parsing Quil program: {0}")]
Parsing(#[from] ProgramError),
#[error("Error converting program to valid Quil: {0}")]
ToQuil(#[from] ToQuilError),
#[error("Shots must be a positive integer.")]
ShotsMustBePositive,
#[error("Declared region {name} has size {declared} but parameters have size {parameters}.")]
RegionSizeMismatch {
name: String,
declared: u64,
parameters: usize,
},
#[error("Could not find region {name} for parameter. Are you missing a DECLARE instruction?")]
RegionNotFound {
name: Box<str>,
},
#[error("Could not communicate with QVM at {qvm_url}")]
QvmCommunication {
qvm_url: String,
source: reqwest::Error,
},
#[error("QVM reported a problem running your program: {message}")]
Qvm {
message: String,
},
#[error("The client failed to make the request: {0}")]
Client(#[from] reqwest::Error),
}
#[cfg(test)]
mod test {
use std::{collections::HashMap, str::FromStr};
use quil_rs::{quil::Quil, Program};
use rstest::{fixture, rstest};
use super::apply_parameters_to_program;
#[fixture]
fn program() -> Program {
Program::from_str("DECLARE ro BIT[3]\nH 0").expect("should parse valid program")
}
#[rstest]
fn test_apply_empty_parameters_to_program(program: Program) {
let parameterized_program = apply_parameters_to_program(&program, &HashMap::new())
.expect("should not error for empty parameters");
assert_eq!(parameterized_program, program);
}
#[rstest]
fn test_apply_valid_parameters_to_program(program: Program) {
let params = HashMap::from([(Box::from("ro"), vec![1.0, 2.0, 3.0])]);
let parameterized_program = apply_parameters_to_program(&program, ¶ms)
.expect("should not error for empty parameters");
insta::assert_snapshot!(parameterized_program.to_quil_or_debug());
}
#[rstest]
fn test_apply_invalid_parameters_to_program(program: Program) {
let params = HashMap::from([(Box::from("ro"), vec![1.0])]);
apply_parameters_to_program(&program, ¶ms)
.expect_err("should error because ro has too few values");
let params = HashMap::from([(Box::from("ro"), vec![1.0, 2.0, 3.0, 4.0])]);
apply_parameters_to_program(&program, ¶ms)
.expect_err("should error because ro has too many values");
let params = HashMap::from([(Box::from("bar"), vec![1.0])]);
apply_parameters_to_program(&program, ¶ms)
.expect_err("should error because bar is not a declared memory region in the program");
}
}