use std::collections::HashMap;
use std::convert::TryFrom;
use quil_rs::program::{Program, ProgramError};
use serde::{Deserialize, Deserializer, Serialize};
use qcs_api_client_openapi::models::InstructionSetArchitecture;
use super::isa::{self, Compiler};
use super::rpcq;
#[cfg(feature = "stubs")]
use pyo3_stub_gen::derive::gen_stub_pyclass;
pub const DEFAULT_COMPILER_TIMEOUT: f64 = 30.0;
pub trait Client {
fn compile_program(
&self,
quil: &str,
isa: TargetDevice,
options: CompilerOpts,
) -> Result<CompilationResult, Error>;
fn get_version_info(&self) -> Result<String, Error>;
fn conjugate_pauli_by_clifford(
&self,
request: ConjugateByCliffordRequest,
) -> Result<ConjugatePauliByCliffordResponse, Error>;
fn generate_randomized_benchmarking_sequence(
&self,
request: RandomizedBenchmarkingRequest,
) -> Result<GenerateRandomizedBenchmarkingSequenceResponse, Error>;
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.compiler.quilc", get_all, frozen)
)]
pub struct CompilationResult {
pub program: Program,
pub native_quil_metadata: Option<NativeQuilMetadata>,
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.compiler.quilc", frozen)
)]
pub struct CompilerOpts {
pub(crate) timeout: Option<f64>,
pub(crate) protoquil: Option<bool>,
}
impl CompilerOpts {
#[must_use]
pub fn new() -> Self {
Self {
timeout: None,
protoquil: None,
}
}
#[must_use]
pub fn with_timeout(&mut self, seconds: Option<f64>) -> Self {
self.timeout = seconds;
*self
}
#[must_use]
pub fn with_protoquil(&mut self, protoquil: Option<bool>) -> Self {
self.protoquil = protoquil;
*self
}
}
impl Default for CompilerOpts {
fn default() -> Self {
Self {
timeout: Some(DEFAULT_COMPILER_TIMEOUT),
protoquil: None,
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd)]
#[serde(tag = "_type")]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.compiler.quilc", frozen, get_all)
)]
pub struct PauliTerm {
pub indices: Vec<u64>,
pub symbols: Vec<String>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd)]
#[serde(tag = "_type")]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.compiler.quilc", frozen, get_all)
)]
pub struct ConjugateByCliffordRequest {
pub pauli: PauliTerm,
pub clifford: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.compiler.quilc", frozen, get_all)
)]
pub(crate) struct ConjugatePauliByCliffordRequest {
#[serde(rename = "*args")]
args: [ConjugateByCliffordRequest; 1],
}
impl From<ConjugateByCliffordRequest> for ConjugatePauliByCliffordRequest {
fn from(value: ConjugateByCliffordRequest) -> Self {
Self { args: [value] }
}
}
#[derive(Clone, Deserialize, Debug, PartialEq, PartialOrd)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.compiler.quilc", frozen, get_all)
)]
pub struct ConjugatePauliByCliffordResponse {
pub phase: i64,
pub pauli: String,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd)]
#[serde(tag = "_type")]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.compiler.quilc", frozen, get_all)
)]
pub struct RandomizedBenchmarkingRequest {
pub depth: u64,
pub qubits: u64,
pub gateset: Vec<String>,
pub seed: Option<u64>,
pub interleaver: Option<String>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd)]
pub(crate) struct GenerateRandomizedBenchmarkingSequenceRequest {
#[serde(rename = "*args")]
args: [RandomizedBenchmarkingRequest; 1],
}
impl From<RandomizedBenchmarkingRequest> for GenerateRandomizedBenchmarkingSequenceRequest {
fn from(value: RandomizedBenchmarkingRequest) -> Self {
Self { args: [value] }
}
}
#[derive(Clone, Deserialize, Debug, PartialEq, PartialOrd)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.compiler.quilc", frozen, get_all)
)]
pub struct GenerateRandomizedBenchmarkingSequenceResponse {
pub sequence: Vec<Vec<i64>>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(
"Problem converting ISA to quilc format. This is a bug in this library or in QCS: {0}"
)]
Isa(#[from] isa::Error),
#[error("Problem connecting to quilc at {0}: {1}")]
QuilcConnection(String, #[source] rpcq::Error),
#[error("Problem compiling quil program: {0}")]
QuilcCompilation(CompilationError),
#[error("Problem when trying to parse the compiled program: {0}")]
Parse(ProgramError),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CompilationError {
#[cfg(feature = "libquil")]
#[error("compilation error from libquil: {0}")]
Libquil(crate::compiler::libquil::Error),
#[error("compilation error from RPCQ: {0}")]
Rpcq(rpcq::Error),
}
#[derive(Clone, Deserialize, Debug, PartialEq, PartialOrd)]
pub(crate) struct QuilToNativeQuilResponse {
pub(crate) quil: String,
#[serde(default)]
pub(crate) metadata: Option<NativeQuilMetadata>,
}
#[allow(unused_qualifications)]
fn deserialize_none_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + std::default::Default,
{
let opt = Option::deserialize(deserializer)?;
Ok(opt.unwrap_or_default())
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, PartialOrd)]
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.compiler.quilc", eq)
)]
pub struct NativeQuilMetadata {
#[serde(deserialize_with = "deserialize_none_as_default")]
pub final_rewiring: Vec<u64>,
pub gate_depth: Option<u64>,
pub gate_volume: Option<u64>,
pub multiqubit_gate_depth: Option<u64>,
pub program_duration: Option<f64>,
pub program_fidelity: Option<f64>,
pub topological_swaps: Option<u64>,
pub qpu_runtime_estimation: Option<f64>,
}
#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) struct QuilcVersionResponse {
pub(crate) quilc: String,
}
#[derive(Serialize, Debug, Clone, PartialEq)]
pub(crate) struct QuilcParams {
pub(crate) protoquil: Option<bool>,
#[serde(rename = "*args")]
args: [NativeQuilRequest; 1],
}
impl QuilcParams {
pub(crate) fn new(quil: &str, isa: TargetDevice) -> Self {
Self {
protoquil: None,
args: [NativeQuilRequest::new(quil, isa)],
}
}
pub(crate) fn with_protoquil(self, protoquil: Option<bool>) -> Self {
Self { protoquil, ..self }
}
}
#[derive(Serialize, Debug, Clone, PartialEq)]
#[serde(tag = "_type")]
struct NativeQuilRequest {
quil: String,
target_device: TargetDevice,
}
impl NativeQuilRequest {
fn new(quil: &str, target_device: TargetDevice) -> Self {
Self {
quil: String::from(quil),
target_device,
}
}
}
#[cfg_attr(feature = "stubs", gen_stub_pyclass)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "qcs_sdk.compiler.quilc", frozen)
)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(tag = "_type")]
pub struct TargetDevice {
isa: Compiler,
specs: HashMap<String, String>,
}
impl TryFrom<InstructionSetArchitecture> for TargetDevice {
type Error = Error;
fn try_from(isa: InstructionSetArchitecture) -> Result<Self, Self::Error> {
Ok(Self {
isa: Compiler::try_from(isa)?,
specs: HashMap::new(),
})
}
}
#[cfg(test)]
mod tests {
use crate::qvm::{self, http::AddressRequest};
use super::*;
use crate::client::Qcs;
use qcs_api_client_openapi::models::InstructionSetArchitecture;
use quil_rs::quil::Quil;
use regex::Regex;
use std::{fs::File, num::NonZeroU16};
const EXPECTED_H0_OUTPUT: &str = "MEASURE 0\n";
fn aspen_9_isa() -> InstructionSetArchitecture {
serde_json::from_reader(File::open("tests/aspen_9_isa.json").unwrap()).unwrap()
}
pub(crate) fn qvm_isa() -> InstructionSetArchitecture {
serde_json::from_reader(File::open("tests/qvm_isa.json").unwrap()).unwrap()
}
fn rpcq_client() -> rpcq::Client {
let qcs = Qcs::load();
let endpoint = qcs.get_config().quilc_url();
rpcq::Client::new(endpoint).unwrap()
}
#[tokio::test]
async fn compare_native_quil_to_expected_output() {
let output = rpcq_client()
.compile_program(
"MEASURE 0",
TargetDevice::try_from(qvm_isa()).expect("Couldn't build target device from ISA"),
CompilerOpts::default(),
)
.expect("Could not compile");
assert_eq!(output.program.to_quil_or_debug(), EXPECTED_H0_OUTPUT);
}
const BELL_STATE: &str = r"DECLARE ro BIT[2]
H 0
CNOT 0 1
MEASURE 0 ro[0]
MEASURE 1 ro[1]
";
#[tokio::test]
async fn run_compiled_bell_state_on_qvm() {
let client = Qcs::load();
let client = qvm::http::HttpClient::from(&client);
let output = rpcq_client()
.compile_program(
BELL_STATE,
TargetDevice::try_from(aspen_9_isa())
.expect("Couldn't build target device from ISA"),
CompilerOpts::default(),
)
.expect("Could not compile");
let mut results = qvm::Execution::new(&output.program.to_quil_or_debug())
.unwrap()
.run(
NonZeroU16::new(10).expect("value is non-zero"),
[("ro".to_string(), AddressRequest::IncludeAll())]
.iter()
.cloned()
.collect(),
&HashMap::default(),
&client,
)
.await
.expect("Could not run program on QVM");
for shot in results
.memory
.remove("ro")
.expect("Did not receive ro buffer")
.into_i8()
.unwrap()
{
assert_eq!(shot.len(), 2);
assert_eq!(shot[0], shot[1]);
}
}
#[tokio::test]
async fn test_compile_declare_only() {
let output = rpcq_client()
.compile_program(
"DECLARE ro BIT[1]\n",
TargetDevice::try_from(aspen_9_isa())
.expect("Couldn't build target device from ISA"),
CompilerOpts::default(),
)
.expect("Should be able to compile");
assert_eq!(output.program.to_quil_or_debug(), "DECLARE ro BIT[1]\n");
assert_ne!(output.native_quil_metadata, None);
}
#[tokio::test]
async fn get_version_info_from_quilc() {
let rpcq_client = rpcq_client();
let version = rpcq_client
.get_version_info()
.expect("Should get version info from quilc");
let semver_re = Regex::new(r"^([0-9]+)\.([0-9]+)\.([0-9]+)$").unwrap();
assert!(semver_re.is_match(&version));
}
#[tokio::test]
async fn test_conjugate_pauli_by_clifford() {
let rpcq_client = rpcq_client();
let request = ConjugateByCliffordRequest {
pauli: PauliTerm {
indices: vec![0],
symbols: vec!["X".into()],
},
clifford: "H 0".into(),
};
let response = rpcq_client
.conjugate_pauli_by_clifford(request)
.expect("Should conjugate pauli by clifford");
assert_eq!(
response,
ConjugatePauliByCliffordResponse {
phase: 0,
pauli: "Z".into(),
}
);
}
#[tokio::test]
async fn test_generate_randomized_benchmark_sequence() {
let rpcq_client = rpcq_client();
let request = RandomizedBenchmarkingRequest {
depth: 2,
qubits: 1,
gateset: vec!["X 0", "H 0"].into_iter().map(String::from).collect(),
seed: Some(314),
interleaver: Some("Y 0".into()),
};
let response = rpcq_client
.generate_randomized_benchmarking_sequence(request)
.expect("Should generate randomized benchmark sequence");
assert_eq!(
response,
GenerateRandomizedBenchmarkingSequenceResponse {
sequence: vec![vec![1, 0], vec![0, 1, 0, 1], vec![1, 0]],
}
);
}
}